跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

零 Flutter教程评论80字数 17450阅读58分10秒阅读模式

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

1. 引言

? 路由是什么? 通俗点说就是「页面跳转」,路由管理 就是「控制和管理页面跳转的过程和规则」。实际开发中,很少直接用Flutter的路由来进行页面跳转,基本用的第三方,比如 GetX 就自带路由管理。

? 如果想搞懂路由框架背后的实现原理,感觉还是有必要对Flutter路由做下深入了解的,所以本节就来扒一扒 Navigator 1.0 的使用方法,以及对关键源码进行解读。文章源自灵鲨社区-https://www.0s52.com/bcjc/flutterjc/17492.html

2. Flutter 路由用法详解

? Flutter 的路由系统主要分为两个版本:文章源自灵鲨社区-https://www.0s52.com/bcjc/flutterjc/17492.html

  • Navigator 1.0:Flutter 早期的路由系统,侧重于 移动端(Android/iOS) ,采用「命令式」编程风格,使用 Navigator.push() 和 Navigator.pop() 等方法来管理「路由栈」。
  • Navigator 2.0:Flutter 1.22 版本后新增,侧重于 复杂的桌面端/网页端,采用「声明式」编程风格,使用 Router 和 RouteInformationParser 等类来描述和管理「路由树」。

? 笔者搞移动端居多,所以本节只研究 Navigator 1.0 的使用与原理,它涉及到的两个 核心类文章源自灵鲨社区-https://www.0s52.com/bcjc/flutterjc/17492.html

  • Route:屏幕或页面的抽象。
  • Navigator:管理Route的Widget,通过Route的 入栈与出栈 来实现页面的跳转。

? Flutter 提供了两种配置路由的跳转的方式:「普通/匿名/动态/直接路由」和「命名/静态路由」,一一讲解~文章源自灵鲨社区-https://www.0s52.com/bcjc/flutterjc/17492.html

2.1. 匿名/动态/直接/普通路由

不需要提前注册,可以直接在代码中创建和导航的路由,适用于简单使用或单次导航的场景。用法非常简单:文章源自灵鲨社区-https://www.0s52.com/bcjc/flutterjc/17492.html

dart文章源自灵鲨社区-https://www.0s52.com/bcjc/flutterjc/17492.html

代码解读
复制代码
// ? 跳转页面
Navigator.push(
  context,
  MaterialPageRoute(builder: (context) => SecondScreen()),
);

// ? 关闭页面
Navigator.pop(context);

? 可以看到除了BuildContext外,匿名路由 还需要传入一个 Route 类型的参数,这里用的是 MaterialPageRoute,它可以使用与对应平台风格一致的 切换动画,Android 上是上下滑动切换,iOS 上是左右滑动切换,如果想实现全平台的左右滑动,可以使用 CupertinoPageRoute文章源自灵鲨社区-https://www.0s52.com/bcjc/flutterjc/17492.html

? 当然,也可以 自定义PageRoute,以完全控制页面切换的动画和行为。如果只是想简单自定义下过渡动画,可以使用 PageRouteBuilder 来快速创建自定义路由,它提供了 pageBuilder 和 transitionsBuilder 属性来定义页面和过渡动画,使用代码示例如下:文章源自灵鲨社区-https://www.0s52.com/bcjc/flutterjc/17492.html

dart文章源自灵鲨社区-https://www.0s52.com/bcjc/flutterjc/17492.html

