TEXTBOOK SECTION / AI LEARNING

ポケモン情報をカードUIで表示する

Flutterアプリケーション開発概論の「Flutter API連携入門|ポケモン図鑑アプリの作り方」より、ポケモン情報をカードUIで表示するを解説。生成AI、AI活用、DX、業務改善を実践しながら学べるオンライン教材です。

13Flutter API連携入門|ポケモン図鑑アプリの作り方Flutter / iOS / Android / MacOS / Windows / 基礎から学ぶ / 開発 / アプリ開発

OVERVIEW

この節で学べること

概要を表示する
項目内容
教材名Flutterアプリケーション開発概論
Flutter API連携入門|ポケモン図鑑アプリの作り方
ポケモン情報をカードUIで表示する
カテゴリFlutter / iOS / Android / MacOS / Windows / 基礎から学ぶ / 開発 / アプリ開発
学習内容生成AI、AI活用、DX、業務改善を実践しながら理解するための教材です。

TABLE OF CONTENTS

目次

CONTENT

ここから

前の節では、Image.network() を使ってポケモン画像を表示しました。

今回は、名前と画像をそのまま並べるのではなく、カードUIとして見やすく整えます。

Flutterでは、まとまりのある情報を表示するときに Card をよく使います。

ポケモン図鑑のように「1体分の情報」を見せる画面と相性が良いです。


この節で作ること

作業内容
Card を使う情報をひとまとまりにする
画像を中央に表示するImage.network() をカード内に置く
名前を大きく表示するText のstyleを調整する
番号も表示する入力したポケモン番号をカードに出す

忙しい方はここだけ見て

カードUIの基本形は、次のコードです。

Card(
  elevation: 4,
  shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.circular(20),
  ),
  child: Padding(
    padding: const EdgeInsets.all(24),
    child: Column(
      children: <Widget>[
        Image.network(
          _pokemonImageUrl!,
          width: 160,
          height: 160,
          fit: BoxFit.contain,
        ),
        const SizedBox(height: 16),
        Text(
          _pokemonName,
          style: const TextStyle(
            fontSize: 28,
            fontWeight: FontWeight.bold,
          ),
        ),
      ],
    ),
  ),
)

Cardとは

Card は、情報をひとつの箱として見せるWidgetです。

ただ文字と画像を並べるより、カードに入れることで「検索結果」として見やすくなります。

Card(
  child: Text('pikachu'),
)

さらに、Padding を入れると余白ができます。

Card(
  child: Padding(
    padding: const EdgeInsets.all(24),
    child: Text('pikachu'),
  ),
)

余白があるだけで、画面の印象はかなり変わります。

アプリらしい、少し落ち着いた見た目になります。


ポケモン番号用の変数を追加する

カード内に番号も表示するため、状態変数を追加します。

String? _pokemonIdText;

状態変数は、次のようになります。

String _pokemonName = 'ポケモン番号を入力してください';
String? _pokemonImageUrl;
String? _pokemonIdText;

検索処理を少し変更する

検索に成功したら、名前・画像URL・番号を保存します。

/**
 * 入力された番号を使ってPokéAPIを呼び出し、カード表示用の情報を更新する関数。
 *
 * 入力: なし
 * 出力: 名前、画像URL、番号を画面表示用に更新する
 */
Future<void> _searchPokemon() async {
  final int? pokemonId = int.tryParse(_pokemonIdController.text);

  if (pokemonId == null) {
    setState(() {
      _pokemonName = '数字を入力してください';
      _pokemonImageUrl = null;
      _pokemonIdText = null;
    });
    return;
  }

  final Map<String, dynamic> data = await fetchPokemonData(pokemonId);

  final String name = data['name'] as String;

  final Map<String, dynamic> sprites =
      data['sprites'] as Map<String, dynamic>;

  final String imageUrl = sprites['front_default'] as String;

  setState(() {
    _pokemonName = name;
    _pokemonImageUrl = imageUrl;
    _pokemonIdText = pokemonId.toString();
  });
}

pokemonId.toString() で、数値を画面表示用の文字列に変えています。


カードUIを作る

画像URLがあるときだけ、カードを表示します。

if (_pokemonImageUrl != null)
  Card(
    elevation: 4,
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(20),
    ),
    child: Padding(
      padding: const EdgeInsets.all(24),
      child: Column(
        children: <Widget>[
          Text(
            'No. $_pokemonIdText',
            style: const TextStyle(
              fontSize: 16,
              color: Colors.grey,
            ),
          ),
          const SizedBox(height: 8),
          Image.network(
            _pokemonImageUrl!,
            width: 160,
            height: 160,
            fit: BoxFit.contain,
          ),
          const SizedBox(height: 16),
          Text(
            _pokemonName,
            style: const TextStyle(
              fontSize: 28,
              fontWeight: FontWeight.bold,
            ),
          ),
        ],
      ),
    ),
  )
else
  Text(
    _pokemonName,
    style: const TextStyle(
      fontSize: 20,
      fontWeight: FontWeight.bold,
    ),
  ),

検索前はメッセージだけ表示します。

検索後はカードを表示します。


完成コード

lib/main.dart を次のようにします。

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

/**
 * アプリの起点になる関数。
 *
 * 入力: なし
 * 出力: MyAppを起動する
 */
void main() {
  runApp(const MyApp());
}

/**
 * 指定されたポケモン番号でPokéAPIからデータを取得する関数。
 *
 * 入力: pokemonId ポケモン番号
 * 出力: ポケモン情報を持つMap<String, dynamic>
 */
