41 Componentization and Platformization How to Organize a Reasonably Stable Flutter Project Structure

41 Componentization and Platformization How to Organize a Reasonably Stable Flutter Project Structure #

Hello, I’m Chen Hang. Today, let’s talk about the topic of engineering architecture in Flutter applications.

In software development, we not only need to follow common design patterns in code implementation, but also adhere to basic design principles in architecture design. Among them, the DRY principle (Don’t Repeat Yourself) can be considered the most important one.

In simple terms, the DRY principle means “do not repeat”. This is a very intuitive concept, because even the most junior developers, after writing code for a while, will unconsciously extract some commonly used repetitive code, and put it into common functions, classes, or independent component libraries to achieve code reuse.

In software development, we usually need to consider how to manage reusability (code reuse) from the perspective of architecture design, that is, how to divide functionality and decompose big problems into several relatively independent smaller problems. Among them, componentization and platformization are the most popular means of division and reuse in client development.

So today, let’s learn about the central ideas of these two types of division and reuse solutions, so that we can have a clear plan when designing the architecture of Flutter applications.

Componentization #

Componentization, also known as modularization, refers to the process of splitting a large software system (App) into multiple independent components or modules based on the purpose of reusability and focusing on separate concerns. Each individual component is a separate system that can be maintained, upgraded, or replaced independently. Components can also depend on other independent components as long as the functionality provided by the component remains unchanged, without affecting the overall functionality of other components and the software system.

Figure 1: Diagram of Componentization

As shown in the figure above, the central idea of componentization is to divide independent functionalities, and the granularity of componentization is relatively loose. An independent component can be a software package, a page, a UI control, or even a module encapsulating some functions.

The granularity of components can vary, so how can we achieve successful encapsulation and reusability of components? Which code should be included in a component? There are some basic principles, including the principles of singularity, abstraction, stability, and self-completeness.

Let’s take a closer look at what these principles mean.

The principle of singularity states that each component should provide only one functionality. The central idea of componentization is to divide and conquer. Each component has its own fixed responsibilities and clear boundaries, focusing on doing one thing well so that the component can develop properly.

A counterexample is a Common or Util component. Such components often exist because of unclear definitions or ambiguous boundaries in the code during development, leading to the thinking of “Oh, this piece of code doesn’t seem to fit anywhere, so let’s put it in Common (Util)”. Over time, these components become neglected junkyards. Therefore, when encountering code with uncertainty about where it should be placed, it is necessary to rethink the design and responsibilities of the components.

The principle of abstraction means that the functionality provided by a component should be abstract and stable, with a high degree of reusability. Stability is intuitively manifested by minimal changes to the exposed interfaces. To achieve this, we need to improve our ability to abstract functionalities when encapsulating components. Design the functionality abstractions and interfaces well, and handle all potential change factors within the component without exposing them to the callers.

The principle of stability suggests that stable components should not depend on unstable components. For example, if Component 1 depends on Component 5 and Component 1 is stable, but Component 5 frequently changes, Component 1 will also become unstable and require frequent adaptation. If there is indeed code in Component 5 that is indispensable for Component 1, we can consider extracting this code into a new component X or directly copying the dependent code into Component 1.

Self-completeness means that components should strive to be self-sufficient and minimize dependencies on other underlying components to achieve code reusability. For example, if Component 1 only depends on a method in a larger Component 5, it is better to remove the dependency of Component 1 on Component 5 and directly copy the method into Component 1. This way, Component 1 will be better prepared for future external changes.

After understanding the basic principles of componentization, let’s take a look at the specific steps of implementing componentization, including extracting basic functionality, abstracting business modules, and minimizing service capabilities.

Firstly, we need to extract basic functionalities unrelated to the business from the application, such as network requests, component middleware, third-party library encapsulation, and UI components, and encapsulate them into independent basic libraries. Then, we manage them using pub in the project. If it is a third-party library, considering the subsequent maintenance and adaptation costs, it is better to encapsulate it with another layer so that the project does not directly depend on external code, making it easier to update or replace it in the future.

Now that the basic functionalities have been encapsulated into more clearly defined components, we can then split the independent modules according to business dimensions, such as the home page, the detail page, the search page, etc. The granularity of the split can be coarse at first and refined later. As long as we can initially determine and divide the business components clearly, we can achieve componentization of the entire business project through iterative distribution and fine-tuning.

After the business components and basic components have been split and encapsulated, the componentized architecture of the application is basically formed. Finally, we can revise the dependencies of each component downward and minimize the exposed capabilities according to the four principles mentioned earlier.

Platformization #

From the definition of components, we can see that components are a loose and broad concept, and their scale depends on the size of the functional dimensions we encapsulate. The relationship between components is maintained solely through dependencies. If the dependency relationships between components are complex, it will lead to functional coupling to some extent.

As shown in the component diagram below, Component 2 and Component 3 are directly referenced by multiple business components and basic functional components. There may even be circular dependencies between Component 2 and Component 5, Component 3 and Component 4. Once the internal implementation or external interface of these components changes, the entire App will enter an unstable state, which is the so-called “a slight change affecting the whole”.