代码解读
复制代码
// ? 只通过 pageBuilder 定制渐入的过渡动画
Navigator.of(context).push(
  PageRouteBuilder(pageBuilder: (context, animation, secondaryAnimation) {
    return FadeTransition(opacity: animation, child: const SecondPage();
  })
);

// ? pageBuilder + transitionsBuilder 定制过渡动画:新页面从屏幕右侧滑入,旧页面从左侧划出
Navigator.of(context).push(PageRouteBuilder(
  pageBuilder: (context, animation, secondaryAnimation) => const SecondPage(),
  transitionsBuilder: (context, animation, secondaryAnimation, child) {
    var begin = const Offset(1.0, 0.0);	// 动画的起始位置,X轴方向屏幕右侧偏移起点
    var end = Offset.zero;	// 动画的结束位置,0表示没有偏移,即初始位置
    var curve = Curves.easeInOut;	// 创建动画曲线,easeInOut 表示开始慢,中间加速,结束慢
    // 创建一个从begin到end的补间动画,.chain 表示与曲线结合,即创建一个遵循指定曲线的动画
    var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
    // SlideTransition 是用于在小部件上应用滑动动画的Widget
    return SlideTransition(
      position: animation.drive(tween),
      child: child,
    );
}));

然后是 页面传参 & 数据回传,前者通过 新页面的构造函数传递,后者通过 pop() 方法回传,写个简单的选颜色例子,先是第一个页面:文章源自灵鲨社区-https://www.0s52.com/bcjc/flutterjc/17492.html

dart

代码解读
复制代码
void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    // 配合 ValueListenableBuilder 组件使用,在值发生变化时自动通知监听者,从而更新UI
    ValueNotifier<Color> colorNotifier = ValueNotifier<Color>(Colors.black);

    return Scaffold(
      appBar: AppBar(title: const Text("Flutter路由学习Demo")),
      body: Center(
        child: Column(
          children: [
            // 颜色改变时刷新Text组件
            ValueListenableBuilder(
                valueListenable: colorNotifier,
                builder: (context, value, child) {
                  return Text("当前选中的颜色:$value", style: TextStyle(color: value));
                }),
            const SizedBox(height: 5),
            // 点击跳转颜色选择页
            ElevatedButton(
                onPressed: () async {
                  // ? await 异步等待页面关闭数据回传
                  Color? chooseColor = await Navigator.of(context)
                      .push(MaterialPageRoute(builder: (context) => ColorChoosePage(color: colorNotifier.value)));
                  if (null != chooseColor) colorNotifier.value = chooseColor;
                },
                child: const Text("跳转颜色选择页"))
          ],
        ),
      ),
    );
  }
}

然后是跳转的选择颜色页:

dart

代码解读
复制代码
class ColorChoosePage extends StatefulWidget {
  // ? 通过构造方法传递数据
  final Color color;

  const ColorChoosePage({super.key, required this.color});

  @override
  State createState() => _ColorChoosePageState();
}

class _ColorChoosePageState extends State<ColorChoosePage> {
  final _random = Random();

  @override
  Widget build(BuildContext context) {
    // 获得屏幕宽度的1/5
    final widgetWidth = MediaQuery.of(context).size.width / 5 - 10;
    // 随机生成25个颜色
    final colors = List.generate(
        25,
        (index) => Color.fromRGBO(
              _random.nextInt(256),
              _random.nextInt(256),
              _random.nextInt(256),
              1,
            ));

    return Scaffold(
      appBar: AppBar(title: const Text('颜色选择页')),
      body: Padding(
        padding: const EdgeInsets.all(5),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text("之前选中的颜色", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
            const SizedBox(height: 5),
            _generateColorItem(widget.color, widgetWidth),
            const Text("选色板", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
            const SizedBox(height: 5),
            Expanded(
                child: GridView.builder(
              gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 5,
                crossAxisSpacing: 5.0,
                mainAxisSpacing: 5.0,
              ),
              itemCount: colors.length,
              itemBuilder: (context, index) => _generateColorItem(colors[index], widgetWidth),
            ))
          ],
        ),
      ),
    );
  }

  // 生成颜色组件
  Widget _generateColorItem(Color color, double widgetWidth) => GestureDetector(
        onTap: () {
          // ? 关闭页面时给上一个页面传递数据
          Navigator.pop(context, color);
        },
        child: Container(
          width: widgetWidth,
          height: widgetWidth,
          color: color,
          alignment: Alignment.center,
          child: Text(
            color.toString().substring(10, 16),
            style: const TextStyle(color: Colors.white),
          ),
        ),
      );
}

运行看看效果

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

2.2. 命名/静态路由

