TEXTBOOK SECTION / AI LEARNING

Image.networkでAPI画像を表示する

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

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

OVERVIEW

この節で学べること

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

TABLE OF CONTENTS

目次

CONTENT

ここから

前の節では、検索ボタンを押してPokéAPIを呼び出し、ポケモン名を表示しました。

今回は、APIから取得した画像URLを使って、画面にポケモン画像を表示します。

Flutterでは、インターネット上の画像を表示するときに Image.network() を使います。


この節で作ること

作業内容
画像URLを取り出すsprites の中から画像URLを取得する
画像用の変数を作る_pokemonImageUrl に保存する
Image.networkを使うAPI画像を画面に表示する
検索結果を更新する名前と画像を同時に表示する

忙しい方はここだけ見て

PokéAPIの画像URLは、次のように取り出します。

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

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

画面には、次のように表示します。

Image.network(
  _pokemonImageUrl,
  width: 160,
  height: 160,
  fit: BoxFit.contain,
)

画像URLの場所

PokéAPIのポケモン画像は、JSONの中で次の場所にあります。

sprites
  └─ front_default

つまり、Mapから取り出すときは2段階になります。

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

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

name は浅い場所にありました。

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

画像URLは、少し奥にあります。


画像用の状態変数を追加する

_PokemonHomePageState の中に、画像URLを保存する変数を追加します。

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

画像URLは、まだ取得していない状態があります。

そのため、String? にしています。


検索処理を変更する

検索ボタンを押したとき、名前だけでなく画像URLも保存します。

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

  if (pokemonId == null) {
    setState(() {
      _pokemonName = '数字を入力してください';
      _pokemonImageUrl = 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;
  });
}

ポイントはここです。

_pokemonImageUrl = imageUrl;

これで、検索結果に応じて画像が変わります。


Image.networkを画面に追加する

画像URLがあるときだけ、Image.network() を表示します。

if (_pokemonImageUrl != null)
  Image.network(
    _pokemonImageUrl!,
    width: 160,
    height: 160,
    fit: BoxFit.contain,
  ),

_pokemonImageUrl!! は、「ここではnullではない」とDartに伝える記号です。

今回は、直前に if (_pokemonImageUrl != null) で確認しているため使えます。


完成コード

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;

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

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

    if (pokemonId == null) {
      setState(() {
        _pokemonName = '数字を入力してください';
        _pokemonImageUrl = 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;
    });
  }

  /**
   * ポケモン検索画面を構築する関数。
   *
   * 入力: BuildContext
   * 出力: Scaffold
   */
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('ポケモン図鑑'),
      ),
      body: Padding(
        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)
              Image.network(
                _pokemonImageUrl!,
                width: 160,
                height: 160,
                fit: BoxFit.contain,
              ),
            const SizedBox(height: 16),
            Text(
              _pokemonName,
              style: const TextStyle(
                fontSize: 28,
                fontWeight: FontWeight.bold,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

動作確認

アプリを起動します。

flutter run

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

pikachu

ピカチュウの画像と名前が表示されれば成功です。

次に 1 を入力します。

bulbasaur

画像もフシギダネに変われば、API画像の表示ができています。


よくあるエラー

エラー原因対応
画像が出ないURLを取得できていないsprites['front_default'] を確認
nullエラーになる画像URLがnullString?if で確認する
名前だけ表示されるImage.network を置いていないbuild内に追加する
画像が大きすぎるサイズ指定なしwidthheight を指定する

まとめ

この節では、Image.network() を使ってAPI画像を表示しました。

大事な流れは、次の3つです。

final Map<String, dynamic> sprites =
    data['sprites'] as Map<String, dynamic>;
final String imageUrl = sprites['front_default'] as String;
Image.network(_pokemonImageUrl!)

これで、ポケモン図鑑アプリに画像が入りました。

次は、名前・画像・番号をカードUIとして見やすく整えていきます。

FAQ

よくある質問

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