Flutterでポケモン図鑑作ってみた

はじめに

SwiftUIでさっくとポケモン図鑑作ってみたというQiitaの記事を見かけたので、Flutterでポケモン図鑑を作ってみた。

使用するデータには、記事内で紹介されているGitHub - fanzeyi/Pokemon-DB: A Pokemon database in JSON format.を使用した。

完成品

Androidで実行しているもの

Webで実行しているもの

データの準備

git submoduleを使って、assets/pokemonにGitHub - fanzeyi/Pokemon-DB: A Pokemon database in JSON format.を置いた。

ディレクトリ構造

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
├── app.dart
├── generated_plugin_registrant.dart
├── main.dart
├── models
│   ├── pokedex_content.dart
│   └── pokedex_content.g.dart
├── screens
│   └── main_screen.dart
└── utils
    └── load_assets.dart

main.dart

app.dart内のAppを呼ぶだけ

1
2
3
4
import 'package:flutter/material.dart';
import 'package:pokedex/app.dart';

void main() => runApp(App());

app.dart

assetsを読み込んで、読み込み次第main screenを表示。 assetsはProviderで下位ウィジェットに渡すようにしている。 エラーを標準出力しているだけにしているけど、本来はcrashlyticsなどに送るなどしたほうが良い。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import 'package:flutter/material.dart';
import 'package:pokedex/screens/main_screen.dart';
import 'package:pokedex/utils/load_assets.dart';
import 'package:provider/provider.dart';

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FutureBuilder<Assets>(
      future: loadAssets(context),
      builder: (context, snapshot) {
        if (snapshot.hasError) {
          print(snapshot.error);
        }

        if (!snapshot.hasData) {
          return const Center(
            child: CircularProgressIndicator(),
          );
        }

        return Provider<Assets>(
          create: (_) => snapshot.data,
          child: MaterialApp(
            title: 'pokedex',
            theme: ThemeData(
              primarySwatch: Colors.red,
            ),
            initialRoute: MainScreen.path,
            routes: {
              MainScreen.path: (context) => MainScreen(),
            },
          ),
        );
      },
    );
  }
}

utils/loadAssets

pokedex.jsonの読み込みを担当。
正直ファイル分ける必要ない。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import 'dart:convert';

import 'package:pokedex/models/pokedex_content.dart';
import 'package:meta/meta.dart';
import 'package:flutter/material.dart';

class Assets {
  Assets({@required this.pokedex}) : assert(pokedex != null);

  final List<PokedexContent> pokedex;
}

Future<dynamic> _loadJsonAsset({
  @required BuildContext context,
  @required String filePath,
}) async {
  return json.decode(await DefaultAssetBundle.of(context).loadString(filePath));
}

Future<Assets> loadAssets(BuildContext context) async {
  final pokedexJson = List<Map<String, dynamic>>.from(await _loadJsonAsset(
    context: context,
    filePath: 'assets/pokemon/pokedex.json',
  ) as List);

  final pokedex =
      pokedexJson.map((json) => PokedexContent.fromJson(json)).toList();

  return Assets(pokedex: pokedex);
}

models/pokedex_content.dart

pokedex.jsonをJSON to Dartに突っ込んで、吐き出されたコードを一部編集したもの。 pokedex_content.g.dartは、json_serializableとbuild_runnerを使用して生成したコードが書かれているだけのもの。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import 'package:json_annotation/json_annotation.dart';

part 'pokedex_content.g.dart';

@JsonSerializable()
class PokedexContent {
  PokedexContent({
    this.id,
    this.name,
    this.type,
    this.base,
  });

  factory PokedexContent.fromJson(Map<String, dynamic> json) =>
      _$PokedexContentFromJson(json);
  Map<String, dynamic> toJson() => _$PokedexContentToJson(this);

  int id;
  Name name;
  List<String> type;
  Base base;
}

@JsonSerializable()
class Name {
  Name({
    this.english,
    this.japanese,
    this.chinese,
    this.french,
  });

  factory Name.fromJson(Map<String, dynamic> json) => _$NameFromJson(json);
  Map<String, dynamic> toJson() => _$NameToJson(this);

  String english;
  String japanese;
  String chinese;
  String french;
}

