8
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Flutterの公式ドキュメントを読んで理解する 〜Navigation & Routing〜

Posted at

今回読んだページ

個人的なまとめ

  • 画面遷移はNavigator.push()を使う。
  • 元の画面に戻るときはNavigator.pop()を使う。
  • 名前付きRouteに遷移する際は、Navigator.pushNamed()を使う。
  • 名前付きRouteに遷移するときに引数を渡す場合は、Navigator.pushNamed()argumentsを指定する
  • 引数を読み出すときは、ModalRoute.of(context).settings.argumentsで取り出す。
  • 遷移したあとの画面から、元の画面へデータを渡したい時は、Navigator.pop()に第2引数を指定するとできる。

Animate a widget across screens

多くの場合、ユーザーが画面から画面へと移動するときに、アプリを通じてユーザーをガイドすることが役立ちます。
アプリを通じてユーザーをリードする一般的な技術は、ある画面から次の画面へとWidgetをアニメーションさせることです。
これにより、2つの画面を接続する視覚的なアンカーが作成されます。

Herowidgetを使って、ある画面から次の画面へとアニメーションさせます。
このレシピには、以下のステップがあります:

  1. 同じ画像を表示する2つの画面を作る
  2. 1つ目の画面にHerowidgetを追加する
  3. 2つ目の画面にHerowidgetを追加する

1. Create two screens showing the same image

この例では、両方の画面に同じ画像を表示させます。
ユーザーが画像をタップしたとき、1つ目の画面から2つ目の画面へと画像をアニメーションさせます。
ここでは、視覚的な構造を作成し、次のステップでアニメーションを取り扱います。

Note: この例は、Navigate to a new screen and backHandle tapsに基づいて構築されます。

class MainScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Main Screen'),
      ),
      body: GestureDetector(
        onTap: () {
          Navigator.push(context, MaterialPageRoute(builder: (_) {
            return DetailScreen();
          }));
        },
        child: Image.network(
          'https://picsum.photos/250?image=9',
        ),
      ),
    );
  }
}

class DetailScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: GestureDetector(
        onTap: () {
          Navigator.pop(context);
        },
        child: Center(
          child: Image.network(
            'https://picsum.photos/250?image=9',
          ),
        ),
      ),
    );
  }
}

2. Add a Hero widget to the first screen

2つの画面をアニメーションでつなぐためには、両方のスクリーンのImagewidgetをHerowidgetでラップします。

Hero(
  tag: 'imageHero',
  child: Image.network(
    'https://picsum.photos/250?image=9',
  ),
);

Herowidgetは2つの引数を必要とします。

  • tag: Heroを識別するオブジェクト。両方の画面で同じである必要があります。
  • child: 画面を通してアニメーションするWidget。

3. Add a Hero widget to the second screen

1つ目の画面との接続を完了するには、1つ目の画面のHeroと同じtagを持ったHerowidgetで、2つ目の画面のImagewidgetをラップします。

Hero(
  tag: 'imageHero',
  child: Image.network(
    'https://picsum.photos/250?image=9',
  ),
);

Note: このコードは1つ目の画面のものと同じです。
ベストプラクティスとしては、コードを繰り返すのではなく、再利用可能なWidgetを作成することです。
この例では、簡単のため、両方のWidgetに同じコードを使用しています。

2つ目の画面にHerowidgetを適用すると、画面間のアニメーションが動きます。

Interactive Example

See the Pen flutter-navigate-animate-1 by popy1017 (@popy1017) on CodePen.

Navigate to a new screen and back

たいていのアプリには、異なるタイプの情報を表示するために、いくつかの画面があります。
例えば、あるアプリが製品を表示するための画面を持っています。
ユーザーが製品の画像をタップすると、製品に関する詳細が新しい画面に表示されます。

用語: Flutterでは、画面やページをRouteと呼んでいます。このレシピの残りの部分では、Routeと呼びます。
(この記事では、「ルート」と書くとRootと混同するのでRouteと表記します。)

Androidでは、routeはActivityと同等です。
iOSでは、routeはViewControllerと同等です。
Flutterでは、routeはwidgetです。

Navigatorを使い、新しいrouteに移動します。このレシピは以下のステップを使います。
次の数セクションで、以下の3ステップで2つのroute間を移動するやり方を示します:

  1. Create two routes.
  2. Navigate to the second route using Navigator.push()
  3. Return to the first route using Navigator.pop()