说完匿名路由,接着说下命名路由,就是 需要提前注册的路由,先给每个路由定义一个「字符串名称」,然后通过这个名称来导航到对应的路由,适用于复杂应用或重复导航的场景。玩法如下:

在应用的 根级别 (MaterialApp 或 CupertinoApp ) 定义路由,使用「routes」参数将路由名称映射到对应的Widget。

? 用命名路由实现前面匿名路由选颜色Demo的效果,先是第一个页面:

dart

代码解读
复制代码
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(primarySwatch: Colors.blue),
      // ? 注册路由
      routes: {
        "/": (context) => const MyHomePage(),
        "/color_choose_page": (context) => const ColorChoosePage(),
      },
      // ? 初始路由
      initialRoute: "/",
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    // 配合 ValueListenableBuilder 组件使用,在值发生变化时自动通知监听者,从而更新UI
    ValueNotifier<Color> colorNotifier = ValueNotifier<Color>(Colors.black);

    return Scaffold(
      appBar: AppBar(title: const Text("Flutter路由学习Demo")),
      body: Center(
        child: Column(
          children: [
            // 颜色改变时刷新Text组件
            ValueListenableBuilder(
                valueListenable: colorNotifier,
                builder: (context, value, child) {
                  return Text("当前选中的颜色:$value", style: TextStyle(color: value));
                }),
            const SizedBox(height: 5),
            // 点击跳转颜色选择页
            ElevatedButton(
                onPressed: () async {
                  // ? await 异步等待页面关闭数据回传,pushNamed 通过路由名称跳转
                  var result = await Navigator.of(context)
                      .pushNamed("/color_choose_page", arguments: {"chooseColor": colorNotifier.value});
                  Color? chooseColor = result as Color?;
                  if (null != chooseColor) colorNotifier.value = chooseColor;
                },
                child: const Text("跳转颜色选择页"))
          ],
        ),
      ),
    );
  }
}

然后是选择颜色页:

dart

代码解读
复制代码
class ColorChoosePage extends StatelessWidget {
  const ColorChoosePage({super.key});

  @override
  Widget build(BuildContext context) {
    // ? 获取路由参数
    var args = ModalRoute.of(context)?.settings.arguments as Map<String, dynamic>?;
    final Color color = args?['chooseColor'] ?? Colors.black;
    final random = Random();
    // 获得屏幕宽度的1/5
    final widgetWidth = MediaQuery.of(context).size.width / 5 - 10;
    // 随机生成25个颜色
    final colors = List.generate(
        25,
        (index) => Color.fromRGBO(
              random.nextInt(256),
              random.nextInt(256),
              random.nextInt(256),
              1,
            ));

    return Scaffold(
      appBar: AppBar(title: const Text('颜色选择页')),
      body: Padding(
        padding: const EdgeInsets.all(5),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text("之前选中的颜色", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
            const SizedBox(height: 5),
            _generateColorItem(context, color, widgetWidth),
            const Text("选色板", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
            const SizedBox(height: 5),
            Expanded(
                child: GridView.builder(
              gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 5,
                crossAxisSpacing: 5.0,
                mainAxisSpacing: 5.0,
              ),
              itemCount: colors.length,
              itemBuilder: (context, index) => _generateColorItem(context, colors[index], widgetWidth),
            ))
          ],
        ),
      ),
    );
  }

  // 生成颜色组件
  Widget _generateColorItem(BuildContext context, Color color, double widgetWidth) => GestureDetector(
        onTap: () {
          // ? 关闭页面时给上一个页面传递数据
          Navigator.pop(context, color);
        },
        child: Container(
          width: widgetWidth,
          height: widgetWidth,
          color: color,
          alignment: Alignment.center,
          child: Text(
            color.toString().substring(10, 16),
            style: const TextStyle(color: Colors.white),
          ),
        ),
      );
}

? 另外,实际开发中,通常会把 注册路由 部分的代码单独抽离出来 动态生成,然后通过 onGenerateRoute 属性传入,改下上面的Demo试试:

