公開日:2022.01.31

FlutterのNavigatorを使ってみた

テクログsmartphone

こんにちは!
のりさんです。

今回はFlutterの画面遷移について書こうと思います。

はじめに

FlutterではNavigatorを使った画面遷移の仕組みがあるようです。
現在Navigatorは2.0になったようですが、引き続き前のやり方(1.0)も利用できるみたいです。
まずは「1.0」の画面遷移方法を知りたいので、今回はそちらで簡単なアプリを作ってみたいと思います。

環境

  • Windows10
  • Android Studio ArcticFox 2020.3.1(patch 4)
  • Flutter 2.8.1
  • パッケージ(flutter_riverpod/flutter_slidable)

Navigatorの仕組み (1.0)

以下の図のように画面用のスタックを利用してメソッドを使った命令的な画面遷移をしていくみたいですね。
pushするとウィジェットが追加され、popするとウィジェットが削除されるようです。
画面スタックに存在している一番上のウィジェットが画面に表示されるといった感じでしょうか。

アプリの内容

  • トップ、リスト、詳細の3つのウィジェット(画面)が存在
  • トップ画面から「ボタン」タップでリスト画面へ遷移する
  • リスト画面から「アイテム」タップで詳細画面に遷移する
  • リスト画面から「戻る」タップでトップ画面に遷移する
  • 詳細画面から「戻る」タップでリスト画面に遷移する
  • 戻ったときに前画面の操作状態がそのままになっている

完成アプリ

完成したアプリは以下のようになりました!
状態管理にriverpod、リストのスライド処理にslidableを使ってます。
リスト画面にてスライドした状態で詳細画面から戻っても状態が維持されていますね。

ソースコード

routesで画面を設定したら、push(pushNamed)やpopを呼び出すだけで画面遷移できました。
popについては戻るボタンが内部で呼び出しているようなので今回ソースに含めていません。

ファイル









pubspec.yaml

...
dependencies:
  flutter:
    sdk: flutter
  flutter_riverpod: ^1.0.3
  flutter_slidable: ^1.2.0
...

main.dart

MaterialAppのルート(routes)と初期ページ(initialRoute)の設定をしました。
例えば「top」という文字列の場合はTopScreenに画面遷移します、という感じで設定しています。


import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:screen_transition/screens/top.dart';
import 'package:screen_transition/screens/list.dart';
import 'package:screen_transition/screens/detail.dart';

void main() {
  runApp(ProviderScope(child: const MyApp()));
}

class MyApp extends ConsumerWidget {
  const MyApp({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return MaterialApp(
        initialRoute: 'top',
        routes: <String, WidgetBuilder> { // 画面遷移の設定
          'top' : (BuildContext context) => TopScreen(),
          'list' : (BuildContext context) => ListScreen(),
          'detail' : (BuildContext context) => DetailScreen(),
        },
    );
  }
}

provider.dart

riverpodのprovider管理用のファイルにしています。
ListViewに表示するデータや新規作成する際の番号を管理し
リストの初期化ではデータベースは利用せずにアイテムクラスを5件生成するようにしています。


import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'model/my_list_item.dart';

final providerListItem = StateProvider<List<MyListItem>>((ref) => const [1,2,3,4,5].map((e) => MyListItem(
  title: "リストアイテムタイトル [$e]",
  description: "リストアイテムデータ詳細 [$e]",
)).toList());

final providerListNumber = StateProvider((ref)=>ref.read(providerListItem).length + 1);

my_list_item.dart

タイトルと説明のプロパティを持ったリスト項目用クラスです。

class MyListItem {
  late final String title;
  late final String description;
  MyListItem({required String title, required String description}) {
    this.title = title;
    this.description = description;
  }
}

top.dart

トップ画面用のウィジェットです。
ボタンをタップするとリスト画面に遷移します。

ボタン押下イベントでpushNamedを呼び出しています。
main.dartにあるルート設定した文字列を指定することで画面遷移が可能です。


import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class TopScreen extends ConsumerWidget {
  const TopScreen({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
        appBar: AppBar(title: const Text("トップ")),
        body: Container(
            child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Center(child: const Text('トップ')),
            ElevatedButton(
              child: Text("リスト画面へ"),
              onPressed: () => Navigator.of(context).pushNamed('list'), // 画面遷移
            ),
          ],
        )));
  }
}

