Author: Idle fish technique – artisan repair

The Design of the Flutter Widget is inspired by React and is a native responsive UI framework. Based on the characteristics of Flutter, this paper tries to discuss our thinking and practice of Flutter React programming paradigm combined with the engineering application of Flutter.

The birth of the Reactive

When you talk about UI, you always talk about MVC. It was very early, before the event-driven (message loop) model that is widely used in modern GUIs, so OVER time, MVC has evolved and been redefined. MVC is a very broad concept by now. Using basic MVC as a framework for development is prone to fuzzy module responsibility boundaries and confusion of logical invocation direction. After the evolution of GUI framework, the distribution and processing of user events are integrated into View module, thus MVP appears. The responsibilities of MVP are clearly divided and the direction of logical invocation is easy to grasp, but it is very tedious and the development efficiency is not high. Then with the development of Web, markup language is applied to interface description, logical interface separation and stateless interface began to appear, MVVM came into being. MVVM lets the architectural layer provide two-way binding between data and View, which eases development effort but sometimes introduces a degree of state chaos. Functional programming has seen a resurgence in recent years, leading to responsive interface development, a throwback to the GUI event-driven model.

Personal understanding of front-end architecture iteration:

From the perspective of the iterative process, Model and View are two relatively fixed roles, which are easy to understand and can well determine the responsibility boundary. How to communicate the Model and View is the key to architectural design, and the general approach of responsiveness is to return the Model to its original event-driven mode, combined with functional data flow to drive the View refresh. Such a clear role division and simple and easy to understand the logical link, can better unified programming mode.

Reactive properties of Flutter

Usually GUI frameworks have something in common, such as a tree hierarchy of views, message loops, Vsync signal refreshes, etc. Flutter also inherits these classic designs, but Flutter does not use markup languages to describe the interface (such as HTML for the Web, XML for Android). Part of this is that Flutter is based on responsiveness. Reactive is a development model with event data streams at its core, and the UI framework provides features to support it.

1. Describe the interface, don’t manipulate it

There is a saying that functional languages differ from imperative languages in that imperative languages give instructions to computers and functional languages describe logic to computers. This thinking is reflected in the Flutter UI. Flutter does not encourage manipulation of the UI. It certainly does not provide an API for manipulating views, such as textView.settext () or button.setonclick (). The description of the interface can be digitized (similar to XML, JSON, etc.), while the operation of the interface is difficult to be digitized, which is very important. Responsiveness requires convenient and sustainable mapping of data into the interface.

This design idea may be a little unaccustomed to the developer, we can use the development of Android ListView(iOS TableView) to understand: We usually prepare a List of data, then implement an Adapter to map the items in the List into itemViews, and finally set the List and Adapter to the ListView. So when we change the data in the List, the ListView will refresh the View accordingly. Flutter, we are ready to Widgets (Widget “container” is just a Tree, not a List), Flutter will provide Adapter (RenderObjectToWidgetAdapter) map it into a RenderObject rendering, The Widget refreshes when it is updated.

In addition, widgets can also be used for cache reuse by setting a Key. Reuse of Item widgets can be very profitable in a listView-like scenario.

2. Communication based on common ancestors

In my country, if you want to connect with someone, sometimes you get into the context of “we were a family 500 years ago”. In Flutter, if two components communicate with each other, it is to find their ancestors (although it is possible that the two components themselves are genetically related). Flutter describes this as “data going up, informing down”.

However, in a very complex tree hierarchy, finding an ancestor is not easy, and performance is not good. Flutter optimizes this by providing an InheritedWidget. When an InheritedWidget inherits this type, The child can be provided by BuildContext inheritFromWidgetOfExactType method convenient to find the closest in the hierarchy of the “ancestors”. This method is optimized to be efficient and allows child and ancestor dependencies to be refreshed easily.

The concept of a controller (like an Activity in Android or a ViewController in iOS) is not promoted in Flutter. The View itself is not operable, so the controller has no meaning. Communication between components must then “take care of itself” at the View layer.

3. Functional data flow

This certainly does not belong to Flutter. To achieve the simplicity and elegance of responsiveness, we need to take advantage of the functional nature of the language. The nice thing about Flutter is that it uses the Dart language to make things very light. You don’t need to introduce any third party libraries to do this (there is the RxDart library, but it feels like an extra enhancement), and the language Api design has obviously been optimized in this direction, making it very convenient. Take a look at Stream and RxDart.

React based framework practice

Unify state management and one-way data flow

Through the practice of React, the reactive mode can solve the problem of updating data to the interface with good efficiency. React officially proposed Flux. In the face of complex business scenarios, Flutter also recommended the Redux architecture. We also built the framework based on this idea.

The first is the separation of business logic from the interface. The interface is Stateless, and we are trying to automate the interface code directly, so there is no business logic code in the Widget. When we pass a data (State) describing the current interface to the View layer, the interface should display properly. The interaction between the user and the interface will generate actions, which represent the intention of the user interaction. Actions can carry information (for example, when a user enters a message, the content of the message should be carried in the Action). Actions are input to the Store, and the Store uses registered Interrupters to pre-block the Action, either by blocking it or by rewriting one Action into another. Store then collects the corresponding bound Reducers and performs a reduce operation on the Action to generate a new State and inform the interface to refresh.

Reducer and Interrupter are normally required when creating a Store:

