Flutter—— 路由(Route)和导航(Navigator),实现页面管理

Flutter的页面,怎么进行跳转的呢?通过路由和导航呢。

一、路由和导航,初认识

言简意赅!

  • 路由(Route) : route是一个屏幕或页面的抽象(可以大概理解为安卓的Activity)
  • 导航(Navigator) : Navigator是管理route的Widget。导航器管理着路由对象的堆栈并提供管理堆栈的方法,如 Navigator.push入栈 和 Navigator.pop出栈

Navigator可以通过route入栈和出栈来实现页面之间的跳转。


一1、最简单的页面跳转

一个页面,在Flutter里面,被理解为一个路由。
多个路由,可以存在与同一个dart文件中的。

  • 使用Navigator.pushNamed方法首先需要在 MaterialApp 中定义routes。
import 'package:flutter/material.dart';

void main() {
  runApp(new MaterialApp(title:'导航页面示例', home: new Demo()));
}

class Demo extends StatelessWidget{
  @override
  Widget build(BuildContext context){
    return new Scaffold(
      appBar: new AppBar(
        title: Text('导航页面示例'),
      ),
      body: new Center(
          child:RaisedButton(
            child: Text('查看详情页面'),
            onPressed: (){
              Navigator.push(context, MaterialPageRoute(builder: (context)=>new SecondScreen()));
            },
          )
      ),
    );
  }
}

class SecondScreen extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('导航页面第二屏'),
      ),
      body: new Center(
        child: new RaisedButton(
          onPressed: (){
            Navigator.pop(context);
          },
          child: new Text('返回页面'),
        ),
      ),
    );
  }
}

.
.

从上面的例子,我们看到进入新页面,用push(入栈),返回上个页面,用pop(出栈)。

MaterialPageRoute

  • MaterialPageRoute 是Material组件库的一个Widget,它可以针对不同平台,实现与平台页面切换动画风格一致的路由切换动画

    • 对于Android,当打开新页面时,新的页面会从屏幕底部滑动到屏幕顶部;当关闭页面时,当前页面会从屏幕顶部滑动到屏幕底部后消失,同时上一个页面会显示到屏幕上。

    • 对于iOS,当打开页面时,新的页面会从屏幕右侧边缘一致滑动到屏幕左边,直到新页面全部显示到屏幕上,而上一个页面则会从当前屏幕滑动到屏幕左侧而消失;当关闭页面时,正好相反,当前页面会从屏幕右侧滑出,同时上一个页面会从屏幕左侧滑入。

  • 默认情况下,当一个模态路由被另一个替换时,上一个路由将保留在内存中,如果想释放所有资源,可以将 maintainState 设置为 false。

MaterialPageRoute的构造函数

 MaterialPageRoute({
    WidgetBuilder builder,
    RouteSettings settings,
    bool maintainState = true,
    bool fullscreenDialog = false,
  })
  • builder 是一个WidgetBuilder类型的回调函数,它的作用是构建路由页面的具体内容,返回值是一个widget。我们通常要实现此回调,返回新路由的实例。

  • settings 包含路由的配置信息,如路由名称、是否初始路由(首页)。

  • maintainState:默认情况下,当入栈一个新路由时,原来的路由仍然会被保存在内存中,如果想在路由没用的时候释放其所占用的所有资源,可以设置maintainState为false。

  • fullscreenDialog表示新的路由页面是否是一个全屏的模态对话框,在iOS中,如果fullscreenDialog为true,新页面将会从屏幕底部滑入(而不是水平方向)。

.
.
示例:

1.gif

一.2、跳转页面时传递数据

  • 使用Navigator.push接收参数的重点在构造函数,通过在构造函数中接受参数进行传递

传递数据,其实也很简单。P1跳转到P2,P2的构造函数预留好参数,P1跳转到P2的时候,传递一下即可。


import 'package:flutter/material.dart';

class Product {
  final String title;
  final String description;
  Product(this.title,this.description);
} //Product 类 属性

void main(){
  runApp(new MaterialApp(
      title:'传递数据示例',
      home:new ProductList(
          products:new List.generate(20, (i)=>new Product('商品 $i', '这是一个商品的详情 $i')) //父子传值
      )
  ));
}


