Example analysis of Flutter 2.0 routing at the 18th bend of mountain road

preface

Last Flutter 2.0's routing confused me The routing of fluent 2.0 is introduced. After reading the introduction, it is basically in the clouds. After tossing around for a day, I finally got a complete example. In a word, it is summarized as: Eighteen turns of mountain road!

Note that this article is long and time-consuming to read (it must take a long time to take the mountain road). If you are impatient, you can click a praise and download the source code directly.

Code structure

In order to simplify understanding, this article removes the previous redundant demonstration, and only retains the startup page, dynamic list and dynamic details page. The source code can be seen here Source address of this article . Specifically, there are three types of codes:

  • Page code: UI interface code, including startup page, dynamic list and dynamic details page
  • Routing code: that is, the 2.0 routing implementation code, including the routing configuration data class AppRouterConfiguration, the routing information parsing class AppRouterInformationParser and the core routing delegation class AppRouterDelegate.
  • App configuration: change the route configuration mode of APP entry class MyApp to 2.0 route configuration mode in main.dart.

The code directory structure is as follows:

2.0 routing concept

The reason why 2.0 routing needs to be changed is more to meet the needs of complex routing on the Web side, and also to meet the concept of state driven interface design. That is, the interface is separated from the behavior, and the interface is driven to complete the established behavior by changing the state. Therefore, the key point of 2.0 routing is that the previous Navigator.push or Navigator.pop methods are missing in the new interface. The interface only responds to user operations to change the data state, and the page routing jump is uniformly handed over to RougterDelegate.

Interpretation of routing code

In order to simplify code reading, the routing configuration related codes are in the app_ router_ In the path.dart class. The following contents are defined here:

  • RouterPaths: page route enumeration. Different enumerations correspond to different pages;
  • AppRouterConfiguration: a routing configuration class, which is a basic type. It stores the current routing enumeration path (to know the current routing address) and a dynamic status data state (used to transfer data to a new page).
  • AppRouterInformationParser: a routing information parsing class, which inherits from RouteInformationParser. When a route jump is performed, the route parsing method will be called to obtain the corresponding route configuration object. This class replicates two methods, one is parseRouteInformation, which is used to return the corresponding route configuration object after matching after route path resolution. Another restoreRouteInformation is to return different routing information objects through different routing enumerations, which is equivalent to the inverse process of parseRouteInformation.

This part of the code is not complex, just read the source code. The complexity lies in the routing delegate implementation class_ Delegate.dart definition. The code of the whole class is as follows:

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 inherits from RouterDelegate < AppRouterConfiguration >. RouterDelegate itself is a generic class. During inheritance, it is specified to use the generic instantiated by AppRouterConfiguration as the routing configuration class.

At the same time, ChangeNotifier and popnavigator routerdelegatemixin are implemented in the with mode. ChangeNotifier is used to add status change listening objects and notify listening objects to take actions. The addition of this listening object is directly completed by the bottom layer. When there is a status change, the notifyListeners method should be called to notify all listeners to take corresponding actions. This is equivalent to the implementation of observer mode. If you are interested, you can take a look at the source code of ChangeNotifier.

PopNavigatorRouterDelegateMixin is used to manage return events. There is only one method that can override its method to customize the return event.

Let's first look at the definition of member attributes:

  • Navigator key: a GlobalKey used to store the state of the navigator so that the current state of the navigator can be known globally.
  • _ routerPath: stores the route enumeration of the current page. When there is a change, you can notify the route jump.
  • _ state: route status object (i.e. route parameter), and_ Together with routerPath, you can build the current routing configuration AppRouterConfiguration object.
  • _ splashFinished: whether the startup page is completed. When there is a startup page, the home page is the startup page. It is used to remove the startup page from the routing table after startup, so as to display the actual home page. This is in the back_ The buildPages method is embodied.

Let's look at routing related methods (excluding interface transfer methods):

  • build method: the route construction method wraps all the route pages through a Navigator, which is somewhat similar to the router of React (I don't know whether to copy React, but it looks like). The first route is the home page, and the latter is matched to the corresponding page according to the current route enumeration status. At the same time, a return processing method is specified, which can be customized according to different return scenarios.
  • _ buildPages method: used to return the pages parameter required by the build method. Here, we will decide what kind of page to return according to whether the startup page is loaded or not.
  • popRoute: overrides the method of PopNavigatorRouterDelegateMixin. It is simply handled here and directly returns true.
  • setNewRoutePath: set the route configuration parameters, where you can update the status used by the route_ routerPath and_ state.
  • _ Handlepopupage: that is, the return processing method used by the build method, which is also a simple processing here.
  • currentConfiguration get: through_ routerPath and_ state returns the current route configuration parameters.

The whole process is that when the routing configuration parameters are changed, the build method will be called again to build the existing page and decide which page to jump to.

Business code change

Since the business code can no longer be used for push and pop jump and return, those involved need to be changed. Because the routing state parameters need to be modified, these state modification behaviors are completed by passing the corresponding callback method when building the business page. The method to end the start page is:_ handleSplashFinished, mark the status when the startup page is completed_ splashFinished is true, and modify the current routing page to a dynamic list_ The handleSplashFinished method will be passed to the startup page. When the startup timing time is up, this method will be called to replace the push method to realize page switching.

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;
  
  //Omit other codes
  
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    if (!_initialized) {
      _initialized = true;
      Timer(const Duration(milliseconds: 2000), () {
        onFinished();
      });
    }
  }
}

The dynamic list is the same. It also needs to receive an onItemTapped method to respond to the click event of each line element, and return the clicked element object to update the routing parameters. There is also a case where the routing passes the function to the dynamic list, and the dynamic list passes the function to each line element. Is it found that it is somewhat similar to the value passed by the parent-child component of React? In fact, the purpose of each business code receiving callback function is to change the routing status parameters and realize page Jump.

It can also be seen here that in fact, the current method exposes the business implementation and destroys the encapsulation. Moreover, if the parent-child elements are nested too deeply, the transmission link will be too long. At this time, like React, a Redux like state manager is needed to decouple.

App routing configuration change

The App routing configuration change is relatively simple. Return the MaterialApp.router method in the build method of the entry to build it. The two key parameters here are the routing delegate routerDelegate and the routing information parser routeInformationParser. Set these two parameters as the implementation object of the corresponding class I define. The source code is as follows:

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: '2.0 route',
      routerDelegate: AppRouterDelegate(),
      routeInformationParser: AppRouterInformationParser(),
      //Omit other codes
    );
  }
}

summary

In general, the routing management of fluent 2.0 is much more complex than that of version 1.0. For non Web applications, the routing of version 1.0 can continue to be used. Of course, after upgrading, it also has the following advantages:

  • Route management and route resolution are separated. You can define your own route resolution class and route parameter configuration class, which is more flexible.
  • Routing pages can be generated dynamically, so it is easier to implement dynamic routing.
  • The page does not need to manage the jump logic, decouples the page and route, and maintains the consistency of the state driven interface.
  • The status management component can be introduced to manage the routing status of the whole App, which is more scalable.

Tags: Front-end Design Pattern Flutter

Posted on Fri, 03 Dec 2021 09:06:10 -0500 by smclay