Tutorial: Relatório com API externa
Neste tutorial você criará um relatório de câmbio USD/BRL que busca dados em tempo real de uma API pública, enriquece e ordena os registros via script e exibe um gráfico de linha com tabela de cotações. Usa o exemplo 10-forex do repositório.
O que você vai aprender:
- Declarar um data source externo com
source: "external"eurl - Usar
RestDataSourceResolverdo pacotesulfite_datasources - Chamar
SulfiteEngine.generate()com resolvers injetados - Enriquecer e filtrar dados com um script
afterQuery - Exibir gráfico de linha + tabela em uma
SummaryBand
Pré-requisitos
- Sulfite instalado (Instalação →)
- Instale o pacote de resolvers externos:
dependencies:
sulfite_core: ^<versão>
sulfite_datasources: ^<versão>Passo 1 — Data source externo
Declare o data source com source: "external" e forneça a URL da API. O schema pode ser vazio {} quando os campos são inferidos em runtime:
{
"dataSources": [
{
"id": "cotacoes",
"type": "list",
"source": "external",
"url": "https://economia.awesomeapi.com.br/json/daily/USD-BRL/15",
"schema": {}
}
]
}A URL pode conter placeholders {paramName} que serão substituídos por parâmetros em runtime. Consulte Filtros de impressão para detalhes.
Passo 2 — Script afterQuery para enriquecer os dados
Declare o script no JSON e registre o handler em Dart para enriquecer, ordenar e limitar os registros:
{
"scripts": [
{ "id": "preparar_cotacoes", "hook": "afterQuery" }
]
}engine.register('preparar_cotacoes', (ctx) {
final rows = ctx.datasource('cotacoes')
.map((r) => {
...r,
// date_label usa create_date quando disponível, fallback para timestamp
'date_label': r['create_date'] ?? r['timestamp'],
})
.toList()
..sort((a, b) {
final ta = int.tryParse(a['timestamp'].toString()) ?? 0;
final tb = int.tryParse(b['timestamp'].toString()) ?? 0;
return tb.compareTo(ta); // decrescente
});
ctx.setDatasource('cotacoes', rows.take(10).toList());
});- Cria
date_labelcom fallback entrecreate_dateetimestamp. - Ordena por
timestampdecrescente. - Restringe para os 10 registros mais recentes.
O resultado é um array ordenado e enriquecido, pronto para ser vinculado ao gráfico e à tabela.
Passo 3 — Header com variável de sistema
Use {SYSDATE} em elementos text para exibir a data de geração:
{
"type": "text",
"id": "header_date",
"x": 612, "y": 40, "width": 190, "height": 18,
"content": "Gerado em: {SYSDATE}",
"fontSize": 8,
"color": "bbdefb",
"align": "right"
}Passo 4 — Detail band vazia
O data source cotacoes é do tipo externo, mas ainda precisa de uma DetailBand vinculada para que o engine reconheça qual data source alimenta a SummaryBand. A detail band pode ter height 0 e sem elementos:
{
"type": "detail",
"id": "detail_cotacoes",
"dataSourceId": "cotacoes",
"height": 0,
"elements": []
}Passo 5 — Gráfico de linha na SummaryBand
{
"type": "chart",
"id": "line_cotacao",
"x": 0, "y": 8, "width": 802, "height": 216,
"chartType": "line",
"dataSourceId": "cotacoes",
"categoryField": "date_label",
"series": [
{ "name": "Compra (bid)", "valueField": "bid", "color": "1565c0" },
{ "name": "Máxima (high)", "valueField": "high", "color": "b71c1c" },
{ "name": "Mínima (low)", "valueField": "low", "color": "2e7d32" }
],
"options": {
"showLegend": true,
"legendPosition": "top",
"showGridLines": true,
"labelFontSize": 8
}
}Passo 6 — Tabela com arredondamento
Abaixo do gráfico, uma tabela exibe os valores numéricos com arredondamento bancário (half_even, precisão 3):
{
"type": "table",
"id": "tbl_cotacoes",
"x": 0, "y": 234, "width": 802,
"dataSourceId": "cotacoes",
"showHeader": true,
"alternateRowColors": true,
"headerFill": "1565c0",
"columns": [
{ "id": "col_date", "header": "Data", "binding": "date_label", "width": 148, "format": "date:dd/MM/yyyy" },
{ "id": "col_bid", "header": "Compra", "binding": "bid", "width": 110, "align": "right", "rounding": { "mode": "half_even", "precision": 3 } },
{ "id": "col_ask", "header": "Venda", "binding": "ask", "width": 110, "align": "right", "rounding": { "mode": "half_even", "precision": 3 } },
{ "id": "col_high", "header": "Máxima", "binding": "high", "width": 100, "align": "right", "rounding": { "mode": "half_even", "precision": 3 } },
{ "id": "col_low", "header": "Mínima", "binding": "low", "width": 100, "align": "right", "rounding": { "mode": "half_even", "precision": 3 } },
{ "id": "col_pct", "header": "Variação %", "binding": "pctChange", "width": 110, "align": "right", "rounding": { "mode": "half_even", "precision": 3 } }
]
}Passo 7 — Paginação no footer
{
"type": "footer",
"id": "footer_main",
"height": 25,
"elements": [
{
"type": "text",
"id": "footer_page",
"x": 680, "y": 8, "width": 120,
"content": "Página {PAGE} de {PAGES}",
"fontSize": 7,
"align": "right"
}
]
}Passo 8 — Dart: injetando o RestDataSourceResolver
import 'dart:io';
import 'package:sulfite_core/sulfite_core.dart';
import 'package:sulfite_datasources/sulfite_datasources.dart';
void main() async {
final engine = SulfiteEngineImpl();
final report = await engine.parseReport(reportJson);
final pdfBytes = await engine.generate(
report,
resolvers: [RestDataSourceResolver()],
format: 'pdf',
);
await File('forex_report.pdf').writeAsBytes(pdfBytes);
}O engine.generate() itera os data sources com source: "external", chama canResolve() em cada resolver da lista e delega ao primeiro que aceitar. O RestDataSourceResolver faz o GET para a URL e decodifica o JSON automaticamente.
Outros formatos
// HTML
final htmlBytes = await engine.generate(report,
resolvers: [RestDataSourceResolver()],
format: 'html',
);
// Excel
final xlsxBytes = await engine.generate(report,
resolvers: [RestDataSourceResolver()],
format: 'excel',
);Parâmetros em runtime
Se a URL contiver placeholders {paramName}, passe os valores via params:
final pdfBytes = await engine.generate(
report,
resolvers: [RestDataSourceResolver()],
params: {'period': '30', 'currency': 'EUR'},
format: 'pdf',
);Uma URL como https://api.example.com/rates/{currency}/last/{period} será resolvida para https://api.example.com/rates/EUR/last/30.
Fluxo completo
report.json (source: external, url: ...)
│
▼
engine.generate(resolvers: [RestDataSourceResolver()])
│
├─ RestDataSourceResolver.resolve(ds) → GET url → List<Map>
│
├─ DataProcessor.process() + script afterQuery → enriquece e ordena registros
│
└─ PdfRenderer.render() → PDF bytes