class ProductList extends StatelessWidget{
  final List<Product> products;
  ProductList({Key key,@required this.products}):super(key:key);
  @override
  Widget build(BuildContext context){
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('商品列表'),
      ),
      body: new ListView.builder(
          itemCount:products.length,
          itemBuilder:(context,index){
            return new ListTile(
              title:new Text(products[index].title),
              onTap: (){
                Navigator.push(
                    context,
                    // 传递数据
                    new MaterialPageRoute(
                        builder: (context)=>new ProductDetail(product:products[index])
                    )
                );
              },
            );
          }
      ),
    );
  }
}

class ProductDetail extends StatelessWidget {
  final Product product;
  ProductDetail({Key key,@required this.product}):super(key:key);
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('${product.title}'),
      ),
      body: new Padding(
        padding: new EdgeInsets.all(16.0),
        child: new Text('${product.description}'),
      ),
    );
  }
}

.
.
效果:

2.gif

一3、页面返回数据

import 'package:flutter/material.dart';

void main() {
  runApp(new MaterialApp(title:'导航页面示例', home: new ArticleListScreen()));
}

class Article {
  String title;
  String content;

  Article({this.title, this.content});
}
class ArticleListScreen extends StatelessWidget {
  final List<Article> articles = new List.generate(
    10,
        (i) => new Article(
      title: 'Article $i',
      content: '文章 $i: 你喜欢这个文章吗亲.',
    ),
  );

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('Article List'),
      ),
      body: new ListView.builder(
        itemCount: articles.length,
        itemBuilder: (context, index) {
          // 当用户点击列表中的文章时将跳转到ContentScreen,并将 article 传递给 ContentScreen。
          //为了实现这一点,我们将实现 ListTile 的 onTap 回调。 在的 onTap 回调中,再次调用Navigator.push方法。

          return new ListTile(
              title: new Text(articles[index].title),
              // 列表项的 onTap 回调,处理内容页面返回的数据并显示。
              /*onTap: () {
                Navigator.push(
                  context,
                  new MaterialPageRoute(
                    builder: (context) => new ContentScreen(articles[index]),
                  ),
                );
              },*/

              onTap: () async {
                // 接受页面返回值
                String result = await Navigator.push(
                  context,
                  new MaterialPageRoute(
                    // 文章页面跳转到内容页面,传递个值
                    builder: (context) => new ContentScreen(articles[index]),
                  ),
                );

                if (result != null) {
                  Scaffold.of(context).showSnackBar(
                    new SnackBar(
                      // 显示一下从别人页面传递过来的值,如果有值的话
                      content: new Text("$result"),
                      duration: const Duration(seconds: 1),
                    ),
                  );
                }
              },
          );
        },
      ),
    );
  }
}



class ContentScreen extends StatelessWidget {
  final Article article;

  ContentScreen(this.article);

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('${article.title}'),
      ),
      body: new Padding(
          padding: new EdgeInsets.all(15.0),
          child: new Column(
            children: <Widget>[
              new Text('${article.content}'),
              new Row(
                mainAxisAlignment: MainAxisAlignment.spaceAround,
                children: <Widget>[
                  new RaisedButton(
                    onPressed: () {
                      // pop 第二个参数,就是表示给上个页面传递
                      Navigator.pop(context, 'Like');
                    },
                    child: new Text('Like'),
                  ),
                  new RaisedButton(
                    onPressed: () {
                      // pop 第二个参数,就是表示给上个页面传递
                      Navigator.pop(context, 'Unlike');
                    },
                    child: new Text('Unlike'),
                  ),
                ],
              )
            ],
          ),

      ),
    );
  }
}

  • 返回数据,主要就是pop的时候返回,然后想接受的,在push的时候,就接受下

.
.
效果

3.gif

二、定制路由

通常,我们可能需要定制路由以实现自定义的过渡效果等。定制路由有两种方式:

  • 继承路由子类,如:PopupRoute、ModalRoute 等。
  • 使用 PageRouteBuilder 类通过回调函数定义路由。
  • 下面使用 PageRouteBuilder 实现一个页面旋转淡出的效果。