dart

代码解读
复制代码
final Map routes = {
  "/": (context) => const MyHomePage(),
  "/color_choose_page": (context) => const ColorChoosePage(),
};

var onGenerateRoute = (settings) {
  // 获取路由名称
  final String? name = settings.name;
  // 获取路由对应的页面构建函数
  final Function? pageContentBuilder = routes[name];
  if (null != pageContentBuilder) {
    // 判断是否有传递参数
    if (settings.arguments != null) {
      return MaterialPageRoute(builder: (context) => pageContentBuilder(context), settings: settings);
    } else {
      return MaterialPageRoute(builder: (context) => pageContentBuilder(context));
    }
  }
  // 未找到路由,跳转到未知页面
  return MaterialPageRoute(builder: (context) => const UnKnowPage());
};


// ? 调用处
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(primarySwatch: Colors.blue),
      // ? 路由导航动态生成
      onGenerateRoute: onGenerateRoute,
      initialRoute: "/",
    );
  }
}

2.3. 路由操作

? 介绍完匿名和命名路由的入栈和出栈,接着说下Flutter提供的一些路由操作~

2.3.1. 路由替换

就 新路由进栈并替换当前路由,适用场景示例:

页面跳转后,点击返回按钮,默认会回到上一个页面,如果不想回到上一个页面,而是直接回到 上上一个页面。比如:登录页跳主页面。

Flutter 提供了三个替换API,使用代码示例如下:

dart

代码解读
复制代码
// ? 匿名路由替换
Navigator.of(context).pushReplacement(
    MaterialPageRoute(builder: (context) => const ReplacePage())
);

// ? 命名路由替换
Navigator.of(context).pushReplacementNamed("/replace");

// ? 命名路由替换,先 pop 再 push 新页面
Navigator.of(context).pushReplacementNamed("/replace");

2.3.2. 新路由入栈+移除之前的路由,直到条件满足

?‍♀️ 如题,Flutter 提供两个API,使用代码示例如下:

dart

代码解读
复制代码
// ? 匿名路由
Navigator.pushAndRemoveUntil(
  context,
  MaterialPageRoute(builder: (context) => NewPage()),
  (Route<dynamic> route) => route.isFirst, // 保留直到第一个路由(即主页)
);

// ? 命名路由
// 假设你有一个Navigator对象和一个路由名称
Navigator.of(context).pushAndRemoveUntil(
  MaterialPageRoute(builder: (context) => NewPage()),
  (Route<dynamic> route) => route.isFirst, // 保留直到第一个路由(即主页)
);

2.3.3. 路由出栈,直到条件满足

就 不跳页面,只弹出导航堆栈中的路由,直到条件满足。使用代码示例如下:

dart

代码解读
复制代码
// ? 弹出直到根路由为止
Navigator.of(context).popUntil((Route<dynamic> route) => route.isFirst); 
Navigator.of(context).popUntil((Route<dynamic> route) => route.settings.name = "/"); 
Navigator.of(context).popUntil(ModalRoute.withName("/")); 

关于 路由出栈,Flutter 还提供了这两个API:

  • canPop:检查当前导航堆栈是否还有路由可以弹出,处于根路由时,会返回false。
  • maybePop:尝试弹出当前路由,如果导航堆栈没其他路由或弹出操作被拦截,不会执行任何操作。

2.3.4. 删除指定路由

?‍♂️ Flutter 提供了两个直接删除路由的API (不涉及路由的推入和弹出,立即关闭):removeRoute() 和 removeRouteBelow() ,使用代码示例如下:

dart

代码解读
复制代码
// 获得当前路由
final route = ModalRoute.of(context);

// ? 移除指定路由,如:当前路由
if (route != null) {
  Navigator.of(context).removeRoute(route);
}

// ? 移除指定路由下方的单个路由
if (route != null) {
  Navigator.of(context).removeRouteBelow(route);
}

? 关于Flutter路由的用法就讲解到这~

3. 源码详解

