sulfite_report_manager
Pacote Flutter que oferece widgets prontos para listar, buscar, criar, editar, excluir e visualizar relatórios Sulfite em qualquer aplicação, com suporte a múltiplos backends de armazenamento e sistema de permissões granular.
Instalação
dependencies:
sulfite_report_manager:
git:
url: https://github.com/rafaelgazani/sulfite.git
path: packages/sulfite_report_managerUso rápido
import 'package:sulfite_report_manager/sulfite_report_manager.dart';
import 'package:sulfite_studio/sulfite_studio.dart';
ReportManagerWidget(
repository: SqliteReportRepository(db),
canInsert: true,
canEdit: true,
canDelete: true,
viewerBuilder: (context, entry, definition) => SulfiteConsumerScreen(
report: definition,
title: entry.name,
dataSourceResolvers: [RestDataSourceResolver()],
onExportPdf: () => _exportPdf(definition),
),
)ReportManagerWidget
Widget principal que renderiza a tela de gerenciamento completa (AppBar + barra de busca + lista + FAB).
Parâmetros
| Parâmetro | Tipo | Padrão | Descrição |
|---|---|---|---|
repository | ReportRepository | — | Backend de armazenamento (obrigatório) |
canInsert | bool | false | Exibe FAB para criar novo relatório |
canEdit | bool | false | Exibe ícone de edição em cada item da lista |
canDelete | bool | false | Exibe ícone de exclusão em cada item da lista |
canSettings | bool | false | Exibe ícone de configurações na AppBar |
title | String | 'Relatórios' | Título exibido na AppBar |
onSettings | VoidCallback? | null | Substituí o dialog padrão ao clicar em configurações |
viewerBuilder | Widget Function(BuildContext, ReportEntry, ReportDefinition)? | null | Builder chamado ao tocar em um relatório — recebe a definição já parseada |
Mapa de permissões
canInsert → FAB "Novo relatório" na lista
canEdit → Ícone de edição em cada item (editar metadados: nome/descrição)
canDelete → Ícone de exclusão em cada item
canSettings → Ícone de configurações na AppBar (onSettings ou dialog padrão)Fluxo de visualização
Ao tocar em um relatório na lista (com viewerBuilder fornecido):
- Abre imediatamente uma rota com loading — sem bloquear a lista
- Aguarda a animação de transição terminar (sem jank)
- Busca o conteúdo completo do relatório (
findById) em background - Parseia a
ReportDefinition - Chama
viewerBuilder(context, entry, definition)→ exibe o widget retornado
O viewerBuilder recebe o ReportEntry completo e a ReportDefinition já parseada. O app controla tudo sobre a visualização — resolvers, botões de exportação, botão de editar no designer, etc.
ReportManagerWidget(
repository: repository,
canInsert: isAdmin,
canEdit: isAdmin,
canDelete: isAdmin,
canSettings: isAdmin,
onSettings: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => ConnectionManagerWidget(registry: myRegistry),
),
),
viewerBuilder: (ctx, entry, definition) {
return SulfiteConsumerScreen(
report: definition,
title: entry.name,
dataSourceResolvers: [
PostgresDataSourceResolver(registry: myRegistry),
RestDataSourceResolver(),
],
onExportPdf: () => _exportPdf(definition),
onExportExcel: () => _exportExcel(definition),
);
},
)Abrir o Designer a partir do visualizador
Use o viewerBuilder para compor o SulfiteConsumerScreen com um botão de edição que abre o Designer:
viewerBuilder: (ctx, entry, definition) {
return SulfiteConsumerScreen(
report: definition,
title: entry.name,
dataSourceResolvers: [RestDataSourceResolver()],
// Quando canSettings: true no manager, exponha o botão de editar:
onEditReport: isAdmin
? () => Navigator.of(ctx).push(
MaterialPageRoute(
builder: (_) => SulfiteDesigner(
initialReport: definition,
onSave: (updated) async {
await repository.update(
entry.id,
ReportEntryDraft(
name: entry.name,
definitionJson: jsonEncode(updated.toJson()),
),
);
},
),
),
)
: null,
);
}Backends de armazenamento
O pacote fornece quatro implementações prontas de ReportRepository:
InMemoryReportRepository
Para testes, protótipos e demos. Dados não persistem entre sessões.
final repo = InMemoryReportRepository();
await repo.insert(ReportEntryDraft(
name: 'Meu Relatório',
definitionJson: jsonEncode(myReport.toJson()),
));SqliteReportRepository
Persiste localmente via sqflite. Ideal para apps mobile/desktop offline.
final db = await openDatabase('reports.db');
final repo = SqliteReportRepository(db);RestReportRepository
Conecta a uma API REST. Espera endpoints padrão:
| Método | Endpoint | Descrição |
|---|---|---|
GET | /reports | Lista todos |
GET | /reports?q={query} | Busca |
GET | /reports/{id} | Busca por ID (com conteúdo) |
POST | /reports | Cria |
PUT | /reports/{id} | Atualiza |
DELETE | /reports/{id} | Exclui |
final repo = RestReportRepository(baseUrl: 'https://api.example.com');PostgresReportRepository
Conecta diretamente ao PostgreSQL via postgres package. Recomendado para servidores/desktop apps.
final conn = await Connection.open(...);
final repo = PostgresReportRepository(conn);
await repo.migrate(); // cria a tabela se não existirImplementar seu próprio repository
Implemente a interface ReportRepository:
abstract class ReportRepository {
Future<List<ReportEntry>> listAll();
Future<List<ReportEntry>> search(String query);
Future<ReportEntry?> findById(String id);
Future<ReportEntry> insert(ReportEntryDraft draft);
Future<ReportEntry> update(String id, ReportEntryDraft draft);
Future<void> delete(String id);
Future<void> dispose() async {}
}Contrato lazy-loading:
listAllesearchretornam entradas sem conteúdo (packageBytesedefinitionJsoncomonull). SomentefindByIdretorna o conteúdo completo. O manager chamafindByIdautomaticamente antes de chamarviewerBuilder.
Modelo ReportEntry
| Campo | Tipo | Descrição |
|---|---|---|
id | String | Identificador único |
name | String | Nome exibido na lista |
description | String? | Descrição opcional |
createdAt | DateTime | Data de criação |
updatedAt | DateTime | Data da última atualização |
storageKind | ReportStorageKind | package (.sulfite) ou json |
packageBytes | Uint8List? | Bytes do .sulfite (lazy — só em findById) |
definitionJson | String? | JSON da definição (lazy — só em findById) |
Formato .sulfite (RFC-017)
O manager suporta nativamente relatórios no formato .sulfite (pacote ZIP com definição + assets embutidos). Ao abrir um relatório com storageKind: package, o SulfitePackageReader extrai a definição automaticamente.
// Importar um .sulfite existente
final bytes = await File('relatorio.sulfite').readAsBytes();
await repo.insert(ReportEntryDraft(
name: 'Meu Relatório',
packageBytes: bytes,
));Gerenciar conexões em runtime
O pacote exporta MutableConnectionRegistry e ConnectionManagerWidget para que o app consumidor possa adicionar, editar e remover conexões de banco de dados ou HTTP em tempo de execução — sem reiniciar o app.
MutableConnectionRegistry
ChangeNotifier que implementa ConnectionRegistry. Usado como fonte de conexões nomeadas pelos resolvers do sulfite_datasources.
final registry = MutableConnectionRegistry([
ConnectionEntry.database(
name: 'producao',
host: 'db.example.com',
port: 5432,
database: 'app_db',
username: 'app_user',
password: 'secret',
ssl: true,
extra: {'schema': 'public'},
),
]);
// Passar para o resolver — os datasources com connectionRef usarão este registry
final resolver = PostgresDataSourceResolver(registry: registry);
// Adicionar ou substituir:
registry.upsert(
ConnectionEntry.database(name: 'homologacao', host: 'db-hom.example.com', ...),
);
// Remover pelo nome:
registry.remove('homologacao');| Método | Descrição |
|---|---|
upsert(entry) | Adiciona ou substitui a conexão com o mesmo name e notifica listeners |
remove(name) | Remove pelo nome; não-op se não existir |
entries | Cópia imutável da lista atual |
resolve(ref) | Busca conexão pelo name (implementa ConnectionRegistry) |
Persistência: as conexões ficam em memória. Para persistir entre sessões, salve e restaure a lista no
initStatedo app.
ConnectionManagerWidget
Tela pronta (Scaffold + AppBar + lista + FAB) para gerenciar as conexões de um MutableConnectionRegistry. Exibe conexões PostgreSQL e HTTP/REST com ações de adicionar, editar, remover e testar.
ConnectionManagerWidget(
registry: myRegistry,
onTestConnection: (entry) async {
// Retorne null se OK, ou uma mensagem de erro
try {
await PostgresDataSourceResolver(
registry: MutableConnectionRegistry([entry]),
).resolve(
DataSource(
id: '_ping', type: 'list', schema: {},
source: 'external', connectionRef: entry.name,
query: 'SELECT 1',
),
);
return null;
} catch (e) {
return e.toString();
}
},
)Parâmetros
| Parâmetro | Tipo | Descrição |
|---|---|---|
registry | MutableConnectionRegistry | Registry mutável que será exibido e editado |
onTestConnection | Future<String?> Function(ConnectionEntry)? | Callback de teste; retorna null em sucesso ou mensagem de erro |
Formulário de conexão
PostgreSQL
| Campo | Descrição |
|---|---|
| Nome | Identificador único — deve coincidir com connectionRef nos datasources |
| Host / Porta / Database | Endereço e banco |
| Usuário / Senha | Autenticação |
| Schema PostgreSQL | Opcional — substituído em {_schema} nas queries |
| Usar SSL | Liga/desliga SSL |
HTTP / REST
| Campo | Descrição |
|---|---|
| Nome | Identificador único |
| Base URL | URL base usada pelos datasources HTTP com connectionRef |
O nome de uma conexão é imutável após criado — serve como chave de lookup. Para "renomear", remova a antiga e crie uma nova.
Integrando com SulfiteDesigner
Passe o ConnectionManagerWidget via onSettings do manager ou diretamente no slot reportManager do designer:
final registry = MutableConnectionRegistry();
SulfiteDesigner(
initialReport: report,
dataSourceResolvers: [
PostgresDataSourceResolver(registry: registry),
RestDataSourceResolver(),
],
reportManager: ReportManagerWidget(
repository: SqliteReportRepository(db),
canInsert: true,
canEdit: true,
canDelete: true,
canSettings: true,
onSettings: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => ConnectionManagerWidget(registry: registry),
),
),
viewerBuilder: (ctx, entry, def) => SulfiteConsumerScreen(
report: def,
title: entry.name,
dataSourceResolvers: [
PostgresDataSourceResolver(registry: registry),
],
),
),
)