Flutter-导航与路由堆栈详解

一个App是由很多page(页面)组成的,我们需要点击跳转到不同的页面,而不是只是单纯通过BottomNavigationBarItem点击设置IndexedStack的属性进行切换页面。通常我们会通过路由来统一的管理跳转。

什么是路由

存在一个路由映射表,通过唯一的标识找到要跳转的页面。在Flutter中,路由管理主要有两个类:RouteNavigator

Route

一个页面要想被路由统一管理,必须包装为一个 Route。MaterialPageRoutePageRoute的子类,表示一个模态路由页面,还定义了路由构建及切换时过渡动画的相关接口及属性。MaterialPageRoute 是 Material 组件库提供的组件,它可以针对不同平台,实现与平台页面切换动画风格一致的路由切换动画。

  • 在 iOS 平台,打开一个页面会从右边滑动到屏幕左边,返回时会从左边到右边消失
  • 在 Android 平台,打开一个页面会从屏幕底部滑动到屏幕的顶部,关闭页面时从顶部滑动到底部消失

继承关系是这样的

MaterialPageRoute->PageRoute->ModalRoute->TransitionRoute->OverlayRoute->Route
复制代码

Navigator

Navigator:管理所有的 Route 的 Widget,通过栈来进行管理的,通常当前屏幕显示的页面就是栈顶的路由。

组件路由

路由跳转,传入一个路由对象route。该方法是把新的路由添加到Navigator管理的路由对象的栈顶

Navigator.of(context).push(route)
复制代码

返回上个页面,这里返回时可以带参数的[ T? result ],退出则是从栈顶把路由对象移出

Navigator.of(context).pop()
复制代码

示例代码

child: ElevatedButton(
  onPressed: (){
    Navigator.of(context).push(MaterialPageRoute(builder: (BuildContext context){
      return PageDetail();
    }));
  },
  child: Text('进入详情页面'),
),
复制代码
child: ElevatedButton(
  onPressed: (){
    Navigator.of(context).pop();
  },
  child: Text('返回'),
),
复制代码

导航返回拦截

为了避免用户误触返回按钮而导致 App 退出,在很多 App 中都拦截了用户点击返回键的按钮,然后进行一些防误触判断,比如当用户在某一个时间段内点击两次时,才会认为用户是要退出(而非误触)。Flutter 中可以通过WillPopScope来实现返回按钮拦截,我们看看 WillPopScope 的默认构造函数:

const WillPopScope({
  ...
  required WillPopCallback onWillPop,
  required Widget child
})
复制代码

onWillPop 是一个回调函数,当用户点击返回按钮时被调用(包括导航返回按钮及 Android 物理返回按钮)。该回调需要返回一个Future对象,如果返回的 Future 最终值为false时,则当前路由不出栈(不会返回);最终值为true时,当前路由出栈退出。我们需要提供这个回调来决定是否退出。

@override
Widget build(BuildContext context) {
  WillPopScope(
      onWillPop: onWillPop,
      child: Scaffold(
        
      )
  )

  onWillPop:() {
    print('即将退出本界面');
    //return Future.value(false);//阻止退出
    return Future.value(true); //退出
  }
}
复制代码

命名路由

基本路由使用还是比较简单的,但是对于页面比较多的场景就不太适应用了,每次跳转新页面就要创建新路由,这样就是比较混乱了。而命名路由是给每个页面起了一个别名,通过这个别名就可以找到打开这个页面,这样管理起来就比较清晰方便了。

要想通过别名来实现页面切换,就需要用到 MaterialApp 提供的一个页面名称映射规则,也就是路由表。这个路由表是一个Map结构,key是页面的别名,value就是对应页面

Navigator 使用的4个关键属性

  • initialRoute: 初始路由的,也就是进入APP,默认页面
  • onGenerateRoute: 路由拦截器,当需要对某个路由做特殊处理时可以使用这个
  • onUnknownRoute: 找不到页面,默认创建一个错误页面
  • routes:也就在执行路由跳转的时候,会到路由集合里面的子路由进行匹配,如果匹配 到那么就调整到指定页面

示例代码

return MaterialApp(
  //路由表
  routes: {
    '/': (context) => HomePage(),
    'basic': (context) => BasicPage(),
    'form_main': (context) => FormMainPage(),
    'nav': (context) => NavigationMainPage(),
  },
  //初始路由页面
  initialRoute: 'hoemPage',
  //找不到路由的页面
  onUnknownRoute: (RouteSettings routerName) {
  print("未匹配到路由:$routerName.name");
  return new MaterialPageRoute(builder: (context) {
    return ErrorPage();
  });
}
  }
复制代码

路由钩子

MaterialApp有一个onGenerateRoute属性,当调用Navigator.pushNamed(…)打开命名路由时,如果指定的路由名在路由表中已注册,则会调用路由表中的builder函数来生成路由组件;如果路由表中没有注册,才会调用onGenerateRoute来生成路由。然后可以在路由中通过参数RouteSettings来判断是否需要特殊处理,比如可以把需要权限判断跳转的路由放到这里打开。

onGenerateRoute: (settings) {
  if (settings.name == "login") {
    return MaterialPageRoute(
        builder: (context) {
          return LoginPage(settings.arguments);
        }
    );
  }
  return null;
},
复制代码

Navigator 如何传参和回传值

  1. 注册好之后就可以通过 Navigator.pushNamed() 打开页面了
