记录一次Flutter路由模块的封装实践

2,147 阅读4分钟

最近用Flutter开发,发现Flutter的命名路由虽然方便,但并不适合大型项目,当你的界面越来越多,你会发现越来越难进行路由管理和维护。
试了下Fluro这个框架,虽然是解决了维护难的问题,但上手实在过于繁杂,且作者的文档寥寥无几,更多的是大神们自己的经验分享,于是便自己操刀,将路由封装成自己想要的样子。

整体封装的思维

按照我的习惯,我更喜欢Vue的路由管理,所以将仿照Vue的路由来进行封装,主要实现:

  • map表形式的路由管理
  • 支持嵌套路由
  • 自定义路由动画
  • 路由传参

实现-map表形式的路由管理

官方的路由表代码为:

//注册路由表
  routes:{
   "new_page":(context) => NewRoute(),
    ... // 省略其它路由注册信息
  } ,

查看方法得知,需要返回Map<String, WidgetBuilder>

便可以新建方法Router,返回对应的格式

Map<String,Widget Function(BuildContext)> Router(BuildContext context){}

再新建方法Routes,用作路由表

List<Map<String,dynamic>> Routes(){
	return [
  	{
      "path":"/",
      "view":Home(),
    },
    {
      "path":"/changeLanguage",
      "view":ChangeLanguage(),
    },
  ]
}

然后在Router方法中,进行遍历,将路由表格式化成需要的格式进行输出

Map<String,Widget Function(BuildContext)> Router(BuildContext context){
  Map<String,Widget Function(BuildContext)> router = {};
  for(var route in Routes()){
    router[route["path"]] = (context)=>route["view"];
  }
  return router;
}

最后将main.dart中的routes替换成我们的方法即可

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      initialRoute: "/",
      routes: Router(context)
    );
  }
}

整体逻辑如图:

实现-支持嵌套路由

简单的路由表实现了,但这样和官方的命名路由方式其实一样,只是变成了表的形式,为了拓展,我们需要实现嵌套的功能

Routes中添加子路由

List<Map<String,dynamic>> Routes(){
  return [
    {
      "path":"/",
      "view":Home(),
    },
    //嵌套路由示例
     {
    //上层路由无需view
       "path":"/busManage",
       "children":[
          //匹配规则为"/busManage/demo1"
         {
           "path":"/demo1",
           "view":Demo1()
         },{
          //匹配规则为"/busManage/demo2/demo3"
           "path":"/demo2",
           "children":[
             {
               "path":"/demo3",
               "view":Demo3()
             }
           ]
         },
       ]
     }
  ];
}

此时观察我们的结构,children的结构和父级结构一致,我们便可以使用递归的方式来实现。
更改Router的方法:

Map<String,Widget Function(BuildContext)> Router(BuildContext context){
  Map<String,Widget Function(BuildContext)> router = {};
  List<Map<String,dynamic>> screenRoutes = [];
  for(var route in Routes()){
    var path = route["path"];
    if(route["children"]!=null){
     TreeChildrenRouter(path, route["children"],screenRoutes);
    }else{
      router[route["path"]] = (context)=>route["view"];
    }
  }
  for(var route in screenRoutes){
    router[route["path"]] = (context)=>route["view"];
  }
  return router;
}

TreeChildrenRouter(String path,List<Map> routes,List<Map<String,dynamic>> screenRoutes){
  String rootPath = path;
  for(var route in routes){
    path=rootPath+route["path"];
    if(route["children"]!=null){
      TreeChildrenRouter(path, route["children"], screenRoutes);
    }else{
      screenRoutes.add({"path":path,"view":route["view"]});
    }
  }
}

如此便实现了嵌套路由。
整体逻辑如图:

实现-自定义路由动画

基本仿Vue的路由实现了,但是有时候业务需要我们做各种各样的动效,包括路由的动效,而官方的命名路由是不支持自定义动画路由的,只能通过模态路由才能实现自定义动画,我们当然不能屈服,我们可以通过新建工具类,来实现命名路由转换成模态路由,从而实现自定义动画。

新建AnimateRouter继承PageRouteBuilder实现动画模态路由

class AnimateRouter extends PageRouteBuilder {
  final Widget x;
  final String mode;