在具体扒源码前,简单思考下设计一个路由系统时需要考虑哪些因素。? 大部分路由系统都是采用 栈(LIFO,后进先出) 的「数据结构」,每个页面作为一个「栈帧」存在在页面栈中。栈帧一般包含以下信息:

  • 页面标识(如:页面名称、URL 等)
  • 页面参数(用于数据传递)
  • 页面状态(如加载状态、缓存状态、生命周期等)

接着是「页面跳转的管理」,基本操作有:

  • 入栈操作:用户从页面A跳到页面B,需要将B压入 栈顶,要保持页面A在栈中的位置。
  • 出栈操作:用户从页面B返回页面A,需要将B从栈顶移除,页面A成为新的栈顶页面。
  • 清栈操作:在某些场景下,如:用户退出应用,需要清空整个页面栈。

再接着是「页面数据如何传递」,通过 页面参数传递?还是 全局数据共享?然后是「页面缓存策略」,需要保持栈内/栈外页面状态吗?是否允许「重复入栈」?「异常处理」页面跳转失败怎么办?是否限制页面栈的深度,栈溢出怎么办... ? 好,思维发散激活完毕,具体来看源码~

3.1. Navigator.xxx(context) & Navigator.of(context).xxx()

?‍♂️ 点开源码,可以看到 Navigator.xxx() 最终还是调用的 Navigator.of(context).xxx() :

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

?‍♀️ 所以这两种写法作用其实都是一样的,往下看看 of() 的源码:

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

从字面意思上不难看出,它的作用是:

从给定的BuildContext中获取 最近 的或 根(最顶层) 的 NavigatorState 返回。

看下这两个查找方法,?代码非常像,就循环逻辑那里有些不同,一个是找到直接跳出返回,一个是找到了暂存,直到没有父元素(根节点)循环结束再返回:

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

3.2. NavigatorState#build()

? 所有的栈操作,都是 of() 获得 NavigatorState 实例后调用,看看这个类:

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

? 哦吼,继承了 State,并混入了 TickerProviderStateMixin (动画支持) 和 RestorationMixin (状态恢复)。状态类的核心任务是完成 Widget的构建,先从 build() 方法入手:

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

? 所以 导航跳转的界面 本质上是通过 Overlay-浮层 来实现的❗️ ,它是一个用于在Widget树顶层绘制浮动元素的工具。允许我们在不改变Widget树结构的情况下,添加悬浮在其它Widget之上的内容。通过 initialEntries 属性传入 OverlayEntry(插入到Overlay中的子组件) 对象列表,定义了初始显示在 Overlay 中的条目。跟下 _allRouteOverlayEntries() :

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

? 遍历 _history 列表中的每个 _RouteEntry 对象,获取它们的 overlayEntries (OverlayEntry对象列表)进行合并,最终返回一个 OverlayEntry 类型的可迭代对象。跟下 _History

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

? 混入 ChangeNotifier,维护一个 _RouteEntry 列表,列表发生更改后发出通知。跟下 _RouteEntry

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

? 从它的构造方法不难看出,它就是对 路由节点的封装存储路由实例管理路由生命周期路由切换 及 状态恢复。而 OverlayEntry 则是 Overlay 组件的 浮层节点,代表一个 可以被添加到屏幕上的组件。这两个类要区分开来:

一个 _RouteEntry 中的路由 (Route) 可能会创建一个或多个 OverlayEntry 来实现其内容的展示。

? 然后 OverlayEntry列表 是放在 OverlayState 中的:

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

而 NavigatorState 中定义了 GlobalKey 类型的 _overlayKey,作为 key 传入 Overlay

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

? 定义了一个 overlay 的属性,方便直接获得 OverlayState 状态类对象。

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

3.3. 初始路由的创建

断点 了一下上面的 _allRouteOverlayEntries() ,发现在创建 Overlay 前, _history 里就有 _RouteEntry 了:

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

? em?这个初始路由是在哪里初始化的呢?在 navigator.dart 中搜了下 _history.add,发现了这个:

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

打下 断点,看下方法调用流程:

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