Navigator.pushNamed(context, "basic");
//带参数的跳转
Navigator.pushNamed(context, "basic", arguments: '要传的参数');
复制代码
  1. 获取传递的参数
Widget build(BuildContext context) {
  // 1.获取数据
  final args= ModalRoute.of(context)!.settings.arguments;
}
复制代码
  1. 返回页面是通过 Navigator.pop
Navigator.pop(context);
Navigator.pop(context, "返回值");
复制代码
  1. 接收返回的数据,需要修改下跳转方法
Navigator.pushNamed(context, 'basic').then((value) => print('接收到的数据${value}'));
复制代码
  1. 返回到指定的页面是通过 Navigator.popUntil
Navigator.popUntil(context, ModalRoute.withName('路由'));
复制代码

在平常使用过程中,是可以把RoutesonGenerateRoute单独提取出来成为一个类,这样后期维护就比较清晰和修改方便。

命名路由的最重要作用,就是建立了字符串标识符与各个页面之间的映射关系,使得各个页面之间完全解耦,应用内页面的切换只需要通过一个字符串标识符就可以搞定,为后期模块化打好基础。

Navigator的各种跳转方式详解

  • pushAndRemoveUntil:跳转到新的页面,并且删除新页面和底部页面之间所有页面
  • pushReplacement:新的页面替换当前页面,只需要创建新的页面,当前页面销毁
  • pushReplacementNamed:新的页面替换当前页面,只是路由的传递,命名路由方式,当前页面销毁
  • popUntil:返回到指定页面,其他页面销毁
  • popAndPushNamed:退出当前页面并从弹出新的页面
  • canPop:判断当前页面能否被弹出栈,栈内只有一个页面时为false,别的时候为true
  • maybePop:判断依据就是看当前路由是否处在栈中“最底部”的位置,如果不是就退出

pushReplacementNamed和popAndPushNamed

有 A、B、C 三个页面,A页面通过 pushNamed 跳转到 B,B 通过 pushReplacementNamed 跳转到 C,点击 C 页面按钮执行 pop。点击 C 页面按钮直接返回到了 A 页面,而不是 B 页面,因为 B 页面使用 pushReplacementNamed 跳转,路由堆栈变化:

截屏2021-12-09 下午10.45.24.png

如果 B 页面跳转到 C 页面,使用 popAndPushNamed,popAndPushNamed 路由堆栈和 pushReplacementNamed 是一样,唯一的区别就是 popAndPushNamed 有 B 页面退出动画。

pushNamedAndRemoveUntil

有如下场景,应用程序进入首页,点击登录进入登录页面,然后进入注册页面或者忘记密码页面...,登录成功后进入其他页面,此时不希望返回到登录相关页面,此场景可以使用 pushNamedAndRemoveUntil。

有A、B、C、D 四个页面,A 通过push进入 B 页面,B 通过push进入 C 页面,C 通过 pushNamedAndRemoveUntil 进入 D 页面同时删除路由堆栈中直到 /B 的路由,C 页面代码:

RaisedButton(
  child: Text('C 页面'),
  onPressed: () {
    Navigator.of(context).pushNamedAndRemoveUntil('/D', ModalRoute.withName('/B'));
  },
),
复制代码

D 页面按钮执行 pop,从 C 到 D 堆栈的变化

截屏2021-12-09 下午10.49.15.png

Navigator.of(context).pushNamedAndRemoveUntil('/D', ModalRoute.withName('/B'));
复制代码

表示跳转到 D 页面,同时删除D 到 B 直接所有的路由,如果删除所有路由,只保存 D

Navigator.of(context).pushNamedAndRemoveUntil('/D', (Route route)=>false);
复制代码

路由堆栈变化:

截屏2021-12-09 下午10.51.20.png

popUntil

有A、B、C、D 四个页面,D 页面通过 popUntil 一直返回到 A 页面,D 页面代码:

RaisedButton(
  child: Text('D 页面'),
  onPressed: () {
    Navigator.of(context).popUntil(ModalRoute.withName('/A'));
  },
)
复制代码

maybePop 和 canPop

在 A 页面时路由堆栈中只有 A,调用 pop 后,路由堆栈变化

截屏2021-12-09 下午10.53.54.png

此时路由堆栈为空,没有可显示的页面,应用程序将会退出或者黑屏,好的用户体验不应如此,此时可以使用 maybePop,maybePop 只在路由堆栈有可弹出路由时才会弹出路由

上面的案例在 A 页面执行maybePop:

RaisedButton(
  child: Text('A 页面'),
  onPressed: () {
    Navigator.of(context).maybePop();
  },
)
复制代码

点击后不会出现弹出路由,因为当前路由堆栈中只有 A,在 B页面执行maybePop,将会返回到 A 页面。

也可以通过 canPop 判断当前是否可以 pop:

RaisedButton(
  child: Text('B 页面'),
  onPressed: () {
    if(Navigator.of(context).canPop()){
      Navigator.of(context).pop();
    }
  },
)
复制代码

三方插件 fluro

fluro 作为一款优秀的 Flutter 企业级路由框架,fluro的使用比官方提供的路由框架要复杂一些,但是却非常适合中大型项目。因为它具有层次分明、条理化、方便扩展和便于整体管理路由等优点。 fluro 的github链接:github.com/lukepighett…

参考文章:这篇文章对于路由堆栈讲解的很详细:juejin.cn/post/687286…

监听路由堆栈的变化:juejin.cn/post/687322…

Guess you like

Origin juejin.im/post/7039498259965935630