hiko1129's note

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

2019/12/21

はじめに

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.を置いた。

ディレクトリ構造

├── 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を呼ぶだけ

import 'package:flutter/material.dart';
import 'package:pokedex/app.dart';

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

app.dart

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

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 {
  
  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の読み込みを担当。
正直ファイル分ける必要ない。

import 'dart:convert';

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

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

  final List<PokedexContent> pokedex;
}

Future<dynamic> _loadJsonAsset({
   BuildContext context,
   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に突っ込んで、吐き出されたコードを一部編集したもの。 pokedexcontent.g.dartは、jsonserializableとbuild_runnerを使用して生成したコードが書かれているだけのもの。

import 'package:json_annotation/json_annotation.dart';

part 'pokedex_content.g.dart';

()
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;
}

()
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';
}

()
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);

  (name: 'HP')
  int hp;
  (name: 'Attack')
  int attack;
  (name: 'Defense')
  int defense;
  (name: 'Sp. Attack')
  int spAttack;
  (name: 'Sp. Defense')
  int spDefense;
  (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で表示しているページ。

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 = '/';

  
  _MainScreenState createState() => _MainScreenState();
}

class _MainScreenState extends State<MainScreen> {
  Assets _assets;

  
  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({
     String id,
     String name,
     String picturePath,
     String iconPath,
     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]}'),
                      ),
                    );
                  })
                ],
              )
            ],
          ),
        ),
      ),
    );
  }

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

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

終わりに

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


hiko1129

hiko1129
Twitter GitHub Qiita Qrunch Blog