当前位置:网站首页>Bolt's practice of route management of flutter (page decoupling, process control, function expansion, etc.)

Bolt's practice of route management of flutter (page decoupling, process control, function expansion, etc.)

2020-12-09 10:44:42 osc_ 0kdyznad

In the major mobile development framework (Android、iOS、Flutter、React Native…) in , Routing management is always UI One of the hottest topics in Architecture .

One big reason is that the application's pages will   Inevitably more , We can use BLOC,MVP,MVI And so on UI Reasonable separation from business logic to achieve a good architecture , But how to integrate a new page into the existing structure is still a big problem .

Android in , Except for the traditional Intent / Fragment Way of doing business ,Google Also in the Jetpack Technical secondary school provides for the management of complex page logic Navigation Components ,Flutter Also launched Navigator2.0 To help us adapt to different routing scenarios .

This article comes from abroad  Bolt  Team article ( original text  https://medium.com/flutter-community/navigation-done-right-a-case-for-hierarchical-routing-with-flutter-ca0aac1275ad ), Summed up their team building Flutter When applied , Some ideas and solutions for managing routing pages , I think it's worth learning from , With the consent of the author , Combined with my own understanding of translation published .

notes : This article is not based on Flutter Navigator2.0.

Break the coupling between single pages

If we need to develop an app , You need to fill in a series of personal information before you can continue , So we need to develop a series of different pages for users to fill in different information , Like hobbies 、 Location 、 Personal signature and so on , After users fill in these information and click Submit, they also need to call the interface to pass the data to the back end .

The easiest way to do this is to put one on each page “ continue ” Button , The user clicks and triggers the routing operation , as follows :

dart

onPressed: () {
  finalResult.setInput(_getCollectedInput());
  Navigator.push(
    context,
    MaterialPageRoute(builder: (context) => NextPage(finalResult)),
  );
}

Click here , When it comes to the last form page , You can submit the final result . here , How the user input data is transferred between routes is the second , We can temporarily store in memory somewhere , What's more important is how to perform the following operations after a page performs its duties .

There is an interesting scene , If we want to continue to develop an entry page that allows users to edit this information , We're going to redevelop a series of editing pages , Or reuse the logic before , Edit the information step by step ?

Obviously , If the user only wants to change a user name , And if you have to go through this whole series of processes , It's going to have a huge impact on the experience , therefore , We can reuse the previous page as shown in the figure below UI And can modify each item of information individually , When in edit mode , You can click on the “ preservation ” Update relevant information directly :

here , You can modify the callback function by clicking the button , As shown below , Determine whether you are currently in edit mode :

dart

onPressed: () {
  final input = _getCollectedInput();
  if (_isInEditMode) {
      _updateProfile(input);
  } else {
      _navigateToNextScreen();
  }
}

Although the function is realized , But in this simple example , The code is already a little bloated , In the actual project , We may also handle more routing related operations , This way of responding to the user's operation is not very particular .

here , Each page coincides with the next page , And each page is forced to rely on the type of data it needs to submit , In large projects , We usually need to reduce this coupling as much as possible 、 rely on , And the same kind of behavior can be extracted separately and put together .

in addition , If we want to adjust the sequence of steps in the future , Introduce new steps , There will be a lot of changes to the original code ; And if there are similar types of information ( If you fill in the user name 、 Personalized signature pages contain only one title Text An input box TextField, You can only write a different UI Components , such , In terms of code reusability and extensibility , It doesn't make sense .

therefore , In this paper, we will focus on how to decouple routing related operations from other business logic , Reduce dependence .

Abstract exit point

A simple solution can break this tight coupling of individual screens . This approach needs to be based on , We've determined what to do next when the current page performs its duties , Then the exit point is abstracted out .

First , We can write an abstract class LocationInputScreenListener, This class is dedicated to listening to   Fill in the position page   Related routing operations in , Abstract the operation of what it should do next after it performs its duties :

dart

abstract class LocationInputScreenListener {

  void onLocationEntered(LocationModel input);

  void onBackPressed();
}

after , We can use the way of interface to handle the event of page Jump , As shown below :

dart

onPressed: () {
  final input = _getCollectedInput();
  _getListener().onLocationEntered(input);
}

such , In addition to relying on LocationInputScreenListener Outside , It's like a completely independent individual .

that , How components get this Listener, Of course, we can pass it layer by layer through the constructor , A better way is to use Flutter The hereditability of states in , because   Open the page / Change page state   The operation of is entirely made up of the upper components   decision / Trigger   Of , therefore , We can put some Listener Put it in an ancestor node , then , Used in child components  context.findAncestorStateOfType  In getting it .

such , We can use the following ways for LocationInputScreenListener Empower , Make it a component state :

dart

abstract class LocationInputScreenListener<T extends StatefulWidget> implements State<T> {

  void onLocationEntered(LocationModel input);

  void onBackPressed();
}

As shown in the following code ,AncestorState Realized LocationInputScreenListener after , We can use... In the subcomponents  context.findAncestorStateOfType<LocationInputScreenListener>  Find the state object directly , And use one of the methods :

dart

class AncestorState extends State<AncestorWidget>
    implements
        LocationInputScreenListener<AncestorWidget>

such , This interface becomes the established rule of page routing , If you want to perform some routing operations, you need to implement the relevant interface . Usually , The interface can be implemented by the direct parent component of the component , You can also implement multiple interfaces to respond to events of different parents , The sample application contains an example :LoggedInFlowController (https://github.com/yarolegovich/flutter_navigation/blob/master/lib/root/loggedin/logged_in_flow_controller.dart#L12) Implemented in Information editing events (OnEditProfileClickedListener) and  RootState (https://github.com/yarolegovich/flutter_navigation/blob/master/lib/root/root.dart#L18) To deal with the  Logout event (OnLoggedOutListener), Both events were caused by profile Page response .

dart

class _ProfilePageState extends LifecycleAwareState<ProfilePage> {

  Widget _buildEditProfileButton() {
    return DesignClickableText(
        text: 'Edit profile',
        onPressed: () => _editClickListener().onEditProfileClicked(),
        textStyle: Design.textCaption(color: Design.colorPrimary.shade900, bold: true),
        padding: EdgeInsets.all(8)
    );
  }
  
  Widget _buildLogOutButton() {
    return DesignHorizontalMargin(DesignDangerButton(
        text: 'Log out',
        onPressed: () => _logOutListener().onLoggedOut()
    ));
  }

  OnLoggedOutListener _logOutListener() {
    return context.findAncestorStateOfType<OnLoggedOutListener>();
  }

  OnEditProfileClickedListener _editClickListener() {
    return context.findAncestorStateOfType<OnEditProfileClickedListener>();
  }
}

besides , This way of abstracting exit points also greatly simplifies project collaboration , Because the developer of a feature doesn't have to wait for the context of the feature to be presented , Just define the interface and build your own functionality , And then put it in the right place .

Process controller

Part of the logic is extracted into the unified ancestor component ,UI Presentation and routing logic may still overlap , At this time , We can go through   Flow controller (flow controller)  This design pattern solves this problem .

We can think of the application as a tree , The leaf node represents a single page , Other nodes represent abstract flows . Back to the example above , Of this application  “ Routing tree ”  It can be represented by the following diagram :

Strictly speaking , This is a rooted acyclic digraph , There is at least one reachable path from the root to any leaf node , And nodes can be reused , Show... In multiple contexts .

Modeling in this way , We can see and   Individual pages related to a single process , For every process , We can create one “ empty ” The ancestors , Its sole responsibility is to coordinate the process , For example, determine which page should be displayed at a certain time .

The biggest benefit of this model is that it can   Unify the logic of routing operations within the scope , There is a unified local management for each process , Including the order of page presentation 、 Conditions 、 data 、 Transition animation and so on . Not only does it give us a clear perspective to manage routing state , And make the code easier to expand and maintain , here , We can change the routing order according to the demand , Introduce or insert new routing pages into the process, etc .

Another big benefit is , Managing the routing stack in each control flow is much simpler than managing the entire application's routing stack , here , In the stack, there are only components related to the process , When performing some less trivial stack operations ( Such as  popUntil) when , This greatly reduces the possibility of errors and their costs .

The process controller also maintains that multiple screen pages can share some common logic or UI Components . stay Flutter In the project , My work also included keeping the logic for displaying dialog boxes and bottom navigation bars in the basic flow controller class , In order to be quick , Easy access to these components .

Implement the basic process controller BaseFlowController

below , I want to show you a more general example , Readers can use it as a basis to expand the use of .

As mentioned above , The process controller is responsible for coordinating the collaborative processes between pages ,BaseFlowController Use a basic empty stack as an example , In more complex applications , If it contains multiple stacks flow, At this time, the application can also display multiple different components at the same time .

In your own FlowController in , It should contain only multiple routing containers ( especially Navigator) And some methods that can operate container routing stack directly ( adopt key),

in addition , The process controller may also contain elements shared among multiple routes , Like the bottom navigation bar 、 Banners that pop up notices, etc , This kind of situation will not be considered in this paper , Leave it to the reader to practice on their own .

FlowControllerState Part of the code is as follows , You can go to GitHub(https://github.com/yarolegovich/flutter_navigation)  Check out the full code :

dart

abstract class FlowControllerState<T extends StatefulWidget> extends State<T> {

  GlobalKey<NavigatorState> _navKey;
  RouteObserver _routeObserver;
  List<String> _navStack;

  @override
  void initState() {
    super.initState();
    _navStack = [];
    _navKey = GlobalObjectKey<NavigatorState>(this);
    _routeObserver = RouteObserver();
  }

  AppPage createInitialPage();

  @override
  Widget build(BuildContext context) {
    return Navigator(
        key: _navKey,
        observers: [_routeObserver],
        onGenerateRoute: (s) {
          AppPage page = createInitialPage();
          _navStack.add(page.name);
          return _buildRoute((s) => page.widget, page.name);
        });
  }

  Route<R> _buildRoute<R>(WidgetBuilder builder, String name) {
    return CupertinoPageRoute(
        builder: builder, 
        settings: RouteSettings(name: name)
    );
  }
}

The above code shows the basic functions of the process controller , Let's continue to explore the specific implementation process .

Expand Navigator The function of

_navStack, Optional , Save the current routing state , With it, we can do a lot of native for the current routing state Navigator There is no specific function provided , If the following methods are provided :

dart

bool containsChild(String routeName) => _navStack.any((element) => element == routeName);

bool isDisplayed(String routeName) => _navStack.last == routeName;

Hide the implementation

_navKey, To access and manipulate the navigator (Navigator), You should avoid exposing it directly to subcomponents , Instead, it provides ways to update the State , In the following code pop、push etc. :

dart

void pushSimple(Widget Function() builder, String name) {
  push(_buildRoute((c) => builder(), name));
}

void pop<T>({T result}) {
  _navStack.removeLast();
  _navigator().pop(result);
}

Future<R> push<R>(Route<R> route) {
  assert(route.settings.name != null);
  _navStack.add(route.settings.name);
  return _navigator().push(route);
}

void popUntilFound(String name) {
  _navigator().popUntil((route) {
    final willPop = route.settings.name != name;
    if (willPop) _navStack.removeLast();
    return !willPop;
  });
}

such , We can easily add Log And more extra general functions , And guarantee in the abstract layer  _navStack  The correctness of the State .

Life cycle perception

_routeObserver  stay FlowController Not used in , But it's good for implementing states that are sensitive to lifecycle States , We can perform some visibility operations based on these States , For example, turn polling on and off when the application returns in the background :

dart

abstract class LifecycleAwareState<T extends StatefulWidget> extends State<T> with WidgetsBindingObserver, RouteAware {

  RouteObserver _routeObserver;
  void onResumed();

  void onPaused();

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    _isResumed = true;
    _isAppInFg = true;
    _isCovered = false;
    onResumed();
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();

    _unsubscribeFromStates();
    _routeObserver = _flowController()?.routeObserver();
    _routeObserver?.subscribe(this, ModalRoute.of(context));
  }

  @override
  void dispose() {
    super.dispose();

    _unsubscribeFromStates();
    WidgetsBinding.instance.removeObserver(this);
  }

  void _unsubscribeFromStates() {
    _routeObserver?.unsubscribe(this);
    _routeObserver = null;
  }

  FlowControllerState _flowController() => context.findAncestorStateOfType<FlowControllerState>();
}

In the example application in this article , I'll use it to Update profile page status https://github.com/yarolegovich/flutter_navigation/blob/master/lib/root/loggedin/home/profile/profile_page.dart#L93), here , When the user changes his personal information and returns from the edit page , The user can then see the latest data .

dart

@override
void onResumed() {
  setState(() {});
}

Handle the return button

Last , And the trickiest part is dealing with the return button . If we just  Navigator  Package to  WillPopScope  In the component , So the top one widget All return events will be received , And ignore the process controllers below .

in addition ,findAncestorStateOfType  Very efficient , Because in the worst case , The number of nodes it accesses is equal to the height of the component tree , However , If we pass an event that returns a button from the upper node to the lower level , Find the right consumer , In the worst case, you need to traverse the nodes of the whole tree .

therefore , To avoid that , We can use only one  WillPopScope  And a list of status to listen for the return button event . The process controller itself can register and log off ,WillPopScope  The container is responsible for distributing event scheduling to each registered component , Here's the code :

dart

abstract class PopScopeHost<T extends StatefulWidget> implements State<T> {

  List<BackPressHandler> _backPressHandlers = [];

  Future<bool> onWillPop() async {
    for (int i = _backPressHandlers.length - 1; i >= 0; i--) {
      if (!_backPressHandlers[i].mounted) continue;
      if (_backPressHandlers[i].handleBackPressed()) {
        return false;
      }
    }
    return true;
  }

  static PopScopeHostSubscription subscribe(BuildContext ctx, BackPressHandler handler) {
    final host = ctx.findAncestorStateOfType<PopScopeHost>();
    host.addBackPressHandler(handler);
    return PopScopeHostSubscription(host, handler);
  }
}

class PopScopeHostSubscription {
  PopScopeHost _host;
  BackPressHandler _handler;

  PopScopeHostSubscription(this._host, this._handler);

  void dispose() {
    _host?.removeBackPressHandler(_handler);
    _host = null;
  }
}

The lower , We can consume this event in the process controller :

dart

@override
void didChangeDependencies() {
  super.didChangeDependencies();
  _popScopeHostSubscription?.dispose();
  _popScopeHostSubscription = PopScopeHost.subscribe(context, this);
}

@override
void dispose() {
  _popScopeHostSubscription?.dispose();
  super.dispose();
}

such , The state object of the root component is mixed with  PopScopeHost , And will  onWillPop  Method passed to WillPopScope after , The function of event distribution can be realized completely :

dart

class RootState extends State<RootPage> with PopScopeHost<RootPage> {
  @override
  Widget build(BuildContext context) {
    return WillPopScope(
        onWillPop: onWillPop,
        child: _isLoggedIn ? 
            LoggedInFlowController() : 
            LoggedOutFlowController()
    );
  }
}

Implement process controller

Last , We use ProfileSetupController For example , Take a look at how to create your own process controller using the above abstract classes , as follows :

dart

class _ProfileSetupControllerState extends FlowControllerState<ProfileSetupController> 
  implements 
      HobbyCategoryPageListener<ProfileSetupController>, 
      HobbyPageListener<ProfileSetupController>, 
      LanguagesPageListener<ProfileSetupController>, 
      LocationPageListener<ProfileSetupController> {
  
  List<Hobby> _selectedHobbies;
  LocationModel _enteredLocation;

  @override
  AppPage createInitialPage() => AppPage(_PAGE_HOBBY_CATEGORY, _createHobbyCategoryPage());

  @override
  void onHobbyCategorySelected(HobbyCategory category) {
    pushSimple(() => _createHobbyPage(category.hobbies), _PAGE_HOBBY);
  }

  @override
  void onHobbiesSelected(List<Hobby> hobbies) {
    _selectedHobbies = hobbies;
    pushSimple(() => _createLocationPage(), _PAGE_LOCATION);
  }

  @override
  void onLocationEntered(LocationModel location) {
    _enteredLocation = location;
    pushSimple(() => _createLanguagesPage(), _PAGE_LANGUAGES);
  }

  @override
  void onLanguagesSelected(List<LanguageModel> languages) {
    final repo = UserRepository.get();
    final user = repo.createNewUser(_selectedHobbies, _enteredLocation, languages);
    _listener().onProfileSetupComplete(user);
  }

  ProfileSetupFlowListener _listener() {
    return context.findAncestorStateOfType<ProfileSetupFlowListener>();
  }
}

here , It's like a lot of architecture books , This way of code is fully embodied   High cohesion and low coupling , The logic related to the routing operation is changed from UI The components are separated .

EditProfileFlowController It's a more complex case ( Information editing page ), At this point, the program needs to deal with such as updating data , Clear the page and so on ( For the complete code, see :https://github.com/yarolegovich/flutter_navigation/blob/master/lib/root/loggedin/editprofile/profile_edit_flow_controller.dart#L22).

For a complete sample project code, see :https://github.com/yarolegovich/flutter_navigation

summary

Bolt The team's article was published in Navigator2.0 Before appearance , There are many similarities between them , After practice , This kind of solution also proved to be able to help them enhance the scalability of the application , Adapt to the new needs of continuous development .

版权声明
本文为[osc_ 0kdyznad]所创,转载请带上原文链接,感谢
https://chowdera.com/2020/12/20201209104358125w.html

随机推荐