Skip to content

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:

  • ImageElement com binding para URL aninhada (dot-path)
  • Transform expand para extrair lista aninhada como data source
  • TableElement alimentado por dados expandidos
  • SulfiteConsumerScreen com onFetchData dinâmico

Código completo: examples/09-rfc001-consumer


Pré-requisitos


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":

json
{
  "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:

json
{
  "scripts": [
    { "id": "extrair_golpes", "hook": "afterQuery" }
  ]
}
dart
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:

json
{
  "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:

json
{
  "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):

json
{
  "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

json
{
  "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:

dart
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:

bash
cd packages/sulfite_core
fvm flutter test test/pokedex_generate_test.dart

Os arquivos são salvos em examples/09-rfc001-consumer/output/.


Próximos passos

Sulfite do 🇧🇷 para o mundo © 2026 Rafael S. Pinheiro