Flutter Analysis and Practice: App Framework Design Practices

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.

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.

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.
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.

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.

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.

6) Small, simple, and complete

  • Fish Redux is very small and contains only about 1,000 lines of code.

Original Source:

Follow me to keep abreast with the latest technology news, industry insights, and developer trends.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store