onTap: () async {
  String result = await Navigator.push(
      context,
      new PageRouteBuilder(
        transitionDuration: const Duration(milliseconds: 1000),
        pageBuilder: (context, _, __) =>
            new ContentScreen(articles[index]),
        transitionsBuilder:
            (_, Animation<double> animation, __, Widget child) =>
                new FadeTransition(
                  opacity: animation,
                  child: new RotationTransition(
                    turns: new Tween<double>(begin: 0.0, end: 1.0)
                        .animate(animation),
                    child: child,
                  ),
                ),
      ));

  if (result != null) {
    Scaffold.of(context).showSnackBar(
      new SnackBar(
        content: new Text("$result"),
        duration: const Duration(seconds: 1),
      ),
    );
  }
},

三、命名路由

在Flutter最初的版本中,命名路由是不能传递参数的,后来才支持了参数.

当使用 initialRoute 时,需要确保你没有同时定义 home 属性。

通常,移动应用管理着大量的路由,并且最容易的是使用名称来引用它们。路由名称通常使用路径结构:“/a/b/c”,主页默认为 “/”

MaterialApp(
  // Start the app with the "/" named route. In this case, the app starts
  // on the FirstScreen widget.
  
  // 使用“/”命名路由来启动应用(Start the app with the "/" named route. In our case, the app will start)
  // 在这里,应用将从 FirstScreen Widget 启动(on the FirstScreen Widget)
  
  initialRoute: '/',
  routes: {
    // When navigating to the "/" route, build the FirstScreen widget.
    // 当我们跳转到“/”时,构建 FirstScreen Widget(When we navigate to the "/" route, build the FirstScreen Widget)
    '/': (context) => FirstScreen(),
    // When navigating to the "/second" route, build the SecondScreen widget.
    // 当我们跳转到“/second”时,构建 SecondScreen Widget(When we navigate to the "/second" route, build the SecondScreen Widget)
    '/second': (context) => SecondScreen(),
  },
);

创建 MaterialApp 时可以指定 routes 参数,该参数是一个映射路由名称和构造器的 Map。MaterialApp 使用此映射为导航器的 onGenerateRoute 回调参数提供路由。

注册路由

      routes: {
        // 注册路由
        "parameters_page":(context)=>ParametersRoute(),
      },

在路由页通过RouteSetting对象获取路由参数:

class ParametersRoute extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    //获取路由参数  
    var args = ModalRoute.of(context).settings.arguments
    //...省略无关代码
  }
}
 

在打开路由时传递参数

onPressed: () {
    //导航到一个新的路由页面
    //Navigator.pushNamed(context, "third_page");
    Navigator.of(context).pushNamed("parameters_page",arguments:"命名路由传递的参数");
},

指定给 parameters_page路由 传递参数 "命名路由传递的参数"

更加常见的,是弄个路由表

本部分和示例无关。

  • 提供一个路由表,这是一个Map,是字符串和WidgetBuilder的对应关系。比如:
/// 路由表
final Map<String, WidgetBuilder> routeTable = {
  '/' : (content) => Home(),
  '/page1' : (content) => Page1(),
  '/page2' : (content) => Page2(),
  '/page3' : (content) => Page3(),
};
  • 把这个路由表放在MaterialApp的routes参数中
void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      routes: routeTable,
    );
  }
}
  • 使用的例子如下,比如跳转到Page1页面:
onPressed: (){
    Navigator.of(context).pushNamed('/page1');
}

一个示例代码

import 'package:flutter/material.dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: '路由测试',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      routes: {
        // 注册路由
        "home": (context) => MyHomePage(),
        "parameters_page": (context) => ParametersRoute(),
      },
      home: new MyHomePage(title: '路由测试'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: new Center(
        child: new Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            FlatButton(
              child: Text(
                "携带参数打开新页面",
                style: TextStyle(fontWeight: FontWeight.bold),
              ),
              textColor: Colors.blue,
              onPressed: ()  {
                //导航到一个新的路由页面
//                Navigator.pushNamed(context, "third_page");
                 Navigator.of(context)
                    .pushNamed("parameters_page", arguments: "命名路由传递的参数");
              },
            )
          ],
        ),
      ),
    );
  }
}

class ParametersRoute extends StatelessWidget {
  final Topic = Text("路由测试");

  @override
  Widget build(BuildContext context) {
    // TODO: implement build

    var args = ModalRoute.of(context).settings.arguments;
    return Scaffold(
      appBar: AppBar(
        title: Text("路由测试"),
      ),
      body: new ListView(

        children: <Widget>[
          
          Center(
            child: Text("路由到此页面获取到的数据为:\n\n" + args),
          ),
        ],

      )
    );
  }
}

