CONTENT
ここから
前の節では、API通信中にローディング表示を出しました。
今回は、存在しない番号を入力したときや、通信に失敗したときに、アプリが止まらないようにエラー表示を作ります。
この節で作ること
| 作業 | 内容 |
|---|---|
try-catch を使う | API通信の失敗を受け止める |
| エラーメッセージを表示する | 画面に分かりやすく出す |
| ローディングを止める | 失敗しても _isLoading を false に戻す |
| 結果カードを消す | 失敗時は古い結果を残さない |
忙しい方はここだけ見て
API通信に失敗する可能性がある処理は、try-catch で囲みます。
try {
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();
_errorMessage = null;
_isLoading = false;
});
} catch (error) {
setState(() {
_pokemonName = '';
_pokemonImageUrl = null;
_pokemonIdText = null;
_errorMessage = 'ポケモンを取得できませんでした';
_isLoading = false;
});
}
try-catchとは
try-catch は、エラーが起きるかもしれない処理を安全に実行するための書き方です。
try {
// 失敗するかもしれない処理
} catch (error) {
// 失敗したときの処理
}
API通信では、次のような理由で失敗することがあります。
| 原因 | 例 |
|---|---|
| 存在しない番号 | 999999 を入力した |
| 通信環境が悪い | インターネットにつながっていない |
| API側の問題 | サーバーが一時的に不安定 |
失敗を想定しておくと、アプリが急に止まりにくくなります。
エラー用の変数を追加する
_PokemonHomePageState に、エラーメッセージ用の変数を追加します。
String? _errorMessage;
状態変数は、次のようになります。
String _pokemonName = 'ポケモン番号を入力してください';
String? _pokemonImageUrl;
String? _pokemonIdText;
String? _errorMessage;
bool _isLoading = false;
String? にしているのは、エラーがない状態もあるためです。
検索処理を修正する
_searchPokemon() を次のように変更します。
/**
* 入力された番号を使ってPokéAPIを呼び出し、成功時と失敗時の表示を切り替える関数。
*
* 入力: なし
* 出力: 検索結果、エラーメッセージ、ローディング状態を更新する
*/
Future<void> _searchPokemon() async {
final int? pokemonId = int.tryParse(_pokemonIdController.text);
if (pokemonId == null) {
setState(() {
_pokemonName = '';
_pokemonImageUrl = null;
_pokemonIdText = null;
_errorMessage = '数字を入力してください';
_isLoading = false;
});
return;
}
setState(() {
_isLoading = true;
_pokemonName = '';
_pokemonImageUrl = null;
_pokemonIdText = null;
_errorMessage = null;
});
try {
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();
_errorMessage = null;
_isLoading = false;
});
} catch (error) {
setState(() {
_pokemonName = '';
_pokemonImageUrl = null;
_pokemonIdText = null;
_errorMessage = 'ポケモンを取得できませんでした';
_isLoading = false;
});
}
}
ポイントは、失敗時にも必ず _isLoading = false に戻すことです。
これを忘れると、ローディング表示がずっと消えません。
エラー表示を画面に追加する
カード表示の前に、エラーメッセージを表示する条件を追加します。
if (_isLoading)
const CircularProgressIndicator()
else if (_errorMessage != null)
Text(
_errorMessage!,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.red,
),
)
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,
),
),
表示の優先順位は、次の順番です。
1. 通信中ならローディング
2. エラーがあればエラー表示
3. 画像URLがあればカード表示
4. それ以外は通常メッセージ
完成コード
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;
String? _errorMessage;
bool _isLoading = false;
/**
* TextEditingControllerを破棄する関数。
*
* 入力: なし
* 出力: Controllerのリソースを解放する
*/
@override
void dispose() {
_pokemonIdController.dispose();
super.dispose();
}
/**
* 入力された番号を使ってPokéAPIを呼び出し、成功時と失敗時の表示を切り替える関数。
*
* 入力: なし
* 出力: 検索結果、エラーメッセージ、ローディング状態を更新する
*/
Future<void> _searchPokemon() async {
final int? pokemonId = int.tryParse(_pokemonIdController.text);
if (pokemonId == null) {
setState(() {
_pokemonName = '';
_pokemonImageUrl = null;
_pokemonIdText = null;
_errorMessage = '数字を入力してください';
_isLoading = false;
});
return;
}
setState(() {
_isLoading = true;
_pokemonName = '';
_pokemonImageUrl = null;
_pokemonIdText = null;
_errorMessage = null;
});
try {
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();
_errorMessage = null;
_isLoading = false;
});
} catch (error) {
setState(() {
_pokemonName = '';
_pokemonImageUrl = null;
_pokemonIdText = null;
_errorMessage = 'ポケモンを取得できませんでした';
_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 (_errorMessage != null)
Text(
_errorMessage!,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.red,
),
)
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,
),
),
],
),
),
);
}
}
動作確認
まず、正しい番号を入力します。
25
ピカチュウのカードが表示されれば成功です。
次に、存在しない番号を入力します。
999999
次のように表示されれば成功です。
ポケモンを取得できませんでした
空欄で検索した場合は、次のように表示されます。
数字を入力してください
よくあるエラー
| エラー | 原因 | 対応 |
|---|---|---|
| アプリが止まる | try-catch がない | API通信部分を囲む |
| ローディングが消えない | 失敗時に _isLoading = false がない | catch 内でも戻す |
| 古いカードが残る | 失敗時に画像URLを消していない | _pokemonImageUrl = null にする |
| エラーが出ない | _errorMessage を表示していない | else if で表示する |
まとめ
この節では、API取得失敗時のエラー表示を作りました。
大事なポイントは3つです。
| ポイント | 内容 |
|---|---|
try-catch | 失敗する可能性のある処理を安全に扱う |
_errorMessage | エラー内容を画面に表示する |
_isLoading = false | 失敗時もローディングを止める |
これで、存在しない番号や通信失敗にも対応できるようになりました。
次は、アプリ全体の見た目を整えて、完成度を上げていきます。