Flutter 嵌套路由实现

4,928 阅读2分钟

前言

最近开发的某个软件需要用到嵌套路由,一开始的实现方法是在 MaterialApp 的页面中再包一个 MaterialApp,这样嵌套起来好像没什么问题。

但是在我测试的时候,发现两个问题:

  1. 在子路由中用系统的返回键返回,这时候子路由和父路由都会跟着返回了。
  2. 子路由有a和b两个页面,两个页面中的某个 Widget 都有 Hero 动画,从a跳到b页面的时候,b页面会马上再跳转到a页面。

其中问题1很容易解决,使用 WillPopScope 包着 MaterialApp,监听返回事件进行处理就行了。但是问题2却没有找到解决方法,再加上这样可能会遇到其他问题,所以我放弃了这样嵌套。

解决方法

既然嵌套 MaterialApp 不行,那要怎么实现呢?

查了下源码,找到了MaterialApponGenerateRoute 参数,该参数接收一个Function,可以实现拦截命名路由。

我们可以在 onGenerateRoute中,获取到当前要跳转的页面的路由名字,根据名字判断是不是子路由的页面,如果是子路由页面,把该页面传进父页面里面返回。

所以有了以下代码:

import 'package:flutter/material.dart';

typedef Widget NestedRouteBuilder(Widget child);

class NestedRoute {
  final Map<String, NestedRoute> subRoutes;
  final NestedRouteBuilder builder;

  const NestedRoute({this.subRoutes, @required this.builder});

  Route buildRoute(List<String> paths, int index) {
    return MaterialPageRoute(
      builder: (_) => _build(paths, index),
    );
  }

  Widget _build(List<String> paths, int index) {
    if (index >= paths.length) {
      final child = subRoutes != null ? subRoutes['/'] : null;
      return builder(child?.builder(null));
    }
    return builder(subRoutes[paths[index]]?._build(paths, index + 1));
  }
}

RouteFactory buildNestedRoutes(Map<String, NestedRoute> routes) {
  return (RouteSettings settings) {
    List<String> paths = [];
    settings.name.split('/').forEach((element) {
      if(element != '') paths.add(element);
    });
    if(paths.length == 0){
      return routes['/']?.buildRoute([], 1);
    }
    return routes[paths[0]].buildRoute(paths, 1);
  };
}

解释一下,假如我们跳转的路由名称是“/parent/child/son”,从中我们可以知道 parent 里面包着 childchild 里面包着 son 。在 onGenerateRoute 中,返回 parent(child(son)) 就能实现嵌套的效果了。

使用示例:

@override
Widget build(BuildContext context) {
  return new MaterialApp(
    onGenerateRoute: buildNestedRoutes(
      {
        '/': NestedRoute(builder: (_) => Text('我是默认页面')),
        'parent': NestedRoute(
            builder: (child) => Column(
              children: [
                Text('is parent'),
              	child,
              ],
            ),
            subRoutes: {
              'child': NestedRoute(
              	builder: (son) => Column(
                  children: [
                    Text('is child'),
                    son,
                  ],
                ),
                subRoutes: {
                  'son': NestedRoute(builder: (_) => Text('is son')),
            	})
              ),
            }),
          ...
      }
    ),
  );
}
Navigator.pushNamed(context, '/parent/child/son');