TEXTBOOK SECTION / AI LEARNING

API通信中のローディング表示を作る

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

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

OVERVIEW

この節で学べること

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

TABLE OF CONTENTS

目次

CONTENT

ここから

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

今回は、検索ボタンを押してからAPIの結果が返ってくるまでの間に、ローディング表示を出します。

通信中に何も変化がないと、ユーザーは「押せていないのかな?」と感じます。

そこで、CircularProgressIndicator を使って、処理中であることを見せます。


この節で作ること

作業内容
通信中フラグを作る_isLoading を追加する
検索開始時にtrueにするローディング表示を出す
通信完了後にfalseにする結果カードを表示する
ボタンの連打を防ぐ通信中はボタンを押せないようにする

忙しい方はここだけ見て

通信中かどうかを管理する変数を追加します。

bool _isLoading = false;

検索開始時に true、終了時に false にします。

setState(() {
  _isLoading = true;
});

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

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

画面では、通信中だけローディングを表示します。

if (_isLoading)
  const CircularProgressIndicator()

ローディング表示とは

ローディング表示は、処理中であることをユーザーに伝えるための表示です。

Flutterでは、くるくる回る表示として CircularProgressIndicator が用意されています。

const CircularProgressIndicator()

API通信は一瞬で終わることもありますが、通信環境によっては少し時間がかかります。

その待ち時間を無言にしないために、ローディング表示を入れます。


手順1:通信中フラグを追加する

_PokemonHomePageState に、次の変数を追加します。

bool _isLoading = false;

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

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

false は「通信中ではない」という意味です。

検索ボタンを押したら true に変えます。


手順2:検索開始時にローディングを出す

_searchPokemon() の中で、API通信の前に _isLoadingtrue にします。

setState(() {
  _isLoading = true;
  _pokemonImageUrl = null;
  _pokemonIdText = null;
  _pokemonName = '検索中です...';
});

これで、検索が始まった瞬間に画面が切り替わります。


手順3:通信完了後にローディングを止める

API通信が終わったら、_isLoadingfalse に戻します。

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

これで、ローディング表示が消えて、検索結果のカードが表示されます。


手順4:通信中はボタンを押せないようにする

通信中に何度も検索ボタンを押すと、API通信が何回も走ってしまいます。

そこで、_isLoadingtrue の間はボタンを無効にします。

ElevatedButton(
  onPressed: _isLoading ? null : _searchPokemon,
  child: Text(_isLoading ? '検索中...' : '検索する'),
)

onPressednull を入れると、ボタンは押せない状態になります。


完成コード

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;
  bool _isLoading = false;

  /**
   * 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;
        _isLoading = false;
      });
      return;
    }

    setState(() {
      _isLoading = true;
      _pokemonName = '検索中です...';
      _pokemonImageUrl = null;
      _pokemonIdText = null;
    });

    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();
      _isLoading = false;
    });
  }

  /**
   * ポケモン検索画面を構築する関数。
   *
   * 入力: 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,
              enabled: !_isLoading,
              decoration: const InputDecoration(
                labelText: 'ポケモン番号',
                hintText: '例: 25',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 16),
            SizedBox(
              width: double.infinity,
              child: ElevatedButton(
                onPressed: _isLoading ? null : _searchPokemon,
                child: Text(_isLoading ? '検索中...' : '検索する'),
              ),
            ),
            const SizedBox(height: 32),
            if (_isLoading)
              const CircularProgressIndicator()
            else 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 と入力して検索します。

検索中に、くるくる回る表示が出ます。

検索中...

通信が終わると、ピカチュウのカードが表示されます。


よくあるエラー

エラー原因対応
ローディングが消えない_isLoading = false がない通信完了後にfalseへ戻す
ボタンを連打できるonPressed が常に有効_isLoading ? null : _searchPokemon にする
検索中も入力できるTextFieldが有効なままenabled: !_isLoading を追加
結果とローディングが同時に出る表示条件が重なっているif / else if / else で分ける

まとめ

この節では、API通信中のローディング表示を作りました。

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

ポイント内容
_isLoading通信中かどうかを管理する
CircularProgressIndicator通信中の表示を出す
onPressed: null通信中のボタン連打を防ぐ

ローディング表示があるだけで、アプリっぽさが出てきます。

次は、存在しない番号や通信失敗時に、エラー表示を出していきます。

FAQ

よくある質問

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