Flutter Analysis and Practice: App Framework Design Practices

Alibaba Cloud
10 min readSep 22, 2020

--

The open-source Fish Redux of Xianyu on GitHub is an assembled Flutter app framework based on Redux data management. It is especially suitable for building large- and medium-sized complex apps. It features the functional programming model, predictable status management, pluggable component system, and best performance. This article describes the features and usage of Fish Redux.

3.1.1 Background and Technical Architecture of Open-Source Fish Redux

At the beginning of Flutter connection, Xianyu’s businesses were complex, mainly in two aspects:

  • Statuses of a page need to be managed in a centralized manner. Different components of a page share the same data source. If the data source changes, all components of the page need to be notified of the change.
  • UIs on a page can be displayed in many forms, such as normal details, Xianyu coin details, community details, and auction details, resulting in a heavy workload. UI components must be reused as much as possible, which requires proper division of components.

When trying Redux and BLOC frameworks, Xianyu found that no framework could solve both the centralized status management and UI componentization issues due to the contradiction between these two issues. To use one framework to solve these issues, Xianyu developed Fish Redux.

Fish Redux has gone through three major iterations and has also been intensively discussed and considered by the team.

The first version was modified based on Flutter_Redux in the community. The core of this version was componentization of UI code. However, for complex details and service publishing, a lot of business logic was used, making it impossible to componentize the logic code.

The second version was significantly modified. Although it solved the problem of separate governance on UI code and logic code, it broke the principle of Redux. This was unacceptable for the Xianyu team, which strives for excellence.

The third version was refactored, during which Xianyu established the overall architectural principles and layered requirements. On the one hand, Redux is implemented on the Flutter side according to ReduxJS code, which completely preserves the principle of Redux. On the other hand, components on Redux are encapsulated. In addition, the capability of separately governing business code is innovatively provided through architectural design at this layer.

In this way, Xianyu has completed the basic design of Fish Redux. In subsequent apps, Xianyu detected the code performance problem after business assembly. Xianyu provided a solution to solve this problem in long-list scenarios. Currently, Fish Redux is running stably online.

3.1.2 Analysis of Fish Redux Technology

As shown in Figure 3–1, Fish Redux is divided into two layers from the bottom up.

3.1.2.1 Redux

1) What can Redux do?

Redux is a centralized and flexible data management framework that is easy to debug and can be used for making predictions. Redux handles all operations, such as adding, deleting, modifying, and querying data.

2) How is Redux designed and implemented?

Traditionally, object-oriented programming (OOP) manages data by using beans, each bean exposing several public APIs to manipulate internal data.

In the functional approach, data is defined as structs (anemic model) and the method of manipulating data is unified into a reducer with the same function signature.

(T, Action) => T:
FP: Struct + Reducer = OOP:bean (hyperemia model)

Redux includes the middleware, (aspect-oriented programming (AOP) mode and “subscribe” mechanism commonly used in FP. This gives the framework strong flexibility and scalability.

Figure 3–1

3) Disadvantages of Redux

Redux is only concerned with data management but not with its scenarios, which is both an advantage and disadvantage of Redux.

Xianyu encounters two specific problems when using Redux:

  • The first is the contradiction between Redux’s centralization and its division of components.
  • Second, Redux’s reducer must be manually assembled layer by layer, which is cumbersome and error-prone.

4) Fish Redux Improvement

Fish Redux performs centralized and observable data management through Redux. In addition, it offers better and higher abstraction than Redux in terms of usage in development scenarios for the Flutter pages on the client side.

Each component consists of a data point (struct) and a reducer. There is a parent-child dependent relationship between different components. Fish Redux uses this dependent relationship to resolve the contradiction between centralization and division of components. Fish Redux also enables the reducer to be automatically assembled by the framework. This greatly simplifies Redux usage. In this way, Xianyu implements centralization and division of code.

5) Alignment with Community Standards

The concepts of state, action, reducer, store, and middleware mentioned previously are consistent with the community’s ReduxJS. Xianyu retains all of Redux’s advantages.

3.1.2.2 Components

Components involve local presentation and function encapsulation. Based on Redux’s principles, functions are subdivided into data modification functions (reducers) and functions other than data modification (effects.)

View, effect, and reducer are the three elements of a component. They are responsible for component presentation, non-modification behavior, and modification behavior, respectively.

This subdivision is oriented to the present and the future. The current Redux regards this subdivision as “data management” and “other,” while future-oriented UI-automation regards this subdivision as “UI expression” and “other.”

