Flutter 入门与实战(二十一):山路十八弯的2.0路由

2,334 阅读7分钟

这是我参与更文挑战的第27天,活动详情查看: 更文挑战

前言

上一篇Flutter 入门与实战(二十):Flutter 2.0的路由把我搞蒙了对 Flutter 2.0的路由做了介绍,看完介绍基本上还是云里雾里。今天折腾了一天(有点夸张,上午在摸鱼看 NBA 太阳对快船直播),终于把一个完整的示例弄出来了,一句话总结就是:山路十八弯! image.png 提示一下,本文篇幅较长,阅读比较耗时(走山路肯定时间久),如果没耐心,点个赞直接下载源码看也是可以的。

image.png

代码结构

为了简化理解,本篇将之前的多余的演示去掉了,只保留了启动页,动态列表和动态详情页面,源码可以看这里本篇源码地址。具体而言代码分三种:

  • 页面代码:即 UI界面代码,包括启动页、动态列表和动态详情页面
  • 路由代码:即2.0路由实现代码,包括路由配置数据类AppRouterConfiguration,路由信息解析类AppRouterInformationParser和核心的路由委托类AppRouterDelegate
  • App配置:在 main.dart 中将 App 入口类 MyApp的路由配置方式改成2.0路由配置方式。

代码目录结构如下: image.png

2.0路由的理念

2.0路由之所以要改动,更多地是为了满足 Web 端复杂路由的需要,同时也是满足状态驱动界面设计的理念。即界面与行为进行分离,通过更改状态来驱动界面完成既定行为。因此,2.0路由最关键的地方就是之前的 Navigator.pushNavigator.pop 方法在新的界面中不见了,界面只是响应用户操作去更改数据状态,而页面路由跳转统一交给了 RougterDelegate 来完成。

路由代码解读

为了简化代码阅读,路由配置相关的代码都在 app_router_path.dart 类中。这里定义了如下内容:

  • RouterPaths:页面路由枚举,不同的枚举对应不同的页面;
  • AppRouterConfiguration:路由配置类,是一个基础类型,存储了当前路由枚举path(以便知道当前的路由地址)和一个动态的状态数据state(用于将数据传递到新的页面)。
  • AppRouterInformationParser:路由信息解析类,继承自RouteInformationParser,当进行路由跳转时就会调用路由解析方法,获取对应的路由配置对象。该类复写了两个方法,一个是parseRouteInformation,这个方法是用于通过路由路径解析后,匹配后返回对应的路由配置对象。另一个restoreRouteInformation,是通过不同的路由枚举返回不同的路由信息对象,相当于是parseRouteInformation的逆过程。

这部分代码并不复杂,阅读源码即可。复杂之处在于路由委托实现类,在 router_delegate.dart定义。整个类的代码如下:

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:home_framework/dynamic_detail.dart';
import 'package:home_framework/models/dynamic_entity.dart';
import 'package:home_framework/not_found.dart';
import 'package:home_framework/routers/app_router_path.dart';
import 'package:home_framework/splash.dart';

import '../dynamic.dart';

class AppRouterDelegate extends RouterDelegate<AppRouterConfiguration>
    with
        ChangeNotifier,
        PopNavigatorRouterDelegateMixin<AppRouterConfiguration> {
  @override
  final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

  RouterPaths _routerPath;
  get routerPath => _routerPath;
  set routerPath(RouterPaths value) {
    if (_routerPath == value) return;
    _routerPath = value;

    notifyListeners();
  }

  dynamic _state;
  get state => _state;

  bool _splashFinished = false;
  get splashFinished => _splashFinished;

  set splashFinished(bool value) {
    if (_splashFinished == value) return;
    _splashFinished = value;

    notifyListeners();
  }

  @override
  Widget build(BuildContext context) {
    return Navigator(
      key: navigatorKey,
      pages: _buildPages(),
      onPopPage: _handlePopPage,
    );
  }

  List<Page<void>> _buildPages() {
    if (_splashFinished) {
      return [
        MaterialPage(
            key: ValueKey('home'),
            child: DynamicPage(_handleDynamicItemChanged)),
        if (_routerPath == RouterPaths.splash)
          MaterialPage(
              key: ValueKey('splash'), child: Splash(_handleSplashFinished)),
        if (_routerPath == RouterPaths.dynamicDetail)
          MaterialPage(
              key: ValueKey('dynamicDetail'), child: DynamicDetail(state)),
        if (_routerPath == RouterPaths.notFound)
          MaterialPage(key: ValueKey('notFound'), child: NotFound()),
      ];
    } else {
      return [
        MaterialPage(
            key: ValueKey('splash'), child: Splash(_handleSplashFinished)),
      ];
    }
  }

  void _handleSplashFinished() {
    _routerPath = RouterPaths.dynamicList;
    _splashFinished = true;
    notifyListeners();
  }

  void _handleDynamicItemChanged(DynamicEntity dynamicEntity) {
    _routerPath = RouterPaths.dynamicDetail;
    _state = dynamicEntity;
    notifyListeners();
  }

  @override
  Future<bool> popRoute() async {
    return true;
  }

  @override
  Future<void> setNewRoutePath(AppRouterConfiguration configuration) async {
    _routerPath = configuration.path;
    _state = configuration.state;
  }

  bool _handlePopPage(Route<dynamic> route, dynamic result) {
    final bool success = route.didPop(result);
    return success;
  }

  @override
  AppRouterConfiguration get currentConfiguration =>
      AppRouterConfiguration(routerPath, state);
}

