Chapter 04

Step3: メイン画面とサブ画面を移動しよう

sugit
sugit
2023.05.07に更新

Step3 の概要

Step3 では、Navigator を使って 2 つのページを遷移させます。

本章で学べること

  • Dart ファイルの分割の方法
  • Navigator での画面遷移の方法
  • ListView の効果的な使い方

画面遷移を実装しよう

ポケモン図鑑のイメージは、まずメイン画面はリストになっていて、選択すると詳細が表示されるようなイメージですね。
Step3 ではそれを作ってみましょう。

メイン画面と詳細画面の移動については Navigator を使用します。

https://flutter.dev/docs/development/ui/navigation

まずはメイン画面と先に作ったポケモンの画面を分けていきます。

.
├── main.dart
└── poke_detail.dart

PokeDetail ウィジェットを poke_detail.dart にうつします。

main.dart からは import './poke_detail'; で取り込みます。ここを相対パスではなくパッケージパスにした方が良いということもありますが、とりあえずこれでいきます。

この import パスについては Linter でルールを設定できます。Linter については別冊をお待ちください。

main.dart ではかわりに TopPage ウィジェットを作成し、中央にボタンをひとつ配置します。そのボタンをクリックすると、PokeDetail に遷移するようにします。

class TopPage extends StatelessWidget {
  const TopPage({Key? key}) : super(key: key);
  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: ElevatedButton(
          child: const Text('detail'),
          onPressed: () => {
            Navigator.of(context).push(
              MaterialPageRoute(
                builder: (BuildContext context) => const PokeDetail(),
              ),
            ),
          },
        ),
      ),
    );
  }
}

Navigator v1 の push/pop のみでいったん作成します。Navigator には色々なページ遷移の作り方があるので、実はちょっと難しいポイントだということを覚えておいてください。

画面を戻したい場合はスワイプで戻ります。

メイン画面の ListView を作る

では画面遷移のようなものができたので、メイン画面をリストにしてみましょう。

ポケモンはとんでもない数(id=1010)いますので(執筆時)単純なリストを作るとちょっとまずいです。まずさも含めてみていきましょう。

ということで、さっきのボタンを Widget として別のクラスにして、1010 種類のポケモンそれぞれのために並べてみましょう!!

class TopPage extends StatelessWidget {
  const TopPage({Key? key}) : super(key: key);
  
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListView(
        children: const [
          PikaButton(),
          PikaButton(),
          PikaButton(),
          PikaButton(),
          PikaButton(),
          PikaButton(),
          PikaButton(),
          PikaButton(),
          PikaButton(),
          PikaButton(),
          PikaButton(),
          PikaButton(),
          PikaButton(),
          PikaButton(),
        ],
      ),
    );
  }
}

class PikaButton extends StatelessWidget {
  const PikaButton({Key? key}) : super(key: key);
  
  Widget build(BuildContext context) {
    return ElevatedButton(
      child: const Text('pikachu'),
      onPressed: () => {
        Navigator.of(context).push(
          MaterialPageRoute(
            builder: (BuildContext context) => const PokeDetail(),
          ),
        ),
      },
    );
  }
}

いや、これは面倒だ。

ふふふ、馬鹿みたいに並べちゃって・・・
こういうときは Iterator っていうのを使うんだぜ!

class TopPage extends StatelessWidget {
  const TopPage({Key? key}) : super(key: key);
  
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListView(
        children: List.generate(10000, (id) => id)
            .map((val) => PikaButton(index: val))
            .toList(),
      ),
    );
  }
}

とはいえ、本来はこちらのほうがベターです。

class TopPage extends StatelessWidget {
  const TopPage({Key? key}) : super(key: key);
  
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListView.builder(
          itemCount: 10000,
          itemBuilder: (context, index) => PikaButton(index: index)),
    );
  }
}

ListView についてはこちらの動画でパフォーマンスのことについて説明されています。

https://www.YouTube.com/watch?v=qax_nOpgz7E

平たく言えば、レンダリングを必要最小限にすべきということです。

では、リストから選択すると詳細に行くんだよ、ということがわかるようにしてみましょう。
リストは ListTile で作ると簡単です。

class PokeListItem extends StatelessWidget {
  const PokeListItem({Key? key, required this.index}) : super(key: key);
  final int index;
  
  Widget build(BuildContext context) {
    return ListTile(
      leading: Image.network(
        "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/25.png",
        height: 50,
        width: 50,
      ),
      title: const Text('pikachu'),
      onTap: () => {
        Navigator.of(context).push(
          MaterialPageRoute(
            builder: (BuildContext context) => const PokeDetail(),
          ),
        ),
      },
    );
  }
}

この PokeListItem を ListView.builder で回せばいい感じです。

それっぽくはなりましたが、ちょっと見た目をもう少しそれっぽくしたいので、調整します。

class PokeListItem extends StatelessWidget {
  const PokeListItem({Key? key, required this.index}) : super(key: key);
  final int index;
  
  Widget build(BuildContext context) {
    return ListTile(
      leading: Container(
        width: 80,
        decoration: BoxDecoration(
          color: Colors.yellow.withOpacity(.5),
          borderRadius: BorderRadius.circular(10),
          image: const DecorationImage(
            fit: BoxFit.fitWidth,
            image: NetworkImage(
              "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/25.png",
            ),
          ),
        ),
      ),
      title: const Text(
        'Pikachu',
        style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
      ),
      subtitle: const Text(
        '⚡️electric',
      ),
      trailing: const Icon(Icons.navigate_next),
      onTap: () => {
        Navigator.of(context).push(
          MaterialPageRoute(
            builder: (BuildContext context) => const PokeDetail(),
          ),
        ),
      },
    );
  }
}

LitTile は leading, title, subtitle, trailing などのスマホあるあるな固定位置に Widget を配置できるのでいい感じですね。

PokeListItem が随分と大きくなってしまったので、別のファイルに取り出しておきましょう。

これにて Step3 は終了です。