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通信の前に _isLoading を true にします。
setState(() {
_isLoading = true;
_pokemonImageUrl = null;
_pokemonIdText = null;
_pokemonName = '検索中です...';
});
これで、検索が始まった瞬間に画面が切り替わります。
手順3:通信完了後にローディングを止める
API通信が終わったら、_isLoading を false に戻します。
setState(() {
_pokemonName = name;
_pokemonImageUrl = imageUrl;
_pokemonIdText = pokemonId.toString();
_isLoading = false;
});
これで、ローディング表示が消えて、検索結果のカードが表示されます。
手順4:通信中はボタンを押せないようにする
通信中に何度も検索ボタンを押すと、API通信が何回も走ってしまいます。
そこで、_isLoading が true の間はボタンを無効にします。
ElevatedButton(
onPressed: _isLoading ? null : _searchPokemon,
child: Text(_isLoading ? '検索中...' : '検索する'),
)
onPressed に 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;
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 | 通信中のボタン連打を防ぐ |
ローディング表示があるだけで、アプリっぽさが出てきます。
次は、存在しない番号や通信失敗時に、エラー表示を出していきます。