Future<Map<String, dynamic>> fetchPokemonData(int pokemonId) async {
  final Uri url = Uri.https(
    'pokeapi.co',
    '/api/v2/pokemon/$pokemonId',
  );

  final http.Response response = await http.get(url);

  if (response.statusCode != 200) {
    throw Exception('ポケモンデータを取得できませんでした');
  }

  final Map<String, dynamic> data =
      jsonDecode(response.body) as Map<String, dynamic>;

  return data;
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  /**
   * アプリ全体のUIを構築する関数。
   *
   * 入力: BuildContext
   * 出力: MaterialApp
   */
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'ポケモン図鑑',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.red),
        useMaterial3: true,
      ),
      home: const PokemonHomePage(),
    );
  }
}

class PokemonHomePage extends StatefulWidget {
  const PokemonHomePage({super.key});

  @override
  State<PokemonHomePage> createState() => _PokemonHomePageState();
}

class _PokemonHomePageState extends State<PokemonHomePage> {
  final TextEditingController _pokemonIdController = TextEditingController();

  String _pokemonName = 'ポケモン番号を入力してください';
  String? _pokemonImageUrl;
  String? _pokemonIdText;

  /**
   * TextEditingControllerを破棄する関数。
   *
   * 入力: なし
   * 出力: Controllerのリソースを解放する
   */
  @override
  void dispose() {
    _pokemonIdController.dispose();
    super.dispose();
  }

  /**
   * 入力された番号を使ってPokéAPIを呼び出し、カード表示用の情報を更新する関数。
   *
   * 入力: なし
   * 出力: 名前、画像URL、番号を画面表示用に更新する
   */
  Future<void> _searchPokemon() async {
    final int? pokemonId = int.tryParse(_pokemonIdController.text);

    if (pokemonId == null) {
      setState(() {
        _pokemonName = '数字を入力してください';
        _pokemonImageUrl = null;
        _pokemonIdText = null;
      });
      return;
    }

    final Map<String, dynamic> data = await fetchPokemonData(pokemonId);
    final String name = data['name'] as String;

    final Map<String, dynamic> sprites =
        data['sprites'] as Map<String, dynamic>;
    final String imageUrl = sprites['front_default'] as String;

    setState(() {
      _pokemonName = name;
      _pokemonImageUrl = imageUrl;
      _pokemonIdText = pokemonId.toString();
    });
  }

  /**
   * ポケモン検索画面を構築する関数。
   *
   * 入力: BuildContext
   * 出力: Scaffold
   */
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('ポケモン図鑑'),
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: <Widget>[
            TextField(
              controller: _pokemonIdController,
              keyboardType: TextInputType.number,
              decoration: const InputDecoration(
                labelText: 'ポケモン番号',
                hintText: '例: 25',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 16),
            SizedBox(
              width: double.infinity,
              child: ElevatedButton(
                onPressed: _searchPokemon,
                child: const Text('検索する'),
              ),
            ),
            const SizedBox(height: 32),
            if (_pokemonImageUrl != null)
              Card(
                elevation: 4,
                shape: RoundedRectangleBorder(
                  borderRadius: BorderRadius.circular(20),
                ),
                child: Padding(
                  padding: const EdgeInsets.all(24),
                  child: Column(
                    children: <Widget>[
                      Text(
                        'No. $_pokemonIdText',
                        style: const TextStyle(
                          fontSize: 16,
                          color: Colors.grey,
                        ),
                      ),
                      const SizedBox(height: 8),
                      Image.network(
                        _pokemonImageUrl!,
                        width: 160,
                        height: 160,
                        fit: BoxFit.contain,
                      ),
                      const SizedBox(height: 16),
                      Text(
                        _pokemonName,
                        style: const TextStyle(
                          fontSize: 28,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                    ],
                  ),
                ),
              )
            else
              Text(
                _pokemonName,
                style: const TextStyle(
                  fontSize: 20,
                  fontWeight: FontWeight.bold,
                ),
              ),
          ],
        ),
      ),
    );
  }
}

動作確認

アプリを起動します。

flutter run

25 と入力して検索します。

No. 25
pikachu

ピカチュウの画像・番号・名前がカード内に表示されれば成功です。


よくあるエラー

エラー原因対応
カードが出ない_pokemonImageUrl がnull検索処理で画像URLを保存する
nullエラーになる! を早く使っているif (_pokemonImageUrl != null) の中で使う
画像だけ出ないURL取得ミスsprites['front_default'] を確認
余白がないPadding なしCard の中に Padding を入れる

まとめ

この節では、ポケモン情報をカードUIで表示しました。

大事なポイントは3つです。

ポイント内容
Card情報をまとまりとして見せる
Paddingカード内に余白を作る
Column番号・画像・名前を縦に並べる

これで、検索結果がかなりアプリらしくなりました。

次は、API通信中にローディング表示を出して、待ち時間も分かりやすくしていきます。

FAQ

よくある質問

ポケモン情報をカードUIで表示するは医療関係者向けだけの内容ですか。
医療分野の例が含まれる場合もありますが、医療関係者だけに限定した内容ではありません。生成AI、AI活用、DX、業務改善、プロトタイプ開発など、一般的なAI学習の事例として読める内容です。
AI初心者でも読めますか。
はい。AIをこれから学ぶ方、数学が苦手な方、仕事でAIを使いたい方にも読み進めやすいように、教材の章と節の流れに沿って整理しています。
サムネイル画像は必ず表示されますか。
はい。教材にcoverUrlが設定されている場合はその画像を表示し、未設定の場合は代替サムネイル画像を表示します。
Flutterアプリケーション開発概論のほかの章も読めますか。
はい。教材トップから章立てを確認でき、前後の節へもページ下部のナビゲーションから移動できます。