AppRouterDelegate 继承自RouterDelegate<AppRouterConfiguration>RouterDelegate本身是一个泛型类,继承时指定了使用AppRouterConfiguration实例化泛型作为路由配置类。

同时使用with方式实现了 ChangeNotifierPopNavigatorRouterDelegateMixin。其中ChangeNotifier用于增加状态更改监听对象和通知监听对象进行动作,这个监听对象的增加有底层直接完成,当有状态改变时应当调用 notifyListeners方法通知所有监听者做出相应的动作。这个相当于观察者模式的实现,有兴趣的可以看一下 ChangeNotifier 的源码。

PopNavigatorRouterDelegateMixin用于管理返回事件的,只有一个方法,可以覆盖其方法自定义返回事件。

首先看定义了成员属性:

  • navigatorKey:用于存储导航器状态的GlobalKey,以便在全局可以获知导航器的当前的状态。
  • _routerPath:存储当前页面的路由枚举,当发生改变后,可以通知路由跳转。
  • _state:路由状态对象(即路由参数),与_routerPath 一起可以构建当前的路由配置 AppRouterConfiguration 对象。
  • _splashFinished:启动页是否完成,在有启动页的时候首页是启动页,用于在启动完成后将启动页移除路由表,以便显示实际的首页。这个在后面的_buildPages 方法有体现。

再来看路由相关的方法(这里不包括界面传递的方法):

  • build方法:路由构建方法,通过一个 Navigator 包裹全部路由页面,有点类似 React 的 路由器(抄没抄 React 我不知道,只是看着像),第一个路由是首页,后面的是根据当前路由枚举状态匹配到再返回对应的页面。同时指定了一个返回处理方法,这个就可以根据不同的返回场景做自定义处理了。
  • _buildPages 方法:用于返回 build 方法所需要的pages参数。这里会根据启动页是否加载完成来决定返回什么样的页面。
  • popRoute:覆写了PopNavigatorRouterDelegateMixin的方法,这里简单处理了,直接返回了 true
  • setNewRoutePath:设置路由配置参数,在这里可以更新路由用到的状态_routerPath_state
  • _handlePopPage:即 build 方法用到的返回处理方法,这里也是简单的处理。
  • currentConfiguration获取:通过_routerPath_state 构建当前的路由配置参数返回。

整个流程是当有路由配置参数改变后,会重新调用 build 方法,来构建里有页面和决定跳转到哪个页面。

业务代码变更

由于业务代码不能再实用 pushpop 跳转和返回,因此涉及到这些的都需要变更,因为需要修改路由状态参数,因此这些修改状态的行为都通过构建业务页面时传递对应的回调方法来完成。对于启动页结束的方法为:_handleSplashFinished,当启动页完成后,标记状态_splashFinishedtrue,以及修改当前的路由页面为动态列表。_handleSplashFinished方法会传递到启动页面,当启动定时时间到了之后就调用该方法来替换 push 方法,从而实现页面切换。

class Splash extends StatefulWidget {
  final Function onFinished;
  Splash(this.onFinished, {Key key}) : super(key: key);

  @override
  _SplashState createState() => _SplashState(onFinished);
}

class _SplashState extends State<Splash> {
  final Function onFinished;
  _SplashState(this.onFinished);
  bool _initialized = false;
  
  //省略其他代码
  
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    if (!_initialized) {
      _initialized = true;
      Timer(const Duration(milliseconds: 2000), () {
        onFinished();
      });
    }
  }
}

动态列表也一样,同样需要接收一个onItemTapped方法,用于响应每行元素的点击事件,并把点击的元素对象回传以更新路由参数。这里还出现了路由传递函数给动态列表,动态列表再把函数传递给每行元素的情况,是不是发现和 React 的父子组件传值有点类似?实际每个业务代码接收回调函数的目的就是为了更改路由状态参数实现页面跳转。

这里也可以看到实际上目前这种方式暴露了业务的实现,破坏了封装性,而且如果父子元素嵌套过深会导致传递链路过长。这个时候就和 React 一样,需要有类似 Redux 的状态管理器来解耦了。

App 路由配置变更

App 路由配置变更相对简单,在入口的 build 方法中返回 MaterialApp.router 方法来构建即可,这里关键的两个参数就是路由委托routerDelegate和路由信息解析器routeInformationParser,将这两个参数设置为我定义的对应类的实现对象即可。源码如下:

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: '2.0路由',
      routerDelegate: AppRouterDelegate(),
      routeInformationParser: AppRouterInformationParser(),
      //省略其他代码
    );
  }
}

总结

总的来说,Flutter 2.0的路由管理相比1.0版本复杂很多,对于非 Web应用来说可以继续沿用1.0的路由。当然升级后,也有如下优点:

  • 路由管理和路由解析分离,可以自己定义路由解析类和路由参数配置类,更为灵活。
  • 路由页面可以动态生成,因此实现动态路由更为简单。
  • 页面无需管理跳转逻辑,将页面和路由分离解耦,保持状态驱动界面的一致性。
  • 可以引入状态管理组件来管理整个 App 的路由状态,扩展性更强。

最后,觉得有收获的掘友欢迎点赞及在评论区互动交流!