Unified Application Framework for Larger-scale Apps: Alibaba’s Fish Redux Moves to Open Source
By Wu Jifeng from Xianyu Tech
During its early stages, Alibaba’s Xianyu( 闲鱼) second-hand trading platform relied heavily on the Flutter mobile app SDK for its development. Developers encountered a number of persistent difficulties, especially strong business code coupling and poor code maintainability. To support its business scenarios, Xianyu needed a unified application framework capable of solving development dilemmas.
As a high-level assembled Flutter application framework based on Redux data management, Fish Redux offered an ideal solution to Xianyu’s early problems, proving especially well-suited to medium and large-scale complex applications. With the primary feature of configurable assembly, it is easy to write in, easy to maintain, and makes collaboration easy. Inspired by renowned frameworks including Redux, React, Elm, and Dva, Fish Redux improved on Redux’s focus, division, reuse, and isolation functions.
As it is soon to be open sourced, this article looks in detail at Fish Redux’s architecture, components, dictionary, and other mechanisms to illustrate its value for application development.
Architecture Overview of Fish Redux
As the following diagram shows, Fish Redux is divided into three layers from the bottom up.
Each layer is used to solve problems and resolve contradictions at a specific level. The following sections discuss these layers in detail.
Redux is a state management framework created by the front-end development community that is predictable, centralized, easy to debug, and flexible. Redux handles all operations such as adding, deleting, revising, and querying data.
In terms of design and implementation, Redux is a functional state management framework. Traditionally, object-oriented programming (OOP) deals with state management using beans, each bean exposing several public APIs to manipulate internal data (hyperemia model). The functional approach is more abstract. Data is defined as struct (anemic model) and the method of manipulating data is unified into a reducer with the same function signature:
(T, Action) => T. FP: Struct (anemia model) + reducer = OOP: bean (hyperemia model).
Meanwhile, Redux adds the middleware (AOP) mode and “subscribe” mechanism commonly used in FP, which gives the framework strong flexibility and scalability. (More information on the anemia and hyperemia models is available on Wikipedia.
The original Redux has a number of disadvantages. The first is the contradiction between Redux’s centralization and its division of components. Second, Redux’s reducer requires manual assembly layer by layer, which is cumbersome and prone to errors.
Fish Redux does away with these issues. It is only related with data management, regardless of specific use scenarios, offering centralized, observable data management through Redux. Beyond this, it offers better and higher abstraction than the original Redux in terms of usage in lateral development scenarios for the client-end Flutter page.
Each component is made up of a data point (struct) and reducer. At the same time, there is a parent-child dependent relationship between the different components. Fish Redux uses this dependent relationship to resolve the conflict between centralization and partitioning. Fish Redux also transforms the reducer’s layer-by-layer manually combine function to provide automatic framework completion, significantly simplifying Redux usage. Finally, this helps achieve the results of centralization while using partitioned code.
The concepts of state, action, reducer, store, and middleware mentioned above are consistent with the community’s ReduxJS, retaining all of the original Redux’s advantages. More information on Redux is available at the Redux Github page.
Components are packages for local presentations and functions. Based on Redux’s principles, functions are subdivided into two kinds, one that modifies data (reducers) and another that does not (side-effects). The result contains three parts: 1) the view; 2) the effect; and 3) the reducer, which are called the three elements of a component. They are responsible for the display of the component, the behavior of non-modifying data, and the behavior of modifying data, respectively.
This subdivision is oriented both to the present and to the future. The current Redux regards subdivision as “data management” and “other”, while future-oriented UI-automation regards subdivision as “UI expression” and “other”. UI expression is now about to enter a black box era as far as programmers are concerned, leading R&D engineers to increasingly focus on the behavior of non-modifying data and modifying data.
Components are the division of views and the division of data. Through layer-by-layer division, complex pages and data are divided into small modules that are independent of each other, facilitating collaborative development within teams.
1. The view
The view is simply a function signature: (T,Dispatch,ViewService) => Widget. It primarily contains information with three characteristics.
First, the view is completely data-driven.
Second, event/callback generated by the view is issued with “intentions” through the dispatch without a specific implementation.
Lastly, component dependencies are needed with it, which are called in a standardized manner, for example through ViewService (in a typical view signiature-compliant function).
2. The effect
As a standard definition for non-modifying data behavior, the effect is a function signature: (Context, Action) => Object. It primarily contains information with four characteristics.
First, the effect receives “intentions” from the view, including the corresponding lifecycle callback, and then makes a specific execution.
Second, its processing may be an asynchronous function, and the data may be modified in the process. This means that data should not be held; rather, the latest data should be obtained through the context.
Third, it does not modify data, and if necessary should send an action to the reducer for processing.
Lastly, its return value is limited to bool or future, which corresponds to the processing flow that supports synchronization functions and coroutines (such as good coroutine support).
3. The reducer
The reducer is a function signature that fully conforms to the Redux specification: (T, Action) => T. The following shows a signature-compliant reducer:
Meanwhile, the widgets and adapters that large components depend on are registered in an explicit configuration; this dependency configuration is called dependencies.
The following formula results: component = view + effect (optional) + reducer (optional) + dependencies (optional). A typical assembly is shown below:
Through abstraction of the component, complete division, multi-latitude reuse, and better decoupling are achieved.
The adapter is also a package for local presentations and functions. It was created for the high-performance scenarios of ListView and is a change in the component implementation aimed to solve three problems the component model faces in flutter-ListView scenarios.
The first problem is that putting a “Big-Cell” in the component means being unable to enjoy the ListView code’s performance optimization.
Second, the component cannot distinguish between appear/disappear and init/dispose.
Finally, the coupling between the effect’s life cycle and the view does not meet the intuitive expectations present in ListView scenarios. In short, what is needed is an abstraction of local presentation and functional encapsulation that is a ScrollView in terms of logic and a ListView in terms of performance. Such a separate layer of abstraction is only considered in terms of its actual effect. The framework is not used for the page, and is instead used for the component and for the component + adapter baseline performance comparison.
The reducer is long-lived, while the effect’s lifespan is in middle-range and the view is short-lived. Constant testing is used to make comparisons, as in the following example with an android device:
Before using the framework, the baseline FPS in the details page is at 52FPS. With the use of the framework in the case of using only the component abstraction, the FPS drops to 40 and encounters the “Big-Cell” trap. With the use of the framework as well as the adapter abstraction, the FPS rises to 53, which is above the baseline and makes a small improvement.
The recommended directory structure in Fish Redux is as shown as follows:
The upper layer is responsible for assembly, while the lower layer is responsible for implementation. Meanwhile, a plug-in is provided to be quickly filled out. The following scenario from the Xianyu platform offers an example of the assembly:
Here, there is complete independence between components and other components and between components and containers.
Fish Redux’s communication mechanism provides for component communications within adaptor and for component communications between adaptors.
In the above, a broadcast clip with priority processing is used. The issued action will first be self-processed, or it will be broadcast to other components and Redux for processing. Finally, all communication requests within components and between components (parent-child, child-parent, sibling-sibling, and so on) will be completed through a simple, intuitive dispatch.
Fish Redux’s refresh mechanism provides both data refreshing and view refreshing.
Data refreshing involves local data modification and layer-by-layer data copying.
In local data modification, a shallow copy of the upper layer data is automatically triggered layer-by-layer. This data is transparent to the upper layer business code.
Layer-by-layer data copying is on the one hand strictly compliant with Redux’s data modifications, and on the other is strictly compliant with data-driven representation.
View refreshing sends all flattened notifications to all components, while components determine whether they need to be refreshed through shouldUpdate.
Key Advantages of Fish Redux
Fish Redux’s first advantage is its centralized, observable management of data through Redux, in which all of the original advantages of Redux are retained. During reducer merging, Redux operations are transformed to be completed automatically by the framework, greatly simplifying the more tedious aspects of using Redux.
Secondly, it excels in the division management of components. Components exist not only as divisions of views, but also as divisions of data. By dividing complex pages and data layer-by-layer, Fish redux divides these into independent small modules, enabling collaborative development within teams.
Third, it provides isolation between the view, effect, and reducer, splitting components into three stateless, non-dependent functions. Because these are stateless functions, they are easier to write, debug, test, and maintain. Meanwhile, this brings greater possibility for combination, reuse, and innovation.
Fourth, it offers declarative configuration assemblies. Components and adapters are assembled through a free configuration, including components’ view, reducer, effect, and child-relationships it depends on.
Fifth, it offers strong scalability. The core framework maintains its own three-tier core focuses which it is concerned with alone, meanwhile maintaining flexible scalability for the upper layer. The framework does not have a single line of printed code, but data flow and component changes can still be observed through standard middleware. Beyond the framework’s three core layers, mixins can also be added to the component and adapter layers by “dart” to flexibly enhance their customization and capabilities of their upper-level applications. Further, the framework is connected to the SDK. There is no barrier between the SDK and the framework, and they are freely assembled by the upper layer.
Finally, Fish Redux is simple and easy to use, requiring only about 1000 lines of code. After a few small functions and assembly tasks the code is finished running, and then it is ready for complete use.
The value of Fish Redux is well demonstrated in the development of Xianyu. Fish Redux offers a unified application framework capable of solving persistent dilemmas.