Store<PublishState> buildPublishStore(String itemId) {// Set the state initial value PublishState initState = new PublishState(); initState.itemId = itemId; initState.isLoading =true; / / create a Reducer and the corresponding Action binding var reducerBinder = ActionBinder. ReducerBinder < PublishState > (.). bind(PublishAction.DETAIL_LOAD_COMPLETED, _loadCompletedReducer) .. bind(PublishAction.DELETE_IMAGE, _delImageReducer) .. bind(PublishAction.ADD_IMAGE, _addImageReducer); / / create the Interrupter and corresponding Action binding var interrupterBinder = ActionBinder. InterrupterBinder < PublishState > (.). bind(PublishAction.LOAD_DETAIL, _loadDataInterrupter) .. bind(PublishAction.ADD_IMAGE, UploadInterruper.imageUploadInterrupter); / / create a Storereturn new CommonStore<PublishState>(
      name: 'Publish',
      initValue: initState,
      reducer: reducerBinder,
      interrupter: interrupterBinder);
}
Copy the code

Reducer is the logical code of actions generated when dealing with user interaction. It receives three parameters, one is the execution context, one is the Action to be processed, and one is the current State. After processing, a new State must be returned. The ideal Reducer of function should be a pure function without side effects. Obviously, we should not visit or change the local variables in Reducer, but sometimes we are dependent on the previous calculation results, so we can store some runtime data in ReduceContext. There should be no asynchronous logic in the Reducer because the Reduce operation of the Store is synchronous and the interface will be updated immediately after a new State is generated. However, updating the State asynchronously does not trigger the refresh.

PublishState _delImageReducer(ReduceContext<PublishState> ctx, Action action, PublishState state) {
  int index = action.args.deleteId;
  state.imageUplads.removeAt(index);
  return state;
}
Copy the code

Interrupter is similar to Reducer in form, except that asynchronous logical processing can be done, such as network requests, in Interrupter.

Why does Interrupter exist?

To put it another way, we can view the entire Store as a function. The input is Action and the output is State. Functions can have side effects. Sometimes we input parameters that do not necessarily have an output, such as the log function (void log(String)). If we type String, we only print a String on the standard output, and the log function returns no value. Similarly, for the Store, not all actions need to change State. Users sometimes trigger actions just to make the phone vibrate, but not to trigger interface updates. So Interrupter is what the Store uses to handle side effects. *

// block a network request Action, Action bool _onMtopReq(InterrupterContext<S> CTX, Action Action) {netService. requestLight(API: action.args.api, version: action.args.ver, params: action.args.params, success: (data) { ctx.store.dispatch(Action.obtain(Common.MTOP_RESPONSE) .. args.mtopResult ='success'. args.data = data); }, failed: (code, msg) { ctx.store.dispatch(Action.obtain(Common.MTOP_RESPONSE) .. args.mtopResult ='failed'. args.code = code .. args.msg = msg); });return true;
  }
Copy the code

Often we use an InheritedWidget at the root of the interface to hold the Store, so that any Widget on the interface can easily access and associate with the Store. This can be done by referring to Redux_demo, which will not be expanded in detail here.

Finally, a brief introduction to the implementation of the Store, which accepts the Action, then executes the Reduce, and finally provides the data source to the widget. Widgets can create a data flow based on the provided data source and refresh the interface in response to data changes. At the heart of this is Dart Stream.

. / / create a distribution of data Stream _changeController = new StreamController. Broadcast (sync:false); / / create receives the Action of Stream _dispatchController = new StreamController. Broadcast (sync:false); Sets the response Action / / _dispatchController. Stream. Listen ((Action) {_handleAction (Action); }); . // Dispatch Action void dispatch(Action Action) {_dispatchController.add(Action); } Stream<State> get onChange => _changecontroller.stream;Copy the code

The core reduce operation for an Action in the Store:

// Collect the Reducer Final List<ReduceContext<State>> Reducers = _reducers.values. Where ((CTX) => ctx._handleWhats.any((what) => what == action.what)) .toList(); // execute reduce Box<Action, State> Box = new Box<Action, State>(Action, _state); box = reducers.fold(box, (box, reducer) { box.state = reducer._onReduce(box.action, box.state);returnbox; }); _state = box. State; _changeController.add(_state);Copy the code

The Widget streams data based on the data source exposed by the Store:

Map ((state) => widget.converter(state)) // Compare the previous data, Where ((value) => (value! = latestValue) // Update interface. Listen ((value){...setState()
          ...
     })
Copy the code

Extensions of componentization

In business development, we find that sometimes one page and one Store will bring inconvenient reuse of components. For example, the video playback component is a logically cohesive component. If the reducer is concentrated in the Store of the page, it will be inconvenient for other pages to reuse this developed video component. The video component may need a separate Store to Store the logic associated with playing the video. We follow the Flutter component communication approach and extend the framework to allow multiple stores and be unaware of Widget development.

A Widget can only sense the Store owner closest to it, and the Store forwards actions to higher-level stores, and receives data changes from higher-level stores and notifies the Widget.

Extend the discussion

While the current popular MVVM framework (Vue,Angular) is capable of fine-grained binding of data and minimization of interface refresh, Flutter has not yet found a good way to implement it automatically within the framework and has to be manually handled by developers. This will inevitably reduce development efficiency and drag down development experience. We are also exploring a better way. If you are interested or have a good solution, please feel free to contact us.

When dealing with complex state pages (multi-animation, multi-view linkage), the Store should provide tools or mechanisms to manage complex state and improve development efficiency. A state machine is an option. If you have an elegant implementation or idea of a state machine framework under Dart, be sure to share it with us.

Finally, xianyu technology team is recruiting all kinds of talents, whether you are proficient in mobile terminal, front-end, background, machine learning, audio and video, automatic testing, etc., welcome to join us, with technology to improve your life!

Resume: [email protected]