? 行吧,Navigator 和 NavigatorState 关联后调 _firstBuild() ,里面调了 initState() 和 didChangeDependencies() ,最后调 build() 方法来构建 Widget。怪不得在 Overlay 创建前, _history 里就有东西了。

? 看下 路由初始化 部分的代码,调用 onGenerateInitialRoutes() 生成 初始Route对象 列表,map() 遍历,为每个Route对象创建一个 _RouteEntry 实例,并加入到 _history 中。

? 看到这里有些读者可能会问:

不就实例化初始路由吗,为啥不直接直接创建 Route 和 _RouteEntry 实例,搞得那么复杂?

? 跟下代码:

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

RouteListFactory 竟是类型别名,所以这样设计的目的其实是「初始路由列表生成的抽象」,就留个口子,给开发者动态定义路由列表的生成,往上看 onGenerateInitialRoutes() 的注释:

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

如果我们没设置这个属性,将使用默认的 defaultGenerateInitialRoutes() 来生成初始路由列表,跟下代码:

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

? 这个方法会保证返回的列表 至少有一个Route对象 (默认路由),然后生成路由对象都调的 _routeNamed() ,看下关键代码:

dart

代码解读
复制代码
Route<T?>? _routeNamed<T>(String name, { required Object? arguments, bool allowNull = false }) {
  // ? 允许空,且没设置 onGenerateRoute 属性,直接返回null
  if (allowNull && widget.onGenerateRoute == null) {
    return null;
  }
  // ? 根据给定的路由名称和可选参数,创建一个 RouteSettings对象
  final RouteSettings settings = RouteSettings(
    name: name,
    arguments: arguments,
  );
  // ? 调用 onGenerateRoute() 回调并传入上面的RouteSettings对象尝试生成路由。
  Route<T?>? route = widget.onGenerateRoute!(settings) as Route<T?>?;
  // ? 生成失败,且 allowNull 不为false,执行 onUnknownRoute() 回调生成路由
  if (route == null && !allowNull) {
    route = widget.onUnknownRoute!(settings) as Route<T?>?;
  }
  return route;
}

看下 onGenerateRoute 的定义:

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

看下它的类型 RouteFactory

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

? 又是类型别名,参数是 RouteSettings,返回 Route 对象,看回前面 动态注册路由 的代码:

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

? 就,需要 创建路由实例 时,调用我们设置的 onGenerateRoute() 回调来动态生成路由实例,哈哈,看到这里有没有一种豁然开朗的感觉?

?‍♂️ 然后,有一点要注意,Demo 里,我们是通过 MaterialApp 而非直接通过 Navigator 来进行路由设置的!MaterialApp (本质上是 WidgetsApp) 内部会 选择性 创建一个 Navigator 来进行路由栈的管理:

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

? 比如,MaterialApp 有 routes 参数,Navigator 没有,它是在 _onGenerateRoute() 中进行了处理:

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

? 行吧,我们暂且知道了 初始Route对象 的构建流程,接着就该摸索下 浮层节点 的创建与添加了~

3.4. _RouteEntry 的生命周期

再继续往下深扒前,先来了解下 _RouteEntry 的生命周期,就 _RouteLifecycle 状态机。这个状态机只能「向下转移」一旦进入下一个状态,就不能回到上一个状态。? 注释里给出了它的 流转图解

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