1. Create two routes.

まず、使用する2つのrouteを作成します。
これは基本的な例なので、各Routeは1つのボタンのみを含みます。
1つ目のRouteのボタンを押すと、2つ目のRouteに遷移します。
2つ目のRouteのボタンを押すと、1つ目のRouteに帰ります。

まずは、視覚的な構造をセットアップします:

class FirstRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('First Route'),
      ),
      body: Center(
        child: RaisedButton(
          child: Text('Open route'),
          onPressed: () {
            // Navigate to second route when tapped.
          },
        ),
      ),
    );
  }
}

class SecondRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Second Route"),
      ),
      body: Center(
        child: RaisedButton(
          onPressed: () {
            // Navigate back to first route when tapped.
          },
          child: Text('Go back!'),
        ),
      ),
    );
  }
}

2. Navigate to the second route using Navigator.push()

新しいRouteに切り替えるためには、Navigator.push()メソッドを使います。
push()メソッドは、Navigatorによって管理されるRouteのスタックにRouteを追加します。
Routeはどこからくるのでしょうか?
独自に作成するか、MaterialPageRouteを使います。
プラットフォーム固有のアニメーションを使って新しいRouteに遷移するので、MaterialPageRouteは便利です。

// Within the `FirstRoute` widget
onPressed: () {
  Navigator.push(
    context,
    MaterialPageRoute(builder: (context) => SecondRoute()),
  );
}

3. Return to the first route using Navigator.pop()

どうやって2つ目のRouteを閉じて1つ目のRouteに戻るのでしょうか?
Navigator.pop()メソッドを使います。
pop()メソッドは、Navigatorによって管理されるRouteのスタックから現在のRouteを削除します。

元のRouteへ戻るのを実装するには、SecondRoutewidgetのonPressed()コールバックを更新します。

// Within the SecondRoute widget
onPressed: () {
  Navigator.pop(context);
}

Interactive example

See the Pen flutter-navigate-animate-2 by popy1017 (@popy1017) on CodePen.

Navigate with named routes

「Navigation to a new screen and back」レシピでは、新しいRouteを作成し、Navigatorにプッシュすることで、新しい画面に遷移する方法を学びました。

しかし、アプリのたくさんの部分で同じ画面に遷移する必要がある場合、このアプローチはコードの重複を引き起こす可能性があります。
解決策は「名前付きRoute(named Route)」を定義することで、ナビゲーションにそれを使うことです。

名前付きRouteを使うためには、Navigator.pushNamed()関数を使います。
この例では元のレシピから機能を複製し、次の手順で名前付きRouteを使用する方法を示します。

  1. Create two screens.
  2. Define the routes.
  3. Navigate to the second screen using Navigator.pushNamed()
  4. Return to the first screen using Navigator.pop()

1. Create two screens

class FirstScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('First Screen'),
      ),
      body: Center(
        child: RaisedButton(
          child: Text('Launch screen'),
          onPressed: () {
            // Navigate to the second screen when tapped.
          },
        ),
      ),
    );
  }
}

class SecondScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Second Screen"),
      ),
      body: Center(
        child: RaisedButton(
          onPressed: () {
            // Navigate back to first screen when tapped.
          },
          child: Text('Go back!'),
        ),
      ),
    );
  }
}

2. Define the routes

次に、MaterialAppコンストラクタに追加のプロパティ(initialRouteroutes)を設定して、Routeを定義します。

initialRouteプロパティは、アプリがどのRouteで始まるかを定義します。
routeプロパティは、設定したRouteにナビゲートする際に利用可能な名前付きRouteとビルドするWidgetを定義します。

MaterialApp(
  // "/"と名前が付けられたRouteからスタートする。
  // この場合、アプリはFirstScreen widgetからスタートする
  initialRoute: '/',
  routes: {
    // "/" routeにナビゲートするときは、FirstScreen widgetをビルドする
    '/': (context) => FirstScreen(),
    // "/second"routeにナビゲートするときは、SecondScrennをビルドする
    '/second': (context) => SecondScreen(),
  },
);

警告: initialRouteを使うときは、homeプロパティは定義しないでください。

3. Navigate to the second screen

WidgetとRouteを配置したら、Navigator.pushNamed()メソッドを使ってナビゲーションをトリガーします。
routesテーブルで定義されたWidgetを作成して画面を起動するようにFlutterに指示します。