list.dart

リスト画面用のウィジェットです。
・丸い+ボタンをタップするとリストに新しい項目が追加可能
・項目をタップして詳細画面に遷移(pushNamed)
・項目を左にスライドすると削除ボタンが表示され、タップしたら削除されます

「Navigator.of(context).pushNamed(‘detail’, arguments: index)」
上記の第2引数で画面遷移先に引数を渡しています。


import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:screen_transition/model/my_list_item.dart';
import 'package:screen_transition/provider.dart';

class ListScreen extends ConsumerWidget {
  const ListScreen({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final GlobalKey<AnimatedListState> _listKey = GlobalKey();
    final List<MyListItem> _list = ref.watch(providerListItem);
    return Scaffold(
        appBar: AppBar(title: Text("リスト")),
        body: AnimatedList(
            key: _listKey,
            initialItemCount: _list.length,
            itemBuilder: (context, index, animation) {
              return Slidable(
                  endActionPane: ActionPane(motion: ScrollMotion(), children: [
                    SlidableAction(
                      onPressed: (_) {
                        var _removeItem = _list.removeAt(index);
                        if (_listKey.currentState != null) {
                          _listKey.currentState?.removeItem(index, (context, animation) {
                            return Container(
                              decoration: BoxDecoration(
                                  border: Border(bottom: BorderSide(width: 1.0, color: Colors.black12))),
                              child: SizeTransition(
                                  sizeFactor: animation,
                                  child: ListTile(title: Text(_removeItem.title))),
                            );
                          });
                        }
                      },
                      backgroundColor: Color(0xFFFE5A50),
                      foregroundColor: Colors.white,
                      icon: Icons.delete,
                      label: 'Delete',
                    ),
                  ]),
                  child: Container(
                      decoration: BoxDecoration(
                          border: Border(bottom: BorderSide(width: 1.0, color: Colors.black12))),
                      child: SizeTransition(
                          sizeFactor: animation,
                          child: ListTile(
                            title: Text(_list[index].title),
                            trailing: Icon(Icons.chevron_right),
                            onTap: () => Navigator.of(context).pushNamed('detail', arguments: index),
                          ))));
            }),
        floatingActionButton: FloatingActionButton(
          child: Icon(Icons.add),
          onPressed: () {
            String _num = ref.read(providerListNumber.state).state.toString();
            var _listViewState = ref.read(providerListItem.state);
            _listViewState.update((state) {
              List<MyListItem> _newList = List.of(_list); // コピー
              _newList.add(MyListItem(title: "リストアイテムタイトル [$_num]", description: "リストアイテムデータ詳細 [$_num]"));
              ref.read(providerListNumber.state).state++;
              return _newList; // インスタンスを差し替えてUIを更新
            });
          },
        ));
  }
}

detail.dart

リスト項目の詳細画面です。
「ModalRoute.of(context)?.settings.arguments」の部分で前の画面から渡された引数を利用しています。


import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:screen_transition/provider.dart';
import 'package:screen_transition/model/my_list_item.dart';

class DetailScreen extends ConsumerWidget {
  const DetailScreen({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 画面遷移時に渡されたパラメータを取得
    final int _index = ModalRoute.of(context)?.settings.arguments as int;
    // リストの項目データを取得
    final List<MyListItem> _list = ref.read(providerListItem);

    return Scaffold(
        appBar: AppBar(title: Text( _list[_index].title )),
        body: Container(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [Center(child: Text( _list[_index].description ))],
            )
        )
    );
  }
}

おわりに

Navigator1.0でメソッドを利用した命令的な画面遷移をしてみました。
今のところNavigator 1.0の仕組みだけでも問題なさそう感じです。

2.0は宣言的な画面遷移というものらしいです。
どのようにNavigator 2.0で進化したのか、引き続きみていきたいと思います。

ではまた!

この記事を書いた人

のりさん

入社年2014年

出身地東京都

業務内容開発

特技または趣味水泳・筋トレ・旅行

のりさんの記事一覧へ

テクログに関する記事一覧