Flutter路由使用指北

路由管理

FLutter中的路由,和原生组件化的路由一样,就是页面之间的跳转,也可以称之为导航。app维护一个路由栈,路由入栈(push)操作对应打开一个新页面,路由出栈(pop)操作对应页面关闭操作,而路由管理主要是指如何来管理路由栈。

MaterialPageRoute

MaterialPageRoute是一种模态路由,可以针对不同平台自适应的过渡动画替换整个屏幕页面:

对于Android,打开新页面时,新页面从屏幕底部导入到顶部。关闭页面的时候,会从顶部滑动到底部消失。

在iOS上,页面从右侧滑入并反向退出。

下面我们介绍一下MaterialPageRoute 构造函数的各个参数的意义:

  MaterialPageRoute({
    WidgetBuilder builder,
    RouteSettings settings,
    bool maintainState = true,
    bool fullscreenDialog = false,
  })

builder 是一个WidgetBuilder类型的回调函数,它的作用是构建路由页面的具体内容,返回值是一个widget。我们通常要实现此回调,返回新路由的实例。
settings 包含路由的配置信息,如路由名称、是否初始路由(首页)。
maintainState:默认情况下,当入栈一个新路由时,原来的路由仍然会被保存在内存中,如果想在路由没用的时候释放其所占用的所有资源,可以设置maintainState为false。
fullscreenDialog表示新的路由页面是否是一个全屏的模态对话框,在iOS中,如果fullscreenDialog为true,新页面将会从屏幕底部滑入(而不是水平方向)

基本使用

Flutter为我们提供了导航器Navigator。参数传入当前的BuildContext和要导航的页面即可。

  1. 调用Navigator.push导航到第二个页面
     Navigator.push(
               context, new MaterialPageRoute(builder:       (context) => Page2()));
    
  1. 调用Navigator.pop返回前一个页面
         Navigator.pop(context, result);
    
    
  1. 关闭页面后获取结果
    有时候我们需要上个页面关闭时传递一个返回值,幸运的是,Navigator的调用方法都是Future,因此我们可以等待它们的结果:

    3.1. 等待Navigator运行
    3.2. 将返回值传递给Navigator.pop函数
    3.3. 等待完成后,获取返回值

    在page1中,导航到page2,并且await到page2传递返回值并pop,根据返回值弹出不同的对话框:

    onPressed: () async {
          var navigationResult = await Navigator.push(
              context, new MaterialPageRoute(builder: (context) => Page2()));
    
          if (navigationResult == 'from_back') {
            showDialog(
                context: context,
                builder: (context) => AlertDialog(
                      title: Text('Navigation from back'),
                    ));
          } else if (navigationResult == 'from_button') {
            showDialog(
                context: context,
                builder: (context) => AlertDialog(
                      title: Text('Navigation from button'),
                    ));
          }
        },
    

    在page2中传递返回值并返回:

    Navigator.pop(context, 'from_button');
    
  2. 拦截返回键
    如果不想点击返回键关闭当前页面,可以使用WillPopScope小部件,用它放在最外层包括住脚手架。并向onWillPop返回false。false告诉系统当前页面不处理返回。

       @override
       Widget build(BuildContext context) {
         return WillPopScope(
           onWillPop: () => Future.value(false),
           child: Scaffold(
             body: Container(
               child: Center(
                 child: Text('Page 2',
                     style: TextStyle(fontSize: 30.0, fontWeight: FontWeight.bold)),
               ),
             ),
           ),
         );
       }
如果想自定义处理返回键,可以在return false 之前自己处理,比如关闭    当前页面并传递返回值:
      WillPopScope(
            onWillPop: () async {
                // You can await in the calling widget for my_value and handle when complete.
                Navigator.pop(context, 'my_value');
                return false;
              },
              ...
      );

命名路由

基本使用

上面代码是在没个需要导航的地方声明路由,不能复用,我们可以先给路由起一个名字,再注册路由表,然后就可以通过路由名字直接打开新的路由了,这为路由管理带来了一种直观、简单的方式,并且可以复用。

MaterialApp的routes属性,既是注册路由表用的,它对应一个Map<String, WidgetBuilder>。

起名:

  static const String page1 = "/page1";
  static const String page2 = "/page2";

声明路由表:

  Map<String, WidgetBuilder> routes = {
    page1: (context) => Page1(),
    page2: (context) => Page2(),
  };

注册路由表:

 @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      routes: routes,
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: Page1(),
    );
  }

然后在需要路由的地方使用命名路由调用:

Navigator.pushNamed(context, page2)

传递参数

给page3起名:

    page3: (context) => Page3(),

打开路由时候传递参数:

              Navigator.of(context).pushNamed(page3, arguments: "hi");

page3中接收参数:

class Page3 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //获取路由参数
    var args = ModalRoute.of(context).settings.arguments;
    return Scaffold(
      body: Container(
        child: Center(
          child: Text('Page 3的参数是$args',
              style: TextStyle(fontSize: 30.0, fontWeight: FontWeight.bold)),
        ),
      ),
    );
  }
}

构造函数传参

上面我们明明给page3传递了参数,但是并非传递到构造函数上。我们看构造函数,并不知道传递了什么参数,必须去看路由,并不是很好的做法。那怎么给构造函数传参呢?

起名:

const String page4 = "/page4";

注册路由:

    page4: (context) => Page4(text: ModalRoute.of(context).settings.arguments),

