今是昨非

今是昨非

日出江花红胜火,春来江水绿如蓝

Flutterレイアウトの基本——ページナビゲーションと戻る

Flutter レイアウトの基本 —— ページナビゲーションと値の受け渡し#

ナビゲーションと言えば、最も一般的なのは iOS のナビゲーションコントローラーの push と pop の効果ですが、Flutter にも同様の効果があり、使用されるのはNavigatorコンポーネントです。

では、Flutter におけるナビゲーション効果Navigator.pushNavigator.popの使用を見てみましょう。

簡単な使用法#

コードは以下の通りです:


class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'ナビゲーションデモ1',
      home: new FirstScreen(),
    );
  }
}

class FirstScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: AppBar(
        title: Text('ナビゲーションページ'),
      ),
      body: Center(
          child: ElevatedButton(
        child: Text('商品詳細ページを見る'),
        onPressed: () {
          Navigator.push(context,
              new MaterialPageRoute(builder: (context) => new SecondScreen()));
        },
      )),
    );
  }
}

class SecondScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('SecondScreen'),
      ),
      body: Center(
        child: OutlinedButton(
          child: Text('戻る'),
          onPressed: () {
            Navigator.pop(context);
          },
        ),
      ),
    );
  }
}

Navigator.pushを使用する際は、遷移する画面をMaterialPageRouteで囲む必要があります。
コード内のSecondScreenにはボタンが追加されており、クリック時の実装方法はNavigator.popで、戻るために使用されます。しかし通常、Navigator.popを特別に実装する必要はありません。なぜなら、iOS ではAppBarを使用すると、自動的に左上に戻るボタンが追加されるからです。また、Android ではシステムの戻るボタンを使用しても直接戻ることができます。

名前付きナビゲーションの使用#

ルーティングのジャンプに似て、クラス名ではなく名前を使用してジャンプします。

以下のコードでは、3 つの画面を定義しています。MyFirstPageがホームページで、MySecondPageMyThirdPageが 2 つの異なる画面です。ホームページには 2 つのボタンがあり、それぞれMySecondPageMyThirdPageにジャンプします。同時に、ジャンプ時にパラメータを渡し、対応するページに表示します。

コードは以下の通りです:


class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyFirstPage(),
      routes: <String, WidgetBuilder>{
        '/a': (BuildContext context) => MySecondPage(title: 'Page A'),
        'b': (BuildContext context) => MyThirdPage(title: 'Page B'),
      },
    );
  }
}

class MyFirstPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('ホームページ'),
      ),
      body: Center(
        child: Column(
          children: [
            OutlinedButton(
              onPressed: () {
                Navigator.pushNamed(context, '/a');
              },
              child: new Text('PageA'),
            ),
            OutlinedButton(
              onPressed: () {
                Navigator.pushNamed(context, 'b');
              },
              child: new Text('PageB'),
            ),
          ],
        ),
      ),
    );
  }
}

class MySecondPage extends StatelessWidget {
  final String title;
  const MySecondPage({Key? key, required this.title}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('セカンドページ'),
      ),
      body: Center(
        child: OutlinedButton(
          onPressed: () {
            Navigator.pop(context);
          },
          child: new Text(this.title),
        ),
      ),
    );
  }
}

class MyThirdPage extends StatelessWidget {
  final String? title;
  const MyThirdPage({Key? key, this.title}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('サードページ'),
      ),
      body: Center(
        child: new OutlinedButton(
            onPressed: () {
              Navigator.pop(context);
            },
            child: new Text(this.title ?? 'デフォルトページ名')),
      ),
    );
  }
}

ここで注意すべき点がいくつかあります:

まず、routesの使用で、型は<String, WidgetBuilder>です。前者は遷移するページの名前で、後者は遷移先のページです。また、遷移するページの名前に/は必須ではなく、自由に定義できますが、ルーティングとして理解する場合は、統一したルールで命名することをお勧めします。

次に、遷移するNavigatorの使用について、前の例では直接遷移する方法はNavigator.pushを使用していましたが、ここではNavigator.pushNamedを使用しています。