UI expression is about to enter a black box era and R&D engineers will focus more on modification and non-modification behavior.

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) View

The view is simply a function signature: (T,Dispatch,ViewService) => Widget. It contains information with three characteristics.

  • First, the view is completely data-driven.
  • Second, for a view-triggered event or callback, an “intention” is sent through the dispatch without a specific implementation.
  • Lastly, required component dependencies are called in a standardized manner through ViewService, for example, in a typical view signature-compliant function.
Widget buildView(PageState state, Dispatch dispatch, ViewService viewService) {
final ListAdapter adapter = viewService.buildAdapter();
return Scaffold(
appBar: AppBar(
backgroundColor: state.themeColor,
title: const Text('ToDoList'),
),
body: Container(
child: Column(
children: <Widget>[
viewService.buildComponent('report'),
Expanded(
child: ListView.builder(
itemBuilder: adapter.itemBuilder,
itemCount: adapter.itemCount))
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => dispatch(PageActionCreator.onAddAction()),
tooltip: 'Add',
child: const Icon(Icons.add),
),
);
}

2) Effect

As a standard definition for non-modification behavior, the effect is a function signature: (Context, Action) => Object. It contains information with four characteristics.

  • First, the effect receives “intentions” from the view, including the lifecycle callback, and makes a specific execution.
  • Second, its processing may be an asynchronous function and data may be modified during processing. Therefore, Xianyu prefers to obtain the latest data through the context rather than holding data.
  • Third, it does not modify data and an action should be sent to the reducer if data needs to be modified.
  • Lastly, its return value is limited to bool or future, which corresponds to the processing flow that supports synchronization functions and coroutines.

The following shows an example of good coroutine support.

void _onRemove(Action action, Context<ToDoState> ctx) async {
final String select = await showDialog<String>(
context: ctx.context,
builder: (BuildContext buildContext) {
return AlertDialog(
title: Text('Are you sure to delete "${ctx.state.title}"?'),
actions: <Widget>[
GestureDetector(
child: const Text(
'Cancel',
style: TextStyle(fontSize: 16.0),
),
onTap: () => Navigator.of(buildContext).pop(),
),
GestureDetector(
child: const Text('Yes', style: TextStyle(fontSize: 16.0)),
onTap: () => Navigator.of(buildContext).pop('Yes'),
)
],
);
});
if (select == 'Yes') {
ctx.dispatch(ToDoActionCreator.removeAction(ctx.state.uniqueId));
}
}

3) Reducer

A reducer is a Redux-compliant function signature: (T, Action) => T. The following shows a signature-compliant reducer.

PageState _initToDosReducer(PageState state, Action action) {
final List<ToDoState> toDos = action.payload ?? <ToDoState>[];
final PageState newState = state.clone();
newState.toDos = toDos;
return newState;
}

The widgets and adapters that large components depend on are registered through explicit configuration. This dependency configuration is called dependencies.

Therefore, Component = View + Effect (optional) + Reducer (optional) + Dependencies (optional).

The following shows a typical assembly example.

class ToDoListPage extends Page<PageState, Map<String, dynamic>> {
ToDoListPage()
: super(
initState: initState,
effect: buildEffect(),
reducer: buildReducer(),
view: buildView,
dependencies: Dependencies<PageState>(
adapter: NoneConn<PageState>() + ToDoListAdapter(),
slots: <String, Dependent<PageState>>{
'report': ReportConnector() + ReportComponent()
}),
);
}

Through abstraction of a component, complete division, multi-dimension reuse, and better decoupling are achieved.

3.1.2.3 Adapter

The adapter also involves local presentation and function encapsulation. It is created for the high-performance scenarios of ListView and is a change in the component implementation.

The adapter aims to solve three problems the component model faces in Flutter-ListView scenarios.

1) Putting a “Big-Cell” in the component makes it impossible to enjoy the ListView code's performance optimization.
2) The component cannot distinguish between appear/disappear and init/dispose.
3) The coupling between the effect's lifecycle and the view does not meet the intuitive expectations in ListView scenarios.

In short, what is needed is an abstraction of local presentation and function encapsulation that is logically a ScrollView but functionally a ListView. Xianyu compares the performance when no framework is used for the page and when the framework and adapter are used.

Reducer is long-lived, Effect is medium-lived, View is short-lived.

Testing with an Android device obtains the following results:

  • Before the framework is used, the baseline frames per second (FPS) in the details page is 52.
  • When the framework is used with the component abstraction, the FPS drops to 40 and encounters the “Big-Cell” trap.
  • When the framework is used with the adapter abstraction, the FPS slightly increases to 53.

