CONTENT
ここから
前の節では、Image.network() を使ってポケモン画像を表示しました。
今回は、名前と画像をそのまま並べるのではなく、カードUIとして見やすく整えます。
Flutterでは、まとまりのある情報を表示するときに Card をよく使います。
ポケモン図鑑のように「1体分の情報」を見せる画面と相性が良いです。
この節で作ること
| 作業 | 内容 |
|---|---|
Card を使う | 情報をひとまとまりにする |
| 画像を中央に表示する | Image.network() をカード内に置く |
| 名前を大きく表示する | Text のstyleを調整する |
| 番号も表示する | 入力したポケモン番号をカードに出す |
忙しい方はここだけ見て
カードUIの基本形は、次のコードです。
Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
children: <Widget>[
Image.network(
_pokemonImageUrl!,
width: 160,
height: 160,
fit: BoxFit.contain,
),
const SizedBox(height: 16),
Text(
_pokemonName,
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
),
),
],
),
),
)
Cardとは
Card は、情報をひとつの箱として見せるWidgetです。
ただ文字と画像を並べるより、カードに入れることで「検索結果」として見やすくなります。
Card(
child: Text('pikachu'),
)
さらに、Padding を入れると余白ができます。
Card(
child: Padding(
padding: const EdgeInsets.all(24),
child: Text('pikachu'),
),
)
余白があるだけで、画面の印象はかなり変わります。
アプリらしい、少し落ち着いた見た目になります。
ポケモン番号用の変数を追加する
カード内に番号も表示するため、状態変数を追加します。
String? _pokemonIdText;
状態変数は、次のようになります。
String _pokemonName = 'ポケモン番号を入力してください';
String? _pokemonImageUrl;
String? _pokemonIdText;
検索処理を少し変更する
検索に成功したら、名前・画像URL・番号を保存します。
/**
* 入力された番号を使ってPokéAPIを呼び出し、カード表示用の情報を更新する関数。
*
* 入力: なし
* 出力: 名前、画像URL、番号を画面表示用に更新する
*/
Future<void> _searchPokemon() async {
final int? pokemonId = int.tryParse(_pokemonIdController.text);
if (pokemonId == null) {
setState(() {
_pokemonName = '数字を入力してください';
_pokemonImageUrl = null;
_pokemonIdText = null;
});
return;
}
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();
});
}
pokemonId.toString() で、数値を画面表示用の文字列に変えています。
カードUIを作る
画像URLがあるときだけ、カードを表示します。
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,
),
),
検索前はメッセージだけ表示します。
検索後はカードを表示します。
完成コード
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;
/**
* 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;
});
return;
}
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();
});
}
/**
* ポケモン検索画面を構築する関数。
*
* 入力: 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,
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),
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 と入力して検索します。
No. 25
pikachu
ピカチュウの画像・番号・名前がカード内に表示されれば成功です。
よくあるエラー
| エラー | 原因 | 対応 |
|---|---|---|
| カードが出ない | _pokemonImageUrl がnull | 検索処理で画像URLを保存する |
| nullエラーになる | ! を早く使っている | if (_pokemonImageUrl != null) の中で使う |
| 画像だけ出ない | URL取得ミス | sprites['front_default'] を確認 |
| 余白がない | Padding なし | Card の中に Padding を入れる |
まとめ
この節では、ポケモン情報をカードUIで表示しました。
大事なポイントは3つです。
| ポイント | 内容 |
|---|---|
Card | 情報をまとまりとして見せる |
Padding | カード内に余白を作る |
Column | 番号・画像・名前を縦に並べる |
これで、検索結果がかなりアプリらしくなりました。
次は、API通信中にローディング表示を出して、待ち時間も分かりやすくしていきます。