Ficha Pokédex — Imagens e Dados Externos
Neste tutorial você vai criar um relatório dinâmico que exibe a ficha de um Pokémon com imagem oficial, atributos e lista de golpes — consumindo dados da PokéAPI em tempo real.
O que você vai aprender:
ImageElementcombindingpara URL aninhada (dot-path)- Transform
expandpara extrair lista aninhada como data source TableElementalimentado por dados expandidosSulfiteConsumerScreencomonFetchDatadinâmico
Código completo:
examples/09-rfc001-consumer
Pré-requisitos
- sulfite_core
^0.1.0 - Familiaridade com Definição do relatório e Data Sources
Estrutura do relatório
O relatório final tem três seções principais:
┌───────────────────────────────┐
│ [Imagem oficial] Nome │ ← HeaderBand
│ Tipos │
│ Altura/Peso│
├───────────────────────────────┤
│ Base Stats: HP / Atk / ... │ ← DetailBand (pokemon)
├───────────────────────────────┤
│ Golpes │ ← TableElement (pokemon_moves)
│ ─────────────────────────── │
│ mega-punch │
│ fire-punch │
│ ... │
└───────────────────────────────┘1. Definindo o data source
A PokéAPI retorna um objeto único para cada Pokémon. Declare um data source com "type": "object":
{
"dataSources": [
{
"id": "pokemon",
"type": "object",
"description": "Dados completos do Pokémon via PokéAPI"
}
]
}O campo type: "object" instrui o DataProcessor a tratar o payload como um único registro (sem iterar sobre lista).
2. Extraindo os golpes com script afterQuery
A PokéAPI retorna os golpes em pokemon.moves — uma lista aninhada dentro do objeto principal. Use um script para extrair essa lista e expô-la como pokemon_moves no payload:
{
"scripts": [
{ "id": "extrair_golpes", "hook": "afterQuery" }
]
}engine.register('extrair_golpes', (ctx) {
final pokemon = ctx.datasource('pokemon');
if (pokemon.isEmpty) return;
final moves = (pokemon.first['moves'] as List? ?? [])
.cast<Map<String, dynamic>>()
.take(5)
.toList();
ctx.setDatasource('pokemon_moves', moves);
});Após o script, o payload conterá pokemon_moves: [{ "move": { "name": "mega-punch" } }, ...].
Declaração do data source auxiliar
Adicione pokemon_moves em dataSources com type: "list" e schema vazio para que o Studio reconheça o binding. O DataProcessor não valida data sources criados em runtime quando o schema está vazio.
3. Parâmetros do relatório
Defina o parâmetro pokemonName para que o consumidor possa buscar qualquer Pokémon:
{
"params": [
{
"id": "pokemonName",
"label": "Nome do Pokémon",
"type": "string",
"defaultValue": "charizard"
}
]
}4. ImageElement com binding para URL
Use binding com dot-path para apontar para a artwork oficial retornada pela API:
{
"type": "image",
"id": "img_pokemon",
"binding": "sprites.other.official-artwork.front_default",
"dataSourceId": "pokemon",
"width": 120,
"height": 120,
"fit": "contain"
}O campo binding aceita qualquer dot-path, incluindo chaves com hífens (official-artwork). A URL é resolvida em tempo de renderização pelo DataProcessor e aplicada tanto no PDF quanto no HTML.
Pré-carregamento de imagens na DetailBand
Se o ImageElement estiver dentro de uma DetailBand, pre-carregue as imagens usando ImageUrlLoader para garantir que sejam embutidas corretamente no PDF. Veja ImageElement — binding para detalhes.
5. TableElement com dados expandidos
Vincule a tabela a pokemon_moves e defina a coluna para o nome do golpe (ainda via dot-path):
{
"type": "table",
"id": "tbl_moves",
"dataSourceId": "pokemon_moves",
"headerBackgroundColor": "#CC0000",
"headerTextColor": "#FFFFFF",
"columns": [
{
"id": "col_move",
"header": "Golpe",
"binding": "move.name",
"width": 200
},
{
"id": "col_url",
"header": "URL",
"binding": "move.url",
"width": 280
}
]
}6. JSON completo
{
"id": "pokedex_profile",
"name": "Ficha Pokédex",
"pageSize": "A4",
"params": [
{ "id": "pokemonName", "label": "Nome do Pokémon", "type": "string", "defaultValue": "charizard" }
],
"dataSources": [
{ "id": "pokemon", "type": "object" }
],
"scripts": [
{ "id": "extrair_golpes", "hook": "afterQuery" }
],
"bands": [
{
"type": "header",
"height": 160,
"elements": [
{
"type": "image",
"id": "img_pokemon",
"binding": "sprites.other.official-artwork.front_default",
"dataSourceId": "pokemon",
"x": 20, "y": 20, "width": 120, "height": 120,
"fit": "contain"
},
{
"type": "text",
"value": "{pokemon.name}",
"x": 160, "y": 30, "width": 300, "height": 40,
"fontSize": 24, "bold": true
},
{
"type": "text",
"value": "Altura: {pokemon.height} | Peso: {pokemon.weight}",
"x": 160, "y": 80, "width": 300, "height": 30,
"fontSize": 12
}
]
},
{
"type": "detail",
"dataSourceId": "pokemon",
"height": 200,
"elements": [
{
"type": "table",
"id": "tbl_moves",
"dataSourceId": "pokemon_moves",
"x": 20, "y": 10, "width": 480,
"headerBackgroundColor": "#CC0000",
"headerTextColor": "#FFFFFF",
"columns": [
{ "id": "col_move", "header": "Golpe", "binding": "move.name", "width": 200 },
{ "id": "col_url", "header": "URL", "binding": "move.url", "width": 280 }
]
}
]
}
]
}7. Consumindo no Flutter
Use SulfiteConsumerScreen para fornecer os dados da API dinamicamente via onFetchData:
class PokedexPage extends StatelessWidget {
final String pokemonName;
const PokedexPage({required this.pokemonName, super.key});
@override
Widget build(BuildContext context) {
return SulfiteConsumerScreen(
reportJson: _loadReportJson(),
params: {'pokemonName': pokemonName},
onFetchData: (params) async {
final name = params['pokemonName'] ?? 'charizard';
final res = await http.get(
Uri.parse('https://pokeapi.co/api/v2/pokemon/$name'),
);
final data = jsonDecode(res.body) as Map<String, dynamic>;
return {'pokemon': data};
},
);
}
}onFetchData
O callback onFetchData recebe os parâmetros resolvidos pelo viewer e deve retornar um Map<String, dynamic> com os data sources. A chave ("pokemon") deve corresponder ao id declarado em dataSources.
8. Executando o exemplo
No app de exemplo (example/), selecione Pokédex na lista de relatórios e informe o nome do Pokémon. O viewer busca os dados, aplica o transform expand e renderiza a ficha completa em PDF.
Para rodar o teste automatizado que gera os artefatos físicos:
cd packages/sulfite_core
fvm flutter test test/pokedex_generate_test.dartOs arquivos são salvos em examples/09-rfc001-consumer/output/.
Próximos passos
- Relatório com parâmetros — formulários de entrada antes de gerar
- Relatório agrupado — script
afterQuerypara ordenação + agrupamento por band - ImageElement — referência completa de binding, cache e formatos