打开路由时传递参数:

              Navigator.of(context).pushNamed(page4, arguments: "hello");

动态路由

MaterialApp还为我们提供了一个onGenerateRoute参数,未在路由表里注册的路由,会在这里寻找。RouteFactory有一个RouteSettings参数,并返回一个Route<dynamic>。这是我们将用来执行所有路由的功能。

Route<dynamic> Function(RouteSettings settings)

我们可以这样使用:
先声明路由表:

Route<dynamic> generateRoute(RouteSettings settings) {
    switch (settings.name) {
        case page5:
            return MaterialPageRoute(builder: (context) => Page5());
        case page6:
            return MaterialPageRoute(builder: (context) => Page6());
        default:
            return MaterialPageRoute(builder: (context) => Page1());
    }

注册:

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      routes: routes,
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      onGenerateRoute: generateRoute,
      home: Page1(),
    );
  }

打开路由:

              Navigator.of(context).pushNamed(page5);

动态路由传递参数

上面说了,settings可以拿到参数,我们当然就可以传递参数了:

Route<dynamic> generateRoute(RouteSettings settings) {
    print('====${settings.name}');
    switch (settings.name) {
        case page5:
            return MaterialPageRoute(builder: (context) => Page5());
        case page6:
            return MaterialPageRoute(builder: (context) => Page6(text: settings.arguments,));
        default:
            return MaterialPageRoute(builder: (context) => Page1());
    }
}

使用:

              Navigator.of(context).pushNamed(page6, arguments: "world");

so easy。

处理未定义的路线

有两种处理未定义路由的方法。

  1. 利用generateRoute,找不到路由名的返回默认路由
Route<dynamic> generateRoute(RouteSettings settings) {
    print('====${settings.name}');
    switch (settings.name) {
        case page5:
            return MaterialPageRoute(builder: (context) => Page5());
        case page6:
            return MaterialPageRoute(builder: (context) => Page6(text: settings.arguments,));
        default:
            return MaterialPageRoute(builder: (context) => NotFindPage());
    }
}
  1. 利用onUnknownRoute返回默认路由
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Demo',
        routes: routes,
        theme: ThemeData(
          primarySwatch: Colors.blue,
          visualDensity: VisualDensity.adaptivePlatformDensity,
        ),
        onGenerateRoute: generateRoute,
        onUnknownRoute: (settings) =>
            MaterialPageRoute(builder: (context) => NotFindPage()));
  }

初始路由

打开应用第一屏的路由,也有2种方式,

  1. 可以设置initialRoute,指定路由表里注册的路由名。
  2. 可以设置home,对应的page。
    initialRoute会覆盖home。
 Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Demo',
        routes: routes,
        theme: ThemeData(
          primarySwatch: Colors.blue,
          visualDensity: VisualDensity.adaptivePlatformDensity,
        ),
        initialRoute: root,
        home: Page2(),
        onGenerateRoute: generateRoute,
        onUnknownRoute: (settings) =>
            MaterialPageRoute(builder: (context) => NotFindPage()));
  }

不使用BuildContext的路由导航

很多情况是,我们已将UI代码从业务逻辑中分离出来(类似于MVVM架构)。viewModel应处理所有逻辑,视图应仅调用模型上的函数,然后在需要时使用新状态重建自身。

我们知道Navigator需要BuildContext的参数,我们在进行实际业务逻辑决策的位置进行导航,而不是在widget里调用路由,如果在viewModel里导航,就要传入context吗?下面实现不要context的导航。

为了遵守MVVM原则,我们将把Navigation功能移动到可以从viewModel调用的服务中。在lib下创建一个名为services的新文件夹,并在其中创建一个名为navigation_service.dart的新文件。

先实现单利模式:

class NavigationService {
  factory NavigationService.getInstance() => _getInstance();

  NavigationService._internal();

  static NavigationService _instance;

  static NavigationService _getInstance() {
    if (_instance == null) {
      _instance = new NavigationService._internal();
    }
    return _instance;
  }
  }
然后利用navigatorKey实现:
 final GlobalKey<NavigatorState> navigatorKey =
      new GlobalKey<NavigatorState>();

  Future<dynamic> navigateTo(String routeName) {
    return navigatorKey.currentState.pushNamed(routeName);
  }

  void goBack() {
    return navigatorKey.currentState.pop();
  }

我们将NavigationService与应用程序链接的方式,通过navigatorKey提供给MaterialApp。转到main.dart文件并设置navigatorKey:

 MaterialApp(
        title: 'Flutter Demo',
        navigatorKey: NavigationService().navigatorKey,
        ...
        )

然后写一个viewModel,尝试导航:

class ViewModel {
  final NavigationService _navigationService = NavigationService();

  Future goPage1() async{
    /// 模拟请求数据后调到首页
      await Future.delayed(Duration(seconds: 1));
      _navigationService.navigateTo(page1);
  }

}

在page6里使用viewModel导航:

  onPressed: () {
          viewModel.goPage1();
        },

现在,将View文件的职责带回到了“显示UI”并将用户操作传递给模型,而不是“显示UI”将用户操作传递给模型并进行导航。更符合MVVM职责的划分。

这样做的好处是,随着导航逻辑的扩展,我们的UI将保持不变,并且模型将承载所有逻辑/状态管理。

代码链接

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,287评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,346评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,277评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,132评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,147评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,106评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,019评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,862评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,301评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,521评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,682评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,405评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,996评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,651评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,803评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,674评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,563评论 2 352