CONTENT
ここから
前の節では、FlutterアプリからPokeAPIへアクセスし、response.body を画面に表示しました。
ただ、表示された内容はかなり長いJSON文字列でした。
そのままでは、ポケモンの名前だけ、画像URLだけ、高さだけ、という形で取り出すことができません。
そこで今回は、APIから返ってきたJSON文字列を、Flutterで扱いやすい Map に変換します。
Dartでは、JSON文字列を変換するときに dart:convert の jsonDecode() を使います。公式ドキュメントでも、jsonDecode() は文字列を解析し、JSONオブジェクトとして返す関数として説明されています。
この節で作ること
この節では、次の流れを作ります。
| 作業 | 内容 |
|---|---|
dart:convert を読み込む | JSON変換用の標準ライブラリを使う |
jsonDecode() を使う | JSON文字列をDartのデータに変換する |
Map<String, dynamic> にする | キーで値を取り出せる形にする |
| ポケモン名を取り出す | name の値を画面に表示する |
今回は、まずピカチュウの名前 pikachu を取り出せる状態にします。
忙しい方はここだけ見て
JSON文字列をMapに変換する基本形は、次のコードです。
import 'dart:convert';
final Map<String, dynamic> data =
jsonDecode(response.body) as Map<String, dynamic>;
final String name = data['name'] as String;
jsonDecode(response.body) によって、APIから返ってきたJSON文字列をDartで扱えるデータに変換できます。
JSONとMapの違い
JSONは、APIから返ってくる文字列形式のデータです。
たとえば、次のような形です。
{
"id": 25,
"name": "pikachu",
"height": 4,
"weight": 60
}
このままだと、Flutter側ではただの文字列に近い状態です。
そこで、Dartの Map に変換します。
{
'id': 25,
'name': 'pikachu',
'height': 4,
'weight': 60,
}
Mapにすると、キーを指定して値を取り出せます。
final String name = data['name'] as String;
つまり、こういうことです。
JSON文字列
↓ jsonDecode()
Map<String, dynamic>
↓ data['name']
pikachu
Dartの dart:convert ライブラリはJSONやUTF-8などの変換機能を提供しており、使用するには import 'dart:convert'; を書きます。
手順1:dart:convertを読み込む
lib/main.dart の一番上に、次のimportを追加します。
import 'dart:convert';
前の節のコードと合わせると、このようになります。
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
dart:convert は、Dartに最初から用意されている標準ライブラリです。
追加パッケージではないので、flutter pub add は必要ありません。
手順2:fetchPokemonDataの戻り値をMapに変える
前の節では、APIから返ってきたJSONをそのまま文字列として返していました。
Future<String> fetchPokemonData() async {
final Uri url = Uri.https(
'pokeapi.co',
'/api/v2/pokemon/25',
);
final http.Response response = await http.get(url);
if (response.statusCode != 200) {
throw Exception('API通信に失敗しました');
}
return response.body;
}
今回は、戻り値を String ではなく Map<String, dynamic> に変えます。
/**
* PokeAPIからピカチュウのデータを取得し、Mapに変換する関数。
*
* 入力: なし
* 出力: ポケモン情報を持つMap<String, dynamic>
*/
Future<Map<String, dynamic>> fetchPokemonData() async {
final Uri url = Uri.https(
'pokeapi.co',
'/api/v2/pokemon/25',
);
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;
}
ここが今回の中心です。
final Map<String, dynamic> data =
jsonDecode(response.body) as Map<String, dynamic>;
jsonDecode() の戻り値は dynamic です。
そのため、今回は as Map<String, dynamic> と書いて、Mapとして扱うことを明示しています。
手順3:ポケモン名を取り出す
Mapに変換できると、キーを指定して値を取り出せます。
PokeAPIのポケモンデータには、name というキーがあります。
final String name = data['name'] as String;
これで、pikachu という文字列を取り出せます。
たとえば、画面に表示する文章を作るなら、次のように書けます。
final String name = data['name'] as String;
setState(() {
_pokemonName = name;
});
長いJSONをそのまま表示していた状態から、必要な情報だけを取り出す段階に進みます。
手順4:画面表示をJSON全文から名前表示に変える
前の節では、状態変数をこのようにしていました。
String _pokemonData = 'まだデータを取得していません';
今回は、名前を表示するので、変数名を少し分かりやすくします。
String _pokemonName = 'まだデータを取得していません';
そして、ボタンを押したときの処理を次のように変えます。
/**
* PokeAPIからデータを取得し、ポケモン名を画面に表示する関数。
*
* 入力: なし
* 出力: _pokemonNameを更新して画面を再描画する
*/
Future<void> _loadPokemonData() async {
final Map<String, dynamic> data = await fetchPokemonData();
final String name = data['name'] as String;
setState(() {
_pokemonName = name;
});
}
これで、画面には長いJSONではなく、pikachu だけが表示されます。
今回の完成コード
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からピカチュウのデータを取得し、Mapに変換する関数。
*
* 入力: なし
* 出力: ポケモン情報を持つMap<String, dynamic>
*/
Future<Map<String, dynamic>> fetchPokemonData() async {
final Uri url = Uri.https(
'pokeapi.co',
'/api/v2/pokemon/25',
);
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> {
String _pokemonName = 'まだデータを取得していません';
/**
* PokeAPIからデータを取得し、ポケモン名を画面に表示する関数。
*
* 入力: なし
* 出力: _pokemonNameを更新して画面を再描画する
*/
Future<void> _loadPokemonData() async {
final Map<String, dynamic> data = await fetchPokemonData();
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: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ElevatedButton(
onPressed: _loadPokemonData,
child: const Text('APIからデータを取得する'),
),
const SizedBox(height: 24),
Text(
_pokemonName,
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
);
}
}
実行して確認する
ターミナルでアプリを起動します。
flutter run
画面に表示されたボタンを押します。
APIからデータを取得する
ボタンを押したあと、画面に次のように表示されれば成功です。
pikachu
前の節では長いJSON文字列が表示されていました。
今回は、その中から name だけを取り出せています。
ここまで来ると、API通信が少し「アプリらしい処理」に変わってきます。
Mapから値を取り出す基本形
Mapから値を取り出すときは、次のように書きます。
final String name = data['name'] as String;
final int height = data['height'] as int;
final int weight = data['weight'] as int;
PokeAPIのポケモンデータでは、height や weight も取得できます。
たとえば、次のように表示用の文章を作れます。
final String name = data['name'] as String;
final int height = data['height'] as int;
final int weight = data['weight'] as int;
final String message = '名前: $name\n高さ: $height\n重さ: $weight';
ただし、画像URLなどは少し深い場所に入っています。
それは次の節以降で扱います。
少しだけ深いJSONを見る
PokeAPIのデータには、単純な値だけでなく、入れ子になったデータもあります。
たとえば、画像URLは次のような場所にあります。
sprites → front_default
Mapで取り出すと、次のような書き方になります。
final Map<String, dynamic> sprites =
data['sprites'] as Map<String, dynamic>;
final String imageUrl = sprites['front_default'] as String;
少し階段を降りるような感覚です。
data
└─ sprites
└─ front_default
ただし、この節ではまず name の取得だけで十分です。
画像表示は後の節で、Image.network() と一緒に扱います。
よくあるエラー
1. jsonDecodeが使えない
次のようなエラーが出る場合があります。
The function 'jsonDecode' isn't defined.
この場合は、dart:convert のimportが抜けています。
import 'dart:convert';
これを main.dart の一番上に追加してください。
2. 型変換でエラーになる
次のようなコードを書くと、型が曖昧になりやすいです。
final data = jsonDecode(response.body);
短く書けますが、教材では最初のうちは型を明確にしておく方が安全です。
final Map<String, dynamic> data =
jsonDecode(response.body) as Map<String, dynamic>;
Dartはnull safetyや強い型付けを持つ言語として説明されており、学習段階でも型を明示しておくと、エラーの場所を見つけやすくなります。
3. data['Name'] と書いてしまう
JSONのキーは、大文字・小文字を区別します。
PokeAPIでは name です。
正しい例です。
final String name = data['name'] as String;
間違いやすい例です。
final String name = data['Name'] as String;
Name と書くと、値が取れません。
今回はまだやらないこと
この節では、JSON文字列をMapに変換し、ポケモン名を取り出しました。
ただし、まだ次の作業は行いません。
| まだやらないこと | 理由 |
|---|---|
| 検索番号を入力する | 次の節以降でTextFieldを使う |
| 画像URLを表示する | Image.network() の節で扱う |
| カードUIに整える | UI表示の節で扱う |
| ローディング表示 | 通信中の表示の節で扱う |
| エラー表示 | 失敗時の表示の節で扱う |
今は、JSONをMapに変換する一点に絞ります。
確認問題
問1
JSON文字列をDartのデータに変換するために使う関数は何ですか?
答え。
jsonDecode()
問2
jsonDecode() を使うために必要なimportはどれですか?
答え。
import 'dart:convert';
問3
APIから返ってきたJSON文字列は、どこに入っていますか?
答え。
response.body
問4
Mapから name を取り出すコードはどれですか?
答え。
final String name = data['name'] as String;
まとめ
この節では、APIから返ってきたJSON文字列を、DartのMapに変換しました。
今回の大事な流れは、次の通りです。
| 流れ | コード |
|---|---|
| JSON変換ライブラリを読み込む | import 'dart:convert'; |
| JSON文字列をMapに変換する | jsonDecode(response.body) as Map<String, dynamic> |
| 名前を取り出す | data['name'] as String |
| 画面を更新する | setState() |
JSONをMapに変換できると、APIデータを自由に扱えるようになります。
長い文字のかたまりだったJSONが、必要な情報を取り出せる「材料」に変わりました。
次の節では、ポケモン番号を入力して検索できるようにしていきます。