最後に、ページ間の値の受け渡しについて注意が必要です。

ページ間の値の受け渡し#

iOS 開発と同様に、ページ間の値の受け渡しは上位画面から下位画面への受け渡しと、下位画面から上位画面へのコールバック受け渡しに分かれます。

上位ページから下位ページへの値の受け渡し#

上記のコードは上位ページから下位ページへの値の受け渡しですが、MySecondPageMyThirdPageの書き方は異なります。比較は以下の通りです:

wecom20210730-172529.png

異なる点は 2 つあります:初期宣言が異なること、具体的な使用法が異なることです。

MySecondPageで宣言されたtitle属性は非 null の String で、required修飾子を使用しています(この点に注意が必要です。requiredであり、@requiredではありません。一部の文書は更新されていません)。使用する際はそのまま使用します。

MyThirdPageで宣言されたtitle属性は nullable の String で、required修飾子は使用されていませんが、使用する際には??を追加してデフォルト値を提供しています。

混同すると、使用時にThe parameter 'title' can't have a value of 'null' because of its type, but the implicit default value is 'null'. Try adding either an explicit non-'null' default value or the 'required' modifier.dart(missing_default_value_for_parameter)というエラーが発生する可能性があります。理由は nullable と non-nullable の違いで、修正方法は、1 つは nullable 型として宣言し、使用時に判断を加えること、もう 1 つはrequired修飾子を使用して non-nullable として宣言することです。

下位ページから上位ページへの値の受け渡し#

Navigator.Pushメソッドは戻り値を持つことができ、戻り値はFuture型です。Navigator.Popメソッドを呼び出す際に、第二のオプション引数に内容を渡すと、Navigator.Pushで戻ります。

コードは以下の通りです:


void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyFirstPage(),
      routes: <String, WidgetBuilder>{
        '/a': (BuildContext context) => MySecondPage(title: 'Page A'),
        'b': (BuildContext context) => MyThirdPage(title: 'Page B'),
      },
    );
  }
}

class MyFirstPage extends StatelessWidget {
  String callbackText = '';
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('ホームページ'),
      ),
      body: Center(
        child: Column(
          children: [
            OutlinedButton(
              onPressed: () {
                _pagePush(context, '/a');
              },
              child: new Text('PageA'),
            ),
            OutlinedButton(
              onPressed: () {
                // Navigator.pushNamed(context, 'b');
                _pagePush(context, 'b');
              },
              child: new Text('PageB'),
            ),
            Text(callbackText),
          ],
        ),
      ),
    );
  }
}

_pagePush(BuildContext context, String target) async {
  final result = await Navigator.pushNamed(context, target);
  ScaffoldMessenger.of(context).showSnackBar(
    new SnackBar(content: Text("$result")),
  );
}

class MySecondPage extends StatelessWidget {
  final String title;
  const MySecondPage({Key? key, required this.title}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('セカンドページ'),
      ),
      body: Center(
        child: OutlinedButton(
          onPressed: () {
            Navigator.pop(context, 'SecondPage コールバック');
          },
          child: new Text(this.title),
        ),
      ),
    );
  }
}

class MyThirdPage extends StatelessWidget {
  final String? title;
  const MyThirdPage({Key? key, this.title}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('サードページ'),
      ),
      body: Center(
        child: new OutlinedButton(
            onPressed: () {
              Navigator.pop(context, 'ThirdPage コールバック');
            },
            child: new Text(this.title ?? 'デフォルトページ名')),
      ),
    );
  }
}

効果は以下の通りです:

pagecallback.gif

上記のボタンのクリック方法に注意してください。_pagePushメソッドを個別に封装し、async修飾子を使用しています。理由はNavigator.pushの戻り値がFuture型であり、awaitを使用する必要があるからです。awaitasync修飾子を持つメソッド内でのみ使用できます。ReactNative を書いたことがある方は、この書き方に馴染みがあるでしょう。

参考#

Navigator Dev Doc
Flutter 無料動画第 4 季 - ページナビゲーションとその他
Dart におけるパラメータが null の値を持つことができない理由
新しいページからデータを前のページに返す
Futureクラス

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。