likes
comments
collection
share

Flutter-导航与路由堆栈详解

作者站长头像
站长
· 阅读数 5

一个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:也就在执行路由跳转的时候,会到路由集合里面的子路由进行匹配,如果匹配 到那么就调整到指定页面

示例代码

封装一个 routes.dart

import 'package:flutter/material.dart';

import '../pages/tabs.dart';
import '../pages/search.dart';
import '../pages/product.dart';

//配置路由,定义Map类型的routes,Key为String类型,Value为Function类型
final Map<String,Function> routes={
      '/':(context)=>Tabs(),
      '/product':(context)=>ProductPage(),
      '/search':(context,{arguments})=>SearchPage(arguments:arguments),
};

//固定写法
var onGenerateRoute=(RouteSettings settings) {      
      //String? 表示name为可空类型
      final String? name = settings.name; 
      //Function? 表示pageContentBuilder为可空类型
      final Function? pageContentBuilder = routes[name];      
      if (pageContentBuilder != null) {
        if (settings.arguments != null) {
          final Route route = MaterialPageRoute(
              builder: (context) =>
                  pageContentBuilder(context, arguments: settings.arguments));
          return route;
        }else{
            final Route route = MaterialPageRoute(
              builder: (context) =>
                  pageContentBuilder(context));
            return route;
        }
      }
};

在main.dart里面配置

void main() => runApp(MyApp());
class MyApp extends StatelessWidget {  
  
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      // home:Tabs(),   
      initialRoute: '/',     //初始化的时候加载的路由
      onGenerateRoute: onGenerateRoute
    );
  }
}

跳转代码

ElevatedButton(
    child: Text("跳转到商品页面"),
    onPressed: () {
      Navigator.pushNamed(context, '/product');
    }
),  

路由钩子

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 跳转,路由堆栈变化:

Flutter-导航与路由堆栈详解

如果 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 堆栈的变化

Flutter-导航与路由堆栈详解

Navigator.of(context).pushNamedAndRemoveUntil('/D', ModalRoute.withName('/B'));

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

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

路由堆栈变化:

Flutter-导航与路由堆栈详解

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 后,路由堆栈变化

Flutter-导航与路由堆栈详解

此时路由堆栈为空,没有可显示的页面,应用程序将会退出或者黑屏,好的用户体验不应如此,此时可以使用 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…