CONTENT
ここから
前の節では、PokeAPIから返ってきたJSONを Map<String, dynamic> に変換し、name を取り出しました。
ただ、今の状態では取得できるポケモンが固定されています。
/api/v2/pokemon/25
この 25 はピカチュウの番号です。
今回は、画面に入力欄を作り、ユーザーが入力した番号でポケモンを検索できるようにします。
この節で作ること
この節では、次の流れを作ります。
| 作業 | 内容 |
|---|---|
| TextFieldを追加する | ポケモン番号を入力できるようにする |
| TextEditingControllerを使う | 入力された文字を取得する |
| 入力値をAPIのURLに使う | /pokemon/25 の数字を変更できるようにする |
| 検索ボタンを作る | ボタンを押したらAPI通信する |
| 取得した名前を表示する | 入力した番号のポケモン名を画面に出す |
今回の完成イメージは、次のような画面です。
ポケモン番号を入力
[ 25 ]
[検索する]
pikachu
忙しい方はここだけ見て
TextFieldの入力値を使ってAPI検索する基本形は、次のコードです。
final int pokemonId = int.parse(_pokemonIdController.text);
final Map<String, dynamic> data = await fetchPokemonData(pokemonId);
final String name = data['name'] as String;
TextField に入力された文字は、TextEditingController から取得します。
final TextEditingController _pokemonIdController = TextEditingController();
そして、画面でこのように使います。
TextField(
controller: _pokemonIdController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: 'ポケモン番号',
hintText: '例: 25',
border: OutlineInputBorder(),
),
)
TextFieldとは
TextField は、Flutterで文字入力欄を作るためのWidgetです。
ユーザーに名前、メールアドレス、検索キーワード、数字などを入力してもらうときに使います。
今回の場合は、ポケモン番号を入力するために使います。
TextField(
keyboardType: TextInputType.number,
)
keyboardType: TextInputType.number を指定すると、スマホでは数字入力向けのキーボードが表示されます。
TextEditingControllerとは
TextField に入力された文字を取得するには、TextEditingController を使います。
final TextEditingController _pokemonIdController = TextEditingController();
たとえば、ユーザーが 25 と入力した場合、次のように値を取り出せます。
final String inputText = _pokemonIdController.text;
ただし、TextField から取得できる値は、最初は文字列です。
'25'
APIの番号として扱いたいので、int.parse() を使って数値に変換します。
final int pokemonId = int.parse(_pokemonIdController.text);
手順1:fetchPokemonDataに番号を渡せるようにする
前の節では、APIのURLが 25 に固定されていました。
Future<Map<String, dynamic>> fetchPokemonData() async {
final Uri url = Uri.https(
'pokeapi.co',
'/api/v2/pokemon/25',
);
...
}
今回は、引数でポケモン番号を受け取る形に変更します。
/**
* 指定されたポケモン番号を使ってPokeAPIからデータを取得する関数。
*
* 入力: 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('API通信に失敗しました');
}
final Map<String, dynamic> data =
jsonDecode(response.body) as Map<String, dynamic>;
return data;
}
ポイントはここです。
'/api/v2/pokemon/$pokemonId'
$pokemonId と書くことで、変数の値を文字列の中に埋め込めます。
たとえば、pokemonId が 25 の場合は、次のURLになります。
https://pokeapi.co/api/v2/pokemon/25
pokemonId が 1 の場合は、次のURLになります。
https://pokeapi.co/api/v2/pokemon/1
手順2:TextEditingControllerを用意する
_PokemonHomePageState の中に、入力欄を管理するControllerを追加します。
final TextEditingController _pokemonIdController = TextEditingController();
状態変数と合わせると、このようになります。
class _PokemonHomePageState extends State<PokemonHomePage> {
final TextEditingController _pokemonIdController = TextEditingController();
String _pokemonName = 'まだデータを取得していません';
...
}
手順3:disposeでControllerを片付ける
TextEditingController を使った場合、画面が閉じられるときに片付ける処理を書きます。
/**
* TextEditingControllerを破棄する関数。
*
* 入力: なし
* 出力: 使用していたControllerを解放する
*/
@override
void dispose() {
_pokemonIdController.dispose();
super.dispose();
}
Flutterでは、Controllerのように画面の状態と一緒に持つものは、使い終わったら dispose() で破棄します。
少し地味ですが、きれいなアプリを作るうえでは大事な習慣です。
手順4:検索処理を作る
次に、ボタンを押したときに呼ばれる関数を作ります。
/**
* 入力されたポケモン番号を使ってAPI通信を行い、名前を画面に表示する関数。
*
* 入力: なし
* 出力: _pokemonNameを更新して画面を再描画する
*/
Future<void> _searchPokemon() async {
final int pokemonId = int.parse(_pokemonIdController.text);
final Map<String, dynamic> data = await fetchPokemonData(pokemonId);
final String name = data['name'] as String;
setState(() {
_pokemonName = name;
});
}
流れは、次の通りです。
1. TextFieldの文字を取得する
2. 文字列をintに変換する
3. 番号を使ってAPIを呼び出す
4. JSONをMapに変換する
5. nameを取り出す
6. setStateで画面を更新する
手順5:TextFieldを画面に追加する
build() の中に TextField を追加します。
TextField(
controller: _pokemonIdController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: 'ポケモン番号',
hintText: '例: 25',
border: OutlineInputBorder(),
),
)
controller に _pokemonIdController を指定することで、入力された文字をDartコード側から取得できます。
手順6:検索ボタンを追加する
次に、検索用のボタンを作ります。
ElevatedButton(
onPressed: _searchPokemon,
child: const Text('検索する'),
)
ボタンを押すと、先ほど作った _searchPokemon() が実行されます。
今回の完成コード
lib/main.dart を次のようにします。
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
/**
* アプリの起点になる関数。
*
* 入力: なし
* 出力: MyAppを起動する
*/
void main() {
runApp(const MyApp());
}
/**
* 指定されたポケモン番号を使ってPokeAPIからデータを取得する関数。
*
* 入力: 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('API通信に失敗しました');
}
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 = 'まだデータを取得していません';
/**
* TextEditingControllerを破棄する関数。
*
* 入力: なし
* 出力: 使用していたControllerを解放する
*/
@override
void dispose() {
_pokemonIdController.dispose();
super.dispose();
}
/**
* 入力されたポケモン番号を使ってAPI通信を行い、名前を画面に表示する関数。
*
* 入力: なし
* 出力: _pokemonNameを更新して画面を再描画する
*/
Future<void> _searchPokemon() async {
final int pokemonId = int.parse(_pokemonIdController.text);
final Map<String, dynamic> data = await fetchPokemonData(pokemonId);
final String name = data['name'] as String;
setState(() {
_pokemonName = name;
});
}
/**
* ポケモン図鑑のトップ画面を構築する関数。
*
* 入力: BuildContext
* 出力: Scaffold
*/
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('ポケモン図鑑'),
),
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
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),
Text(
_pokemonName,
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
);
}
}
実行して確認する
ターミナルでアプリを起動します。
flutter run
画面が表示されたら、入力欄に 25 と入力します。
25
検索ボタンを押します。
検索する
画面に次のように表示されれば成功です。
pikachu
次に、1 と入力して検索してみます。
1
次のように表示されれば成功です。
bulbasaur
これで、固定のピカチュウ表示ではなく、入力した番号に応じてポケモンを検索できるようになりました。
番号を変えて試してみる
PokeAPIでは、番号を変えることで別のポケモンを取得できます。
| 番号 | 表示される名前 |
|---|---|
| 1 | bulbasaur |
| 4 | charmander |
| 7 | squirtle |
| 25 | pikachu |
| 39 | jigglypuff |
まずは、よく知っているポケモンで試すと分かりやすいです。
API通信の練習では、「自分で入力した値によって結果が変わる」ことを体験するのが大事です。
ここができると、検索アプリらしさが一気に出てきます。
入力値の注意点
今のコードは、数字が正しく入力される前提です。
そのため、空欄のまま検索したり、文字を入力したりするとエラーになります。
たとえば、次のような入力です。
空欄
abc
ピカチュウ
今はまだ、入力エラーへの対応は作り込みません。
この章の後半で、API取得失敗時のエラー表示を作る節があります。
そこで、空欄や存在しない番号への対応も扱うと分かりやすいです。
ただし、最低限の防御だけ先に入れるなら、次のように書けます。
final int? pokemonId = int.tryParse(_pokemonIdController.text);
if (pokemonId == null) {
setState(() {
_pokemonName = '数字を入力してください';
});
return;
}
int.tryParse() は、変換に失敗した場合に null を返します。
int.parse() よりも安全です。
少し安全な検索処理にする
教材としては、こちらの書き方の方が実用的です。
/**
* 入力されたポケモン番号を検証し、API通信を行う関数。
*
* 入力: なし
* 出力: 検索結果または入力エラーメッセージを画面に表示する
*/
Future<void> _searchPokemon() async {
final int? pokemonId = int.tryParse(_pokemonIdController.text);
if (pokemonId == null) {
setState(() {
_pokemonName = '数字を入力してください';
});
return;
}
final Map<String, dynamic> data = await fetchPokemonData(pokemonId);
final String name = data['name'] as String;
setState(() {
_pokemonName = name;
});
}
最初に出した完成コードはシンプル版です。
授業や教材で使うなら、こちらの安全版に差し替えると、初心者にも親切です。
TextFieldの見た目を少し整える
入力欄は、InputDecoration で見た目を整えられます。
decoration: const InputDecoration(
labelText: 'ポケモン番号',
hintText: '例: 25',
helperText: '1以上の番号を入力してください',
border: OutlineInputBorder(),
)
helperText を追加すると、入力欄の下に補足説明が表示されます。
1以上の番号を入力してください
初心者向けアプリでは、何を入力すればよいかを画面に書いておくと親切です。
キーボードの検索ボタンでも検索する
スマホで使う場合、キーボードの完了ボタンや検索ボタンで検索できると便利です。
TextField に次の2つを追加します。
textInputAction: TextInputAction.search,
onSubmitted: (_) => _searchPokemon(),
全体では、このようになります。
TextField(
controller: _pokemonIdController,
keyboardType: TextInputType.number,
textInputAction: TextInputAction.search,
onSubmitted: (_) => _searchPokemon(),
decoration: const InputDecoration(
labelText: 'ポケモン番号',
hintText: '例: 25',
border: OutlineInputBorder(),
),
)
これで、入力後にキーボード側の検索ボタンを押してもAPI通信できます。
よくあるエラー
1. TextEditingControllerが使えない
次のように書いているか確認します。
final TextEditingController _pokemonIdController = TextEditingController();
TextEditingController はFlutterのMaterialライブラリに含まれるため、次のimportが必要です。
import 'package:flutter/material.dart';
2. 空欄で検索するとエラーになる
int.parse() は、空文字を数値に変換できません。
final int pokemonId = int.parse(_pokemonIdController.text);
安全にするなら、int.tryParse() を使います。
final int? pokemonId = int.tryParse(_pokemonIdController.text);
if (pokemonId == null) {
setState(() {
_pokemonName = '数字を入力してください';
});
return;
}
3. 入力したのに結果が変わらない
次の点を確認してください。
| 確認すること | 正しい状態 |
|---|---|
TextField にcontrollerがあるか | controller: _pokemonIdController |
| ボタンが関数につながっているか | onPressed: _searchPokemon |
| API関数に番号を渡しているか | fetchPokemonData(pokemonId) |
| URLに番号を埋め込んでいるか | '/api/v2/pokemon/$pokemonId' |
特に、URLがまだ /25 のままだと、どの番号を入力してもピカチュウしか出ません。
今回はまだやらないこと
この節では、TextFieldで番号検索できるようにしました。
ただし、まだ次のことは行いません。
| まだやらないこと | 理由 |
|---|---|
| 検索ボタンのデザイン調整 | 次の節以降でUIを整える |
| 画像表示 | Image.network() の節で扱う |
| ローディング表示 | API通信中の表示で扱う |
| 通信失敗時の本格対応 | エラー表示の節で扱う |
| ポケモン情報のカード化 | カードUIの節で扱う |
今は、入力した番号でAPIのURLが変わる。
そこだけを確実に押さえます。
確認問題
問1
Flutterで文字入力欄を作るWidgetは何ですか?
答え。
TextField
問2
TextFieldに入力された文字を取得するために使うものは何ですか?
答え。
TextEditingController
問3
TextEditingControllerから入力文字を取得するコードはどれですか?
答え。
_pokemonIdController.text
問4
文字列の数字を int に変換するコードはどれですか?
答え。
int.parse(_pokemonIdController.text)
問5
変換に失敗してもアプリを止めにくい書き方はどれですか?
答え。
int.tryParse(_pokemonIdController.text)
まとめ
この節では、TextField を使ってポケモン番号検索を作りました。
今回できるようになったことは、次の通りです。
| できるようになったこと | 内容 |
|---|---|
| 入力欄を作る | TextField を使う |
| 入力値を取得する | TextEditingController を使う |
| 文字列を数字に変換する | int.parse() または int.tryParse() を使う |
| APIのURLを変える | '/api/v2/pokemon/$pokemonId' を使う |
| 検索結果を表示する | setState() で画面を更新する |
ここまでで、ポケモン図鑑アプリは「固定表示」から「検索できるアプリ」に進化しました。
次の節では、検索ボタンを押したタイミングでPokeAPIを呼び出す処理を、もう少し整理していきます。