对各个状态解释一波 (?翻译的注释):

  • creation of a _RouteEntry:_RouteEntry 实例刚创建的 起始状态
  • staging:准备阶段,等待具体的过渡决策。
  • pushReplace:通过 pushReplace() 及相关方法添加的路由,准备执行 install()、didPush() 等操作。
  • push* :通过 push() 及相关方法添加的路由,准备执行 install() 、didPush() 等操作。
  • add* :准备添加到Navigator堆栈中,通常是通过 onGenerateInitialRoutes 或 widget.pages 创建。
  • replace* :通过 replace() 及相关方法添加的路由,准备执行 install() 、didReplace() 等操作。
  • pushing# :路由推送中,等待 didPush() 操作完成。
  • adding:路由添加中,等待顶层路由的 didPush() 操作完成。
  • idle空闲状态,不执行任何操作。
  • pop* :准备调用 didPop() ,表示路由将要被弹出。
  • complete* :路由完成操作,准备调用 didComplete() 。
  • remove* :路由准备被移除,将执行 didReplace() / didRemove() 等操作。
  • removing# :等待后续路由完成动画,将转换到dispose状态。
  • popping# :路由弹出中,等待调用 finalizeRoute() 以转换到dispose状态。
  • dispose* :路由将被立即销毁。
  • disposing:_RouteEntry实例正在等待其小部件子树先销毁。在等待销毁期间,它被存储在_entryWaitingForSubTreeDisposal中。
  • disposed:路由已被销毁。
  • _RouteEntry garbage collected:终态,实例的内存已被垃圾回收,彻底从系统中移除。

注:「*」表示这些状态是临时的,一旦 _flushHistoryUpdates 被调用,_RouteEntry 将退出这些状态。「#」表示这些状态等待未来的事件或其他异步操作完成,然后自动转移到下一个状态。

? 有个大概印象就行~

3.5. 浮层节点的创建&添加

?‍♂️ 上面创建完初始路由后,还执行了一句 _flushHistoryUpdates() :

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

? 见名知意,就是 刷新_history列表,点开方法打下断点,上面创建初始路由时设置了 initialState: _RouteLifecycle.add,这里自然是走的 add 逻辑:

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

调用了 _RouteEntry#handleAdd() ,跟下:

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

其中调用了 Route#install() ,点开:

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

空实现,打下断点,看下具体实现类:

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

MaterialPageRoute → PageRoute → ModalRoute → TransitionRoute → OverlayRoute,最终定位到了往 浮层列表 中添加浮层的代码:

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

看下 createOverlayEntries() 具体是怎么创建浮层的:

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

? 创建了 两个浮层节点

  • _modalBarrier遮罩,防止用户与下面的路由交互,通常会部分遮挡下面的路由。这个屏障可以是透明的,也可以有颜色 (如:一个对话框打开时,下面的页面通常会被暗化)。另外,如果 barrierDismissible 属性设置为 true,用户可以通过点击屏障来关闭路由。
  • _modalScope:目标路由,实际页面或视图的容器,它还管理路由的状态,如动画和焦点。

另外,除了在 OverlayRoute#install() 中添加了浮层,还在 TransitionRoute#install() 中添加了路由动画切换的 AnimationController 和 动画状态监听

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

3.6. add 操作的调用流程

? 搞清楚浮层节点在创建&添加,继续看完add操作执行流程,更新路由状态为 adding,往 _observedRouteAdditions 里添加一个 _NavigatorPushObservation 路由Push变化事件:

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

然后 continue 直接进入下轮循环,然后调 _RouteEntry#didAdd() :

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

看下代码:

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

接着走 idle 的判定:

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

再往下走,更新参数,继续向后遍历Navigator 的 _history,直到 index < 0,遍历结束。

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

最后执行后续操作:

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

? 看下这个浮层节点重排 Overlay#rearrange() 具体做了什么?

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

old.removeAll(newEntriesList); 这段代码的作用是:保证重新插入条目时,不会有重复的条目。

3.7. push 操作的调用流程

? 看源码可以发现 pushName() 最终调用的还是 push() :

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

_routeNamed() 前面讲过了,传 RouteSetting 参数,调我们传入的 onGenerateRoute() 生成Route对象。跟下 push() :

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

创建了一个状态为 push 的 _RouteEntry 实例传入 _pushEntry() 方法中,点开:

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

好吧,也是走的 _flushHistoryUpdates() :

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

? 吼,pushpushReplacereplace 都调的 _RouteEntry#handlePush() :

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

读者有疑问的代码估计是这句:final TickerFuture routeFuture = route.didPush(); 点开代码看看:

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

跟下 TickerFuture#_complete() :

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