Figure 2 Circular Dependency Phenomenon

Platformization is an upgrade of componentization, namely, based on componentization, it classifies the functions provided by them, uniformly divides them into layers, and introduces the concept of dependency governance. In order to conceptually classify these functional units more uniformly, we use the four-quadrant analysis method to decompose the components of the application into 4 dimensions based on business and UI, to analyze the types of components.

Figure 3 Component Classification Principles

After decomposing them from business and UI, we can see that these components can be classified into 4 types:

  1. Independent business modules with UI attributes;
  2. Basic business functions without UI attributes;
  3. UI controls without business attributes;
  4. Basic functions without business attributes.

According to their self-definition, these 4 types of components actually imply a layered dependency relationship. For example, the home page in the business module relies on the account function in the basic business module; for example, the carousel card in the UI control module relies on the storage management function in the basic function module. We divide them vertically based on the order of dependencies from top to bottom, and this will be a complete App.

Figure 4 Component Layering

It can be seen that the biggest difference between platformization and componentization is the introduction of the concept of layering. The functionality of each layer is based on the functionality of the same layer and the layer below, so that each component maintains independence while having certain flexibility. They can perform their own duties based on functional divisions without crossing boundaries.

Compared with componentization, which pays more attention to the independence of components, platformization pays more attention to the rationality of the relationships between components. This is also a principle to be considered when designing platformized architecture: the principle of one-way dependence.

The principle of one-way dependence means that the order of component dependencies should follow the layers of the application architecture, from top to bottom, avoiding scenarios where lower-level modules depend on higher-level modules in a cyclic manner. This can maximize the avoidance of complex coupling and reduce the difficulties encountered during componentization. If each component only depends on other components in a one-way manner, the relationships between components will be clear, and code decoupling will become very easy.

Platformization emphasizes the order of dependencies. In addition to not allowing lower-level components to depend on higher-level components, the dependencies between cross-layer and same-layer components should also be strictly controlled, as such dependencies often lead to confusion in architectural design.

What should we do if a lower-level component really needs to call the code of a higher-level component?

In this case, we can use an intermediate layer such as an Event Bus, Provider, or Router to implement information synchronization in the form of intermediate layer forwarding. For example, in the network engine located in the fourth layer, it may jump to the unified error page located in the first layer for specific error codes. In this case, we can use the named routing provided by the Router to complete the jump without knowing the implementation details of the error page. Another example is that in the account component located in the second layer, it will actively refresh the home page and my page in the first layer when the user logs in or logs out. In this case, we can use the Event Bus to trigger the account switch event and notify them to update the interface without needing to obtain the page instance. For more information on this topic, you can refer to relevant content in articles 20 and 21, and it will not be further elaborated here.

Platformized architecture is the most widely used software architecture design at present. Its core lies in how to layer discrete components according to the principle of one-way dependence. As for the specific layering logic, in addition to the business and UI four-quadrant rule we introduced above, you can also use other classification strategies as long as the overall structure is clear and there are no components that are difficult to determine their affiliation.

For example, Flutter uses the overall three-layer division of Embedder (operating system adaptation layer), Engine (rendering engine and Dart VM layer), and Framework (UI SDK layer). It can be seen that each layer of the Flutter framework has clear boundaries, and the functionality provided upward and the dependence downward are also very clear.

Figure 5 Flutter Framework Architecture

Note: This image is from Flutter System Overview

Summary #

Alright, that’s all for today’s sharing. Let’s summarize the main points.

Componentization and platformization are popular means of software development, which can split the functions of an App into multiple independent components or modules.

Among them, componentization focuses more on maintaining the independence of components. As long as the split functions are independent, the constraints are relatively loose, which can easily lead to a certain degree of functional coupling in medium and large-scale Apps. On the other hand, platformization emphasizes the rationality of the relationship between components, adding the concept of layers, so that there are both boundaries and a certain level of flexibility between components. As long as the unidirectional dependency principle is met, the relationships between components are clear.

Divide and conquer is an architecture concept unrelated to technology, which helps reduce the complexity of the project and improve the scalability and maintainability of the App. In today’s article, I focused on sharing the ideas behind componentization and platformization architecture designs, without going into the specific implementations. There are already many articles available on the internet about the implementation details of componentization and platformization, so you can search and learn about them on your own. If you have any other questions about componentization and platformization, please leave them in the comments.

In fact, you can also figure out that the purpose of today’s article is to help you understand the core ideas of App architecture design. Because once you understand the ideas, all that’s left is to put them into practice. When you need to design App architecture in the future, recalling these contents or retrieving this article will definitely be helpful.

Thought-provoking question #

Finally, I’ll leave you with a thought-provoking question.

In app architecture design, what approach would you use to manage dependencies related to resource classes?

Feel free to leave a comment in the comment section and share your thoughts. I’ll be waiting for you in the next article! Thank you for listening and feel free to share this article with more friends to read together.