Flutter 山路十八弯的2.0路由

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 总的来说,Flutter 2.0的路由管理相比1.0版本复杂很多也更强大,对于非 Web应用来说可以继续沿用1.0的路由,对于 Web 应用或大型移动应用可以考虑2.0路由。

前言

上一篇Flutter 2.0的路由把我搞蒙了对 Flutter 2.0的路由做了介绍,看完介绍基本上还是云里雾里。今天折腾了一天,终于把一个完整的示例弄出来了,一句话总结就是:山路十八弯!
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 的路由状态,扩展性更强。

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


欢迎关注个人公众号:公众号

相关文章
|
Dart Android开发 UED
带你读《深入浅出Dart》二十七、Flutter路由管理
带你读《深入浅出Dart》二十七、Flutter路由管理
130 0
Flutter Getx 路由 until 方法帮助你跳转指定路由
不少同学都会问我,这样一个场景,当我点击商品列表,进入商品页,点击购买,支付成功后,想返回商品页,或者我的中心的订单列表。怎么做,这中间跨度了 n 个路由。 我不只一次的推荐 GetX 的 until 方法,和 offNamedUntil 方法。 我写了个 demo 今天我们就一起来看下这两个方法如何使用。
1567 0
Flutter Getx 路由 until 方法帮助你跳转指定路由
|
15天前
|
Go 网络架构 开发者
Flutter &鸿蒙next中的路由使用详解【基础使用】
本文介绍了 Flutter 路由系统的使用方法,包括基本路由、命名路由、参数传递、返回参数和动态路由。通过 `Navigator` 类实现页面跳转,支持简单和复杂参数的传递,并可通过 `onGenerateRoute` 实现更灵活的动态路由管理。示例代码展示了如何在实际项目中应用这些技术,帮助开发者构建清晰、易于维护的导航结构。
62 1
|
1月前
|
UED 开发者
flutter:获取对象&路由管理 (四)
本文介绍了Flutter中如何通过Context获取状态对象、使用GlobalKey获取状态对象、基本的路由管理、路由传值、命名路由、返回根路由以及点击图标跳转的方法。示例代码展示了如何在应用中实现这些功能,包括页面跳转、传递参数和返回上一页等操作。
|
6月前
|
安全 Go 数据安全/隐私保护
Flutter开发笔记:Flutter路由技术
Flutter开发笔记:Flutter路由技术
782 0
|
4月前
|
Android开发
Flutter路由跳转参数处理小技巧
Flutter路由跳转参数处理小技巧
41 0
|
6月前
|
前端开发 开发者 iOS开发
【Flutter前端技术开发专栏】Flutter中的路由管理与页面跳转
【4月更文挑战第30天】本文介绍了Flutter的路由管理与页面跳转,包括基本和命名路由管理。基本路由使用`Navigator`的`push`和`pop`方法,如`MaterialPageRoute`和`CupertinoPageRoute`。命名路由则通过路由表注册名称进行跳转,如`Navigator.pushNamed`。此外,还展示了如何通过构造函数、`arguments`和`PageRouteBuilder`进行路由传值。掌握这些知识能提升Flutter开发效率。
109 0
【Flutter前端技术开发专栏】Flutter中的路由管理与页面跳转
|
6月前
|
BI
Flutter笔记:路由观察者
Flutter笔记:路由观察者
287 0
|
6月前
|
UED
Flutter之自定义路由切换动画
Flutter之自定义路由切换动画 在Flutter中,我们可以通过Navigator来实现路由管理,包括路由的跳转和返回等。默认情况下,Flutter提供了一些简单的路由切换动画,但是有时候我们需要自定义一些特殊的动画效果来提高用户体验。本文将介绍如何在Flutter中实现自定义的路由切换动画。
215 0
|
6月前
|
API
Flutter状态管理终极方案GetX第一篇——路由
Flutter状态管理终极方案GetX第一篇——路由 GetX是Flutter中一个非常流行的状态管理库,它不仅提供了简单易用的状态管理功能,还可以帮助我们方便地管理路由。在这篇文章中,我们将介绍如何使用GetX来实现路由管理。
345 0