.
.

4.gif

四、onGenerateRoute 拦截器

假设我们要开发一个电商APP,当用户没有登录时可以看店铺、商品等信息.
但交易记录、购物车、用户个人信息等页面需要登录后才能看。为了实现上述功能,我们需要在打开每一个路由页前判断用户登录状态!如果每次打开路由前我们都需要去判断一下将会非常麻烦,那有什么更好的办法吗?答案是有! —— onGenerateRoute

  • onGenerateRoute 可以做拦截器
  • onGenerateRoute可以变相的接受参数
  • 可以在onGenerateRoute()函数中提取参数并将它们传递给widget,而不是直接在窗口小部件中提取参数。
  • 注意,onGenerateRoute只会对命名路由生效

.
.

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      // 提供处理命名路由的函数。使用此功能可识别要推送的命名路径,并创建正确的页面。
      onGenerateRoute: (settings) {
        // 如果您要 打开 PassArguments 路由
        if (settings.name == PassArgumentsScreen.routeName) {
          // 将参数转换为正确的类型:ScreenArguments。
          final ScreenArguments args = settings.arguments;

          // 从参数中提取所需数据并将数据传递到正确的屏幕。
          return MaterialPageRoute(
            builder: (context) {
              return PassArgumentsScreen(
                title: args.title,
                message: args.message,
              );
            },
          );
        }
      },
      title: 'Navigation with Arguments',
      home: HomeScreen(),
    );
  }
}

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Home Screen'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            // A button that navigates to a named route that. The named route
            // extracts the arguments by itself.
            RaisedButton(
              child: Text("带参数 push 方式"),
              onPressed: () {
                // When the user taps the button, navigate to the specific route
                // and provide the arguments as part of the RouteSettings.
                Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) => ExtractArgumentsScreen(),
                    // Pass the arguments as part of the RouteSettings. The
                    // ExtractArgumentScreen reads the arguments from these
                    // settings.
                    settings: RouteSettings(
                      arguments: ScreenArguments(
                        'tag1  来自HomeScreen的 push',
                        'tag1  这个消息将被  build 方法 提取',
                      ),
                    ),
                  ),
                );
              },
            ),
            // A button that navigates to a named route. For this route, extract
            // the arguments in the onGenerateRoute function and pass them
            // to the screen.
            RaisedButton(
              child: Text("带参 pushNamed onGenerateRoute 方式"),
              onPressed: () {
                // When the user taps the button, navigate to a named route
                // and provide the arguments as an optional parameter.
                Navigator.pushNamed(
                  context,
                  PassArgumentsScreen.routeName,
                  arguments: ScreenArguments(
                    'tag2  来自HomeScreen的 pushNamed',
                    'tag2  这个消息来自 将被 onGenerateRoute 函数提取',
                  ),
                );
              },
            ),
          ],
        ),
      ),
    );
  }
}

// 一个Widget,它从ModalRoute中提取必要的参数。
class ExtractArgumentsScreen extends StatelessWidget {
  static const routeName = '/extractArguments';

  @override
  Widget build(BuildContext context) {
    // 从当前ModalRoute设置中提取参数并将其转换为ScreenArguments。
    final ScreenArguments args = ModalRoute.of(context).settings.arguments;

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

// Widget,通过构造函数接受必要的参数。
class PassArgumentsScreen extends StatelessWidget {
  static const routeName = '/passArguments';

  final String title;
  final String message;

  // 此Widget接受参数作为构造函数参数。它不从ModalRoute中提取参数。
  //
  // 参数由提供给MaterialApp小部件的onGenerateRoute函数提取。
  const PassArgumentsScreen({
    Key key,
    @required this.title,
    @required this.message,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Text(message),
      ),
    );
  }
}

// 您可以将任何对象传递给arguments参数。在此示例中,创建一个包含可自定义标题和消息的类。
class ScreenArguments {
  final String title;
  final String message;

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

这是官方的例子,稍微改了点文字

5.gif

.
.
END

.
.
.

参数:
Flutter (十五) 路由及导航

6.2.初识Flutter应用之路由管理

Flutter 路由和导航

推荐阅读更多精彩内容