  AnimateRouter(this.x, this.mode)
      : super(
            transitionDuration: Duration(milliseconds: 500),
            pageBuilder: (BuildContext context, Animation<double> animation1,
                Animation<double> animation2) {
              return x;
            },
            transitionsBuilder: (BuildContext context,
                Animation<double> animation1,
                Animation<double> animation2,
                Widget child) {
              Widget routerWidget;
              switch (mode) {
                case 'slide':
                  routerWidget = SlideTransition(
                    child: child,
                    position: Tween<Offset>(
                            begin: Offset(1.0, 0.0), end: Offset(0.0, 0.0))
                        .animate(CurvedAnimation(
                            parent: animation1, curve: Curves.ease)),
                  );
                  break;
                case 'fade':
                  routerWidget = FadeTransition(
                    child: child,
                    opacity: Tween(begin: 0.0, end: 1.0).animate(
                        CurvedAnimation(
                            parent: animation1, curve: Curves.linear)),
                  );
                  break;
                case 'scale':
                  routerWidget = ScaleTransition(
                    child: child,
                    scale: Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation(
                        parent: animation1, curve: Curves.linear)),
                  );
                  break;
                case 'rotation':
                  routerWidget = RotationTransition(
                    child: child,
                    turns: Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation(
                        parent: animation1, curve: Curves.linear)),
                  );
                  break;
                case 'rotationScale':
                  routerWidget = RotationTransition(
                    child: ScaleTransition(
                      child: child,
                      scale: Tween(begin: 0.0, end: 1.0).animate(
                          CurvedAnimation(
                              parent: animation1, curve: Curves.linear)),
                    ),
                    turns: Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation(
                        parent: animation1, curve: Curves.linear)),
                  );
                  break;
              }
              return routerWidget;
            });
}

其中的x便是我们的需要传入的路由组件,mode是路由动画的判断key,根据mode来控制路由做哪个变换动画

新建RouterUtil来实现命名路由转换成动画模态路由,思路就是从我们自定义的路由表获取到对应的路由组件,传入模态路由即可,其中的argument是路由传参,下节会说。

class RouterUtil {
  RouterUtil.pushNamed(BuildContext context, String name,
      {String mode = 'slide', dynamic argument = ''}) {
    Map<String, Widget Function(BuildContext)> routerList = Router(context);
    if (routerList.containsKey(name)) {
      setArgument(argument);
      Navigator.of(context)
          .push(AnimateRouter(routerList[name](context), mode));
    } else {
      throw FormatException("未寻找到命名路由");
    }
  }

  RouterUtil.pushReplacementNamed(BuildContext context, String name,
      {String mode = 'slide', dynamic argument = ''}) {
    Map<String, Widget Function(BuildContext)> routerList = Router(context);
    if (routerList.containsKey(name)) {
      setArgument(argument);
      Navigator.of(context)
          .pushReplacement(AnimateRouter(routerList[name](context), mode));
    } else {
      throw FormatException("未寻找到命名路由");
    }
  }

  RouterUtil.pushNamedAndRemoveUntil(BuildContext context, String name,
      {String mode = 'slide', dynamic argument = ''}) {
    Map<String, Widget Function(BuildContext)> routerList = Router(context);
    if (routerList.containsKey(name)) {
      setArgument(argument);
      Navigator.of(context).pushAndRemoveUntil(
          AnimateRouter(routerList[name](context), mode), (route) => false);
    } else {
      throw FormatException("未寻找到命名路由");
    }
  }
}

整体逻辑如图:

实现-路由传参

有了上面的路由封装,会发现有个致命的缺陷,我们的自定义动画路由没法传参。官方的模态路由传参是在路由组件中进行的,而我们的封装方式无法进行这种操作,于是另辟蹊径,使用get方式来实现

RouterUtil方法的外层,命名变量argument

dynamic _argument;

dynamic get argument => _argument;

setArgument(dynamic argument) {
  _argument = argument;
}

这样在路由组件中,直接获取argument便是我们传的参数了。

结语

以上实践来源于Flutter Create Framework,这是一个Flutter的快速开发框架,是一个基于实际的业务需求而形成的Template
国内仓库:gitee
国外仓库:github
开发文档:wiki