class BaseKeys {
  static const hp = 'hp';
  static const attack = 'attack';
  static const defense = 'defense';
  static const spAttack = 'spAttack';
  static const spDefense = 'spDefense';
  static const speed = 'speed';
}

@JsonSerializable()
class Base {
  Base({
    this.hp,
    this.attack,
    this.defense,
    this.spAttack,
    this.spDefense,
    this.speed,
  });

  factory Base.fromJson(Map<String, dynamic> json) => _$BaseFromJson(json);
  Map<String, dynamic> toJson() => _$BaseToJson(this);

  @JsonKey(name: 'HP')
  int hp;
  @JsonKey(name: 'Attack')
  int attack;
  @JsonKey(name: 'Defense')
  int defense;
  @JsonKey(name: 'Sp. Attack')
  int spAttack;
  @JsonKey(name: 'Sp. Defense')
  int spDefense;
  @JsonKey(name: 'Speed')
  int speed;

  Map<String, dynamic> toMap() {
    return <String, dynamic>{
      BaseKeys.hp: hp,
      BaseKeys.attack: attack,
      BaseKeys.defense: defense,
      BaseKeys.spAttack: spAttack,
      BaseKeys.spDefense: spDefense,
      BaseKeys.speed: speed,
    };
  }
}

screens/main_screen.dart

assetsからpokedexを取り出して、ポケモンを表示しているCardをListで表示しているページ。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
import 'package:flutter/material.dart';
import 'package:pokedex/models/pokedex_content.dart';
import 'package:pokedex/utils/load_assets.dart';
import 'package:provider/provider.dart';
import 'package:intl/intl.dart';
import 'package:url_launcher/url_launcher.dart';

class MainScreen extends StatefulWidget {
  static const path = '/';

  @override
  _MainScreenState createState() => _MainScreenState();
}

class _MainScreenState extends State<MainScreen> {
  Assets _assets;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _assets = Provider.of<Assets>(context);
  }

  Widget _buildList(List<PokedexContent> pokedex) {
    final numberFormat = NumberFormat('000');

    return ListView.builder(
      itemBuilder: (context, index) {
        final pokemon = pokedex[index];

        final pokemonId = numberFormat.format(pokemon.id);

        return _buildPokemonCard(
          id: pokemonId,
          name: pokemon.name.english,
          base: pokemon.base,
          picturePath: 'pokemon/images/$pokemonId.png',
          iconPath: 'pokemon/sprites/${pokemonId}MS.png',
        );
      },
      itemCount: pokedex.length,
    );
  }

  Widget _buildPokemonCard({
    @required String id,
    @required String name,
    @required String picturePath,
    @required String iconPath,
    @required Base base,
  }) {
    final chips = <String, dynamic>{
      'HP': base.hp,
      'Attack': base.attack,
      'Defense': base.defense,
      'Sp. Attack': base.spAttack,
      'Sp. Defense': base.spDefense,
      'Speed': base.speed,
    };

    const pathPrefix = 'assets';

    return GestureDetector(
      onTap: () async => launch('https://www.pokemon.jp/zukan/detail/$id.html'),
      child: Card(
        child: Padding(
          padding: const EdgeInsets.all(4),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            mainAxisSize: MainAxisSize.max,
            children: <Widget>[
              ListTile(
                leading: Image.asset('$pathPrefix/$iconPath'),
                title: Text(name),
              ),
              Image.asset('$pathPrefix/$picturePath'),
              Wrap(
                children: <Widget>[
                  ...chips.keys.map((chipLabel) {
                    return Padding(
                      padding: const EdgeInsets.only(right: 4),
                      child: Chip(
                        label: Text('$chipLabel: ${chips[chipLabel]}'),
                      ),
                    );
                  })
                ],
              )
            ],
          ),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    final pokedex = _assets.pokedex;

    return Scaffold(
      appBar: AppBar(
        title: const Text('Pokedex'),
      ),
      body: _buildList(pokedex),
    );
  }
}

終わりに

雑だけど、とりあえず作ってみたので書きました。
試しにWebで動かしてみたけど、普通に動いて感動した。

Built with Hugo
テーマ StackJimmy によって設計されています。