FirstScreenwidgetのbuild()メソッドのonPressed()を更新します:

// Within the `FirstScreen` widget
onPressed: () {
  // Navigate to the second screen using a named route.
  Navigator.pushNamed(context, '/second');
}

4. Return to the first screen

1つ目の画面に戻るためには、Navigation.pop()を使います。

// Within the SecondScreen widget
onPressed: () {
  // Navigate back to the first screen by popping the current route
  // off the stack.
  Navigator.pop(context);
}

Pass arguments to a named route

Navigatorは共通の識別子を使ってアプリのどこからでも名前付きRouteにナビゲートする機能を提供します。
ある場合には、名前付きRouteに引数を渡す必要があるかもしれない。
例えば、/userrouteに遷移して、そのrouteにユーザーに関する情報を渡したいかもしれない。

Navigator.pushNamed()メソッドのargumentsパラメータを使うことでこれを達成できる。
ModalRoute.of()メソッドを使用するか、MaterialAppCupertinoAppコンストラクタが提供するonGenerateRoute()の中から引数を抽出する。

このレシピは、以下のステップにしたがって、名前付きRouteへの引数の渡し方とModalRoute.of()onGenerateRoute()を使った引数の読み出し方を示す:

  1. Define the arguments you need to pass.
  2. Create a widget that extracts the arguments.
  3. Register the widget in the routes table.
  4. Navigate to the widget.

1. Define the arguments you need to pass.

まず、新しいRouteに渡す必要がある引数を定義する。
この例では、2つのデータを渡す。: 画面のtitleと、message

両方のデータを渡すために、この情報を保持するClassを作成する。

// arguments parameterにはどんなオブジェクトでも渡せる
// この例では、カスタマイズ可能なtitleとmessageを含んだClassを作る
class ScreenArguments {
  final String title;
  final String message;

  ScreenArguments(this.title, this.message);
}

2. Create a widget that extracts the arguments

次に、ScreenArgumentsからtitlemessageを抽出して表示するWidgetを作成します。
ScreenArgumentsにアクセスするためには、ModalRoute.of()メソッドを使います。
このメソッドは引数を持った現在のRouteを返します。

// ModalRouteから必要な引数を抽出するWidget
class ExtractArgumentsScreen extends StatelessWidget {
  static const routeName = '/extractArguments';

  @override
  Widget build(BuildContext context) {
    // 現在のModalRoute settingsから引数を抽出し、ScreenArgumentsにキャストする
    final ScreenArguments args = ModalRoute.of(context).settings.arguments;

    return Scaffold(
      appBar: AppBar(
        title: Text(args.title),
      ),
      body: Center(
        child: Text(args.message),
      ),
    );
  }
}

3. Register the widget in the routes table

次に、MaterialAppwidgetに提供されたroutesにエントリーを追加します。
routesはRouteの名前に基づいて作成されるべきWidgetを定義します。

MaterialApp(
  routes: {
    ExtractArgumentsScreen.routeName: (context) => ExtractArgumentsScreen(),
  },
);

4. Navigate to the widget

最後に、ユーザーがボタンをタップしたときに、Navigator.pushNamed()を使って、ExtractArgumentsScreenにナビゲートする。
argumentsプロパティを介してRouteに引数を渡します。
ExtractArgumentsScreenはこれらの引数からtitlemessageを抽出します。

// 名前付きRouteにナビゲートするボタン。
// 名前付きRouteは自身で引数を抽出する。
RaisedButton(
  child: Text("Navigate to screen that extracts arguments"),
  onPressed: () {
    // ユーザーがボタンをタップしたとき、名前付きRouteにナビゲートして、
    // 任意のパラメータとして引数を渡す
    Navigator.pushNamed(
      context,
      ExtractArgumentsScreen.routeName,
      arguments: ScreenArguments(
        'Extract Arguments Screen',
        'This message is extracted in the build method.',
      ),
    );
  },
),

Alternatively, extract the arguments using onGenerateRoute

Widget内で直接引数を抽出する代わりに、onGenerateRoute()関数の中で引数を抽出し、Widgetに渡すこともできる。

onGenerateRoute()関数は、与えられたRouteSettingsに基づいて正しいRouteを作成する。

