實例解析山路十八彎的Flutter 2.0路由

前言

上一篇Flutter 2.0的路由把我搞懵了對 Flutter 2.0的路由做了介紹,看完介紹基本上還是雲裏霧裏。折騰了一天,終於把一個完整的示例弄出來了,一句話總結就是:山路十八彎!


提示一下,本文篇幅較長,閱讀比較耗時(走山路肯定時間久),如果沒耐心,點個贊直接下載源碼看也是可以的。

代碼結構

爲了簡化理解,本篇將之前的多餘的演示去掉了,只保留了啓動頁,動態列表和動態詳情頁面,源碼可以看這裏本篇源碼地址。具體而言代碼分三種:

  • 頁面代碼:即 UI界面代碼,包括啓動頁、動態列表和動態詳情頁面
  • 路由代碼:即2.0路由實現代碼,包括路由配置數據類AppRouterConfiguration,路由信息解析類AppRouterInformationParser和核心的路由委託類AppRouterDelegate
  • App配置:在 main.dart 中將 App 入口類 MyApp的路由配置方式改成2.0路由配置方式。

代碼目錄結構如下:


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 的路由狀態,擴展性更強。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章