今是昨非

今是昨非

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

Flutter Layout Basics - Page Navigation and Return

Flutter Layout Basics - Page Navigation and Passing Values#

When it comes to navigation, the most common effect is similar to the push and pop effects of the navigation controller in iOS. Flutter also has similar effects, using the Navigator component.

Next, let's take a look at the usage of navigation effects Navigator.push and Navigator.pop in Flutter.

Simple Usage#

The code is as follows:


class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Navigation Demo 1',
      home: new FirstScreen(),
    );
  }
}

class FirstScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: AppBar(
        title: Text('Navigation Page'),
      ),
      body: Center(
          child: ElevatedButton(
        child: Text('View Product Details Page'),
        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('Return'),
          onPressed: () {
            Navigator.pop(context);
          },
        ),
      ),
    );
  }
}

Note that when using Navigator.push, the page to be navigated to is wrapped in MaterialPageRoute. The SecondScreen in the code adds a button, and the implementation method for clicking is Navigator.pop, which is used to return; however, usually, there is no need to specifically implement Navigator.pop, because in iOS, when using AppBar, a return button is automatically added to the top left corner; in Android, the system back button can also directly return.

Named Navigation#

Similar to route jumping, navigation can be done using names instead of class names.

The following code defines three pages, MyFirstPage is the home page, and MySecondPage and MyThirdPage are two different pages. There are two buttons on the home page, corresponding to jumping to MySecondPage and MyThirdPage, while passing parameters during the jump, which are displayed on the corresponding pages.

The code is as follows:


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('homepage'),
      ),
      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('Second Page'),
      ),
      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('Third Page'),
      ),
      body: Center(
        child: new OutlinedButton(
            onPressed: () {
              Navigator.pop(context);
            },
            child: new Text(this.title ?? 'Default PageName')),
      ),
    );
  }
}

There are a few points to note here:

First, the use of routes, which is of type <String, WidgetBuilder>, where the former is the name of the page to jump to and the latter is the corresponding page to jump to. Moreover, the name of the page to jump to does not have to start with /, so it can be defined freely, but according to routing understanding, it is still recommended to follow a unified naming convention.

Secondly, regarding the use of Navigator for navigation, in the previous direct jump example, the method used was Navigator.push; while here, it is Navigator.pushNamed.

Finally, it is important to note how to pass values between pages.

Passing Values Between Pages#

Just like in iOS development, passing values between pages can be divided into passing values from the upper-level page to the lower-level page and callback values from the lower-level page to the upper-level page.

Passing Values from Upper-Level Page to Lower-Level Page#

The above code passes values from the upper-level page to the lower-level page, but the implementations of MySecondPage and MyThirdPage are different, as shown below:

wecom20210730-172529.png

There are two differences: initial declaration and specific usage;

In MySecondPage, the declared title property is a non-nullable String, using the required modifier (note that it is required and not @required, as some articles have not been updated), and it is used directly.

In MyThirdPage, the declared title property is a nullable String, without using the required modifier, but when used, it adds ?? to provide a default value.

If confused, you may encounter the error 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), which is due to the difference between nullable and non-nullable types. The solution is either to declare it as a nullable type and add a check when using it, or to use the required modifier to declare it as non-nullable.

Passing Values from Lower-Level Page to Upper-Level Page#

The Navigator.Push method can have a return value, and the return value is of type Future. When calling the Navigator.Pop method, if the second optional parameter is passed in, it will be returned in Navigator.Push.

The code is as follows:


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('homepage'),
      ),
      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('Second Page'),
      ),
      body: Center(
        child: OutlinedButton(
          onPressed: () {
            Navigator.pop(context, 'SecondPage Callback');
          },
          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('Third Page'),
      ),
      body: Center(
        child: new OutlinedButton(
            onPressed: () {
              Navigator.pop(context, 'ThirdPage Callback');
            },
            child: new Text(this.title ?? 'Default PageName')),
      ),
    );
  }
}

The effect is as follows:

pagecallback.gif

Note the usage of the button click methods above, where a separate _pagePush method is encapsulated, and it is marked with async because the return value of Navigator.push is of type Future, which needs to be used with await, and await can only be used in methods marked with async. If you have written React Native, you should be familiar with this syntax.

References#

Navigator Dev Doc
Flutter Free Video Season 4 - Page Navigation and Others
The parameter can't have a value of 'null' because of its type in Dart
Returning data from a new page to the previous page
Future class

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.