然后对这个 TickerFuture 添加了 whenCompleteOrCancel() 的回调,⚠️注意,里面的代码不会立即执行,会在就 跳转动画完成或取消时 才执行,代码的执行顺序是先⑧⑨,再④⑤。即动画完成后,才将路由状态设置为 idle,调 _flushHistoryUpdates() 触发刷新。? em... 简单提下这两个类,后面讲动画的时候再详细解读~

  • Ticker:Flutter中用于驱动动画的一个类,通常用于更新动画状态,主要作用:提供一个定时器,在每次屏幕刷新时调用回调函数
  • TickerFuture:与Ticker相关的Future,表示一个 Ticker 的完成状态,可以用来等待 Ticker的完成或取消。

说完 push & pushReplace 的逻辑,接着看下 replace,调用了 route.didReplace(previous);

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

好吧,没啥,就是把就路由的AnimationController赋值给新路由,保持状态一致而已~

3.8. pop 操作的调用流程

各种 popXxx() 最终都会调用 pop() 方法:

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

跟下 _RouteEntry#pop() :

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

暂存返回值,修改路由状态为 pop,然后调路由的 onPopInvoked(didPop) ,该方法在路由弹出操作处理后会被调用,即便弹出操作被取消 (如通过PopScope),didPop 参数用于指示后退导航是否成功。跟下 _flushHistoryUpdates() pop状态的处理代码:

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

跟下 handlePop() :

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

点开 Route#didPop() :

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

? 这么点东西,看不出什么,具体实现肯定是子类实现的,打下断点看下方法调用流程:

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

跟下 finishedWhenPopped

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

跟下 _popFinalized

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

行吧,最终都会调用 finalizeRoute() ,关键的代码如下:

dart

代码解读
复制代码
void finalizeRoute(Route<dynamic> route) {
  // ? 查找历史记录中的路由索引
  final int index = _history.indexWhere(_RouteEntry.isRoutePredicate(route));
  final _RouteEntry entry =  _history[index];

  // ? 销毁路由
  entry.finalize();

  // ? 刷新历史记录
  if (!_flushingHistory) {
    _flushHistoryUpdates(rearrangeOverlay: false);
  }
}

# ? _RouteEntry#finalize() 
void finalize() {
  currentState = _RouteLifecycle.dispose;
}

行吧,就动画执行完后会销毁出栈的Route,路由成功弹出往下走:

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

感觉有必要把这两属性拎出来讲一哈,也顺带提下 didPopNext() :

  • poppedRoute最近被弹出的路由,当路由被弹出时,它会被设置为该路由,此变量用于通知新的栈顶路由,其上方的路由已被弹出,可以执行相应的处理逻辑。
  • seenTopActiveRoute是否已经处理过栈顶活动路由的标记,没处理过调用 entry.handleDidPopNext(poppedRoute) 进行处理。
  • didPopNext() :当一个路由被弹出,Navigator 会调用此方法来通知下一个路由,以便它可以执行相应的处理。(handleDidPopNext() 里就是调这个方法)。

继续往下走,往 _observedRouteDeletions 里添加一个 _NavigatorPopObservation 路由变化事件。最后将 canRemoveOrAdd 标记设置为 true,此时路由的状态处于 popping (弹出路由待销毁转换为dispose状态),修改 canRemoveOrAdd 为 true,允许移除或添加路由。

3.9. 其它操作

3.9.1. popUntil()

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

3.9.2. popAndPushNamed()

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

3.9.3. canPop()

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

3.9.4. maybePop()

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

3.9.5. pushAndRemoveUntil()

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

4. 小结

? 最后,总结下本节学习的内容吧,先是概念相关的以及相关的API:

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

然后是源码:

跟?杰哥一起学Flutter (二十二、玩转Flutter路由之——Navigator 1.0详解✈️)

? 调用流程就不画了,读者自己跟着走一遍就很清楚啦~

零
  • 转载请务必保留本文链接:https://www.0s52.com/bcjc/flutterjc/17492.html
    本社区资源仅供用于学习和交流,请勿用于商业用途
    未经允许不得进行转载/复制/分享

发表评论