3.1.2.4 Directory

The recommended directory structure in Fish Redux:

sample_page
-- action.dart
-- page.dart
-- view.dart
-- effect.dart
-- reducer.dart
-- state.dart
components
sample_component
-- action.dart
-- component.dart
-- view.dart
-- effect.dart
-- reducer.dart
-- state.dart

The upper layer is responsible for assembly, while the lower layer is responsible for implementation. A plug-in is provided for quick filling. Figure 3–2 shows an example of the assembly in a details page scenario.

Figure 3–2
class DetailPage extends Page<RentalDetailState, DetailParams> {
DetailPage()
: super(
initState: initRentalDetailState,
view: buildMainView,
reducer: asReducer(RentalDetailReducerBuilder.buildMap()),
dependencies: Dependencies<RentalDetailState>(
slots: <String, Dependent<RentalDetailState>>{
'appBar': CommonBuyAppBarConnector() + AppBarComponent(),
'body': CommonBuyItemBodyConnector() +
RentalBodyComponent(slots: <Dependent<RentalBodyState>>[
galleryConnector() + RentalGalleryComponent(),
priceConnector() + RentalPriceComponent(),
titleConnector() + RentalTitleComponent(),
bannerConnector() + RentalBannerComponent(),
detailTitleConnector() + RentalDetailTitleComponent(),
equipmentConnector() + RentalEquipmentComponent(),
descConnector() + RentalDescComponent(),
tagConnector() + RentalTagComponent(),
itemImageConnector() + RentalImageComponent(),
marketConnector() + RentalMarketComponent(),
channelConnector() + RentalChannelComponent(),
productConnector() + ProductParamComponent(),
recommendConnector() + RentalRecommendAdapter(),
paddingConnector() + PaddingComponent(),
]),
'bottomBar':
CommonBuyBottomBarConnector() + RentalBottomBarComponent(),
},
),
);
}

There is complete independence between components and between components and containers.

3.1.2.5 Communication Mechanism

Figure 3–3 shows intra-component communication and inter-component communication.

A prioritized broadcast clip (self-first-broadcast) is used for dispatch communication APIs. An issued action will first be self-processed, or it will be broadcast to other components and Redux for processing. Finally, all communication requirements inside components and between components (such as parent-child, child-parent, and sibling-sibling components) are implemented through a simple and intuitive dispatch.

Figure 3–3

3.1.2.6 Refresh Mechanism

1) Data Refreshing

In local data modification, a shallow copy of the upper layer data is automatically triggered layer by layer, as shown in Figure 3–4. This is transparent to the upper layer business code. Layer-by-layer data copying strictly complies with Redux’s data modification and data-driven presentation.

Figure 3–4

2) View Refreshing

Notifications are sent to all components, and components determine whether they need to refresh the view through shouldUpdate, as shown in Figure 3-5.

Figure 3–5

3.1.2.7 Key Advantages of Fish Redux

1) Centralized Data Management

Fish Redux performs centralized and observable data management through Redux, in which all the advantages of Redux are retained. In addition, the reducer is assembled automatically by the framework, greatly simplifying the use of Redux.

2) Division Management of Components

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.

3) Isolation Between the View, Effect, and Reducer

A component is split into three stateless and independent functions. These functions are easy to write, debug, test, and maintain due to their statelessness. This also brings greater possibilities for combination, reuse, and innovation.

4) Declarative Configuration Assemblies

Components and adapters are assembled through free and declarative configuration, including components’ view, reducer, effect, and dependent child-relationships.

5) Strong Scalability

The core framework only deals with its core focuses while maintaining flexible scalability for the upper layer.

  • The framework does not have a single line of printed code, but data flows and component changes can still be observed through standard middleware.
  • In addition, mixins can be added to the component and adapter layers through Dart code, to flexibly enhance their customizability and capabilities at the upper layer.
  • The framework communicates with other middlewares, such as automatic exposure and high availability. There is no barrier between each middleware and the framework, and they are freely assembled by the upper layer.

6) Small, simple, and complete

  • Fish Redux is very small and contains only about 1,000 lines of code.
  • It is easy to use, and only a few small functions are required to complete the assembly before running.
  • Fish Redux provides a variety of functions.

Original Source:

--

--

Alibaba Cloud

Follow me to keep abreast with the latest technology news, industry insights, and developer trends. Alibaba Cloud website:https://www.alibabacloud.com