TEXTBOOK SECTION / AI LEARNING

TextFieldでポケモン番号検索を作る

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

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

OVERVIEW

この節で学べること

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

TABLE OF CONTENTS

目次

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 と書くことで、変数の値を文字列の中に埋め込めます。

たとえば、pokemonId25 の場合は、次のURLになります。

https://pokeapi.co/api/v2/pokemon/25

pokemonId1 の場合は、次の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では、番号を変えることで別のポケモンを取得できます。

番号表示される名前
1bulbasaur
4charmander
7squirtle
25pikachu
39jigglypuff

まずは、よく知っているポケモンで試すと分かりやすいです。

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を呼び出す処理を、もう少し整理していきます。

FAQ

よくある質問

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