MaterialApp(
  // 名前付きRouteを扱う関数を提供する。この関数を使い、
  // プッシュされた名前付きRouteを特定し、正しい画面を作成する
  onGenerateRoute: (settings) {
    // PassArguments routeをプッシュした場合
    if (settings.name == PassArgumentsScreen.routeName) {
      // 正しい型(ScreenArguments)に引数をキャストする
      final ScreenArguments args = settings.arguments;

      // そして、引数から必要なデータを抽出し、
      // 正しい画面にデータを渡す
      return MaterialPageRoute(
        builder: (context) {
          return PassArgumentsScreen(
            title: args.title,
            message: args.message,
          );
        },
      );
    }
  },
);

Interactive example

See the Pen flutter-navigate-animate-3 by popy1017 (@popy1017) on CodePen.

Return data from a screen

ある場合では、新しい画面からデータを返したいかもしれない。
例えば、ユーザーに対する2つのオプションがある新しい画面をプッシュするとします。
ユーザーがオプションをタップしたとき、ユーザーの選択を1つ目の画面に知らせて、その情報に基づいてアクションできるようにする必要があります。

以下の手順で、Navigator.pop()メソッドを使うことでこれを行うことができます。

  1. Define the home screen
  2. Add a button that launches the selection screen
  3. Show the selection screen with two buttons
  4. When a button is tapped, close the selection screen
  5. Show a snackbar on the home screen with the selection

1. Define the home screen

ホーム画面はボタンを表示します。タップされると、選択画面を起動します。

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Returning Data Demo'),
      ),
      // 次のステップでSelectionButton widgetを作成する
      body: Center(child: SelectionButton()),
    );
  }
}

2. Add a button that launches the selection screen

次に、次のようなSelectionButtonを作成します。

  • タップされたときにSelectionScreenが起動します。
  • SelectionScreenが結果を返すのを待ちます。
class SelectionButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RaisedButton(
      onPressed: () {
        _navigateAndDisplaySelection(context);
      },
      child: Text('Pick an option, any option!'),
    );
  }

  // SelectionScreenを起動し、Navigator.popからの結果を待つメソッド
  _navigateAndDisplaySelection(BuildContext context) async {
    // Navigator.pushは、SelectionScreenでNavigator.popが呼ばれたあとに
    // 完了するFutureを返す。
    final result = await Navigator.push(
      context,
      // Create the SelectionScreen in the next step.
      MaterialPageRoute(builder: (context) => SelectionScreen()),
    );
  }
}

3. Show the selection screen with two buttons

次に、2つのボタンを含む選択画面を作成する。
ユーザーがボタンをタップしたとき、アプリは選択画面を閉じ、ホーム画面にどのボタンがタップされたかを知らせる。

このステップはUIを定義する。次のステップで、データを返すコードを追加する。

class SelectionScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Pick an option'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: RaisedButton(
                onPressed: () {
                  // Pop here with "Yep"...
                },
                child: Text('Yep!'),
              ),
            ),
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: RaisedButton(
                onPressed: () {
                  // Pop here with "Nope"
                },
                child: Text('Nope.'),
              ),
            )
          ],
        ),
      ),
    );
  }
}

4. When a button is tapped, close the selection screen

次に、両方のボタンのonPressed()コールバックを更新する。
最初の画面にデータを返すためには、resultと呼ばれる2番目の任意の引数を受け付けるNavigator.pop()メソッドを使います。
結果はSelectionButtonでFutureに返されます。

Yep_button
RaisedButton(
  onPressed: () {
    // The Yep button returns "Yep!" as the result.
    Navigator.pop(context, 'Yep!');
  },
  child: Text('Yep!'),
);
Nope_button
RaisedButton(
  onPressed: () {
    // The Nope button returns "Nope!" as the result.
    Navigator.pop(context, 'Nope!');
  },
  child: Text('Nope!'),
);

5. Show a snackbar on the home screen with the selection

選択画面を起動し結果を待機しているので、返却された情報で何かを実行する必要があります。

この場合、SelectionButtonの中で_navigateAndDisplaySelection()メソッドを使って、結果を表示するスナックバーを表示します。

_navigateAndDisplaySelection(BuildContext context) async {
  final result = await Navigator.push(
    context,
    MaterialPageRoute(builder: (context) => SelectionScreen()),
  );

  // 選択画面が結果を返したあと、前のスナックバーを隠し、新しい結果を表示する
  Scaffold.of(context)
    ..removeCurrentSnackBar()
    ..showSnackBar(SnackBar(content: Text("$result")));
}

補足

上記のコードの最終行で登場する..はCascade表記と呼ばれるDart固有の書き方?である。
Language tour | Dart
Cascadeを使うと、同じオブジェクトにたいして一連の操作を行うことができる。
関数呼び出しだけでなく、オブジェクトのフィールドにもアクセスすることができる。
以下のCascadeあり・なしのコードを見るとわかるが、一時的な変数を用意する必要がなくなるので、コードをきれいにすることができる。

通常の書き方
var button = querySelector('#confirm');
button.text = 'Confirm';
button.classes.add('important');
button.onClick.listen((e) => window.alert('Confirmed!'));
Cascade表記
querySelector('#confirm') // Get an object.
  ..text = 'Confirm' // Use its members.
  ..classes.add('important')
  ..onClick.listen((e) => window.alert('Confirmed!'));

Send data to a new screen

多くの場合、新しい画面に移動するだけでなく、画面いデータも渡したいでしょう。
例えば、タップされたアイテムの情報を渡したいかもしれません。

画面は単なるWidgetであることを覚えてください。
この例では。Todoリストを作ります。
Todoがタップされると、そのTodoに関する情報が表示される新しい画面(Widget)に遷移します。
このレシピは以下の手順で行われます:

  1. Define a todo class.
  2. Display a list of todos.
  3. Create a detail screen that can display information about a todo.
  4. Navigate and pass data to the detail screen.

1. Define a todo class

まず、Todoを表すシンプルな方法が必要です。
この例では、titleとdescriptionの2つのデータを含むクラスを作成します。

class Todo {
  final String title;
  final String description;

  Todo(this.title, this.description);
}

2. Create a list of todos

次に、Todoのリストを表示します。
この例では、20個のTodoを作成し、ListViewを使って表示します。
リストの操作に関してはUse lists recipeを参照してください。

Generate the list of todos

final todos = List<Todo>.generate(
  20,
  (i) => Todo(
    'Todo $i',
    'A description of what needs to be done for Todo $i',
  ),
);

Display the list of todos using a ListView

ListView.builder(
  itemCount: todos.length,
  itemBuilder: (context, index) {
    return ListTile(
      title: Text(todos[index].title),
    );
  },
);

ここまでは順調です。これで、20個のTodoが作成され、ListViewに表示されます。

3. Create a detail screen to display information about a todo

次に、2つ目の画面を作成します。
画面のタイトルはTodoのタイトルを含み、画面のbodyには説明が表示されます。

詳細画面は通常のStatelessWidgetであるため、ユーザーにUIでTodoの入力を要求します。
そして、与えられたTodoを使ってUIを構築します。

class DetailScreen extends StatelessWidget {
  // Todoを保持するフィールドを宣言する
  final Todo todo;

  // コンストラクタでは、Todoを必須とする。
  // In the constructor, require a Todo.
  DetailScreen({Key key, @required this.todo}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // Todoを使ってUIを作成する
    return Scaffold(
      appBar: AppBar(
        title: Text(todo.title),
      ),
      body: Padding(
        padding: EdgeInsets.all(16.0),
        child: Text(todo.description),
      ),
    );
  }
}

4. Navigate and pass data to the detail screen

DetailScreenを配置したら、ナビゲーションを行う準備ができました。
この例では、ユーザーがリストのTodoをタップするとDetailScreenに遷移します。
DetailScreenにはTodoを渡します。

ユーザーのタップをキャプチャするには、ListTilewidgetでonTap()コールバックを書きます。
onTap()コールバックの中で、Navigator.push()関数を使います。

ListView.builder(
  itemCount: todos.length,
  itemBuilder: (context, index) {
    return ListTile(
      title: Text(todos[index].title),
      // ユーザーがタップしたら、DetailScreenに遷移する
      // DetailScreenを作成しているだけでなく、
      // 現在のTodoを渡していることに注意してください
      onTap: () {
        Navigator.push(
          context,
          MaterialPageRoute(
            builder: (context) => DetailScreen(todo: todos[index]),
          ),
        );
      },
    );
  },
);

Interactive example

See the Pen flutter-navigate-animate-4 by popy1017 (@popy1017) on CodePen.

8
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?