preface

About the Android architecture, probably in the mind of many people has always been a nebulous existence, just for the sake of using, everywhere mechanically copied, this situation is really limited. I have experience in refactoring several projects, and I happen to be interested in the design field. Today I will share my understanding of architecture and design with you without reservation.

This article will not go into the details of WHAT MVC, MVP, and MVVM are, but the points I describe should be the cornerstone of these patterns. In essence, understand why they are done, what are the benefits of doing them, and then look at the corresponding architectural patterns with the support of these underlying ideas, I believe it will give you a refreshing feeling.

Knowledge reserve: need to masterJava Object-oriented, six design principlesIf you don’t, don’t worry. I’ll try to describe the design principles I use in detail

directory

  • 1. What’s the point of modularity?
    • 1.1 Basic concepts and underlying ideas
    • 1.2 Which features should we use to make modular division?
    • 1.3 How to Perform hierarchical Processing on Android?
    • 1.4 Data Mapper may be the antidote
    • 1.5 The Service logic cannot be installed
  • 2. Layering is the foundation for a data-driven UI
    • 2.1 What is Inversion of Control?
    • 2.2 What is data-driven UI?
    • 2.3 Why the underlying idea of data-driven UI is Inversion of Control?
    • 2.4 Why is Diff introduced?
  • 3. Why do I recommend functional programming
    • 3.1 What is functional Programming?
    • 3.2 Android view development can learn from the idea of functional programming

1. What’s the point of modularity?

1.1 Basic concepts and underlying ideas

All modularity is designed to meet the single design principle (literally) : the more responsibilities a function or class or module has, the more reusable it will be, while indirectly reducing coupling

In the context of software engineering, there is always a chance that something will go wrong with a change. Don’t say, “I can’t go wrong if I pay attention,” because people are not machines. What we can do is to make the module as single as possible, and the more single responsibility there is, the less likely it is to affect the outer module, and thus the lower the probability of error.

So the core idea of modularity is: a single design principle

1.2 Which features should we use to make modular division?

When doing modularization, try to carry out functional features and business features based on the two features

features

Networking, image loading, and so on can be called features. For example, network: we can write the integration, encapsulation and so on of network framework into the same module (module, package, etc.), which can enhance readability (the same directory is clear at a glance), reduce the probability of misoperation, and facilitate maintenance and security. Modules can also be hosted remotely, such as maven libraries, for use by multiple projects, further improving reusability

Business characteristics

Business features can be understood literally, that is, we often write business, need to be based on business features for module division

Why do you sayBusiness characteristicsThe priority is higher thanfeatures?

Here’s an example:

I believe many people have seen or are using this subcontracting method, in the business layer to put all Adapter, Presenter, Activity and so on in the corresponding package, is this way reasonable? First of all, this is already in the business layer. Everything we do is actually serving the business layer, so the business should be the highest priority. We should first put the corresponding classes in the same package according to the business characteristics.

The core of function module is function, which should be divided into modules by function. The core of business module is business, so it should be divided by business first, and then by function.

1.3 How to Perform hierarchical Processing on Android?

Front-end development is all about moving data and presenting it to the view. Data and views are two different concepts, and in order to improve reusability and maintainability, we should layer them according to a single design principle, so whether MVC, MVP, or MVVM, the core point is to layer data and views.

A stumbling block:

In general, we get data structures from network requests that are defined in the back end, which means that the view layer has to directly use the fields defined in the back end. Once the business changes are made in the back end, the front end will have to change from the data layer to the view layer, as shown in the pseudocode below:

// Raw logical data layer Model{title} UI layer View{textView = model.title} // Back-end adjusted data layer Model{title prefix} UI layer View{textView = model.prefix + model.title }Copy the code

At first, our textView displays the title in the Model, but after the back-end adjustment, we need to add a prefix field to the Model, and we also need to do a string concatenation of the textView display content. The view layer is passively modified due to changes in the data layer. Since we have done the layering, we want to be sure that views and data do not interfere with each other. How to solve this problem? Look down…

1.4 Data Mapper may be the antidote

Data Mapper is a common concept in the back end. In general, they do not use the database fields directly, but add a Data Mapper to the database table into the Java Bean as needed, the benefits of this is obvious, no matter how much to do with the table structure will not affect the business layer code.

As for the front end, I think Data Mapper can be appropriately introduced to transform the back-end Data into the local model. The local model only corresponds to the design drawing and completely isolates the back-end business from the view. This solves the problem faced by 1.3 in the following ways:

Data layer Model{title prefix} LocalModel (corresponding to the design) LocalModel{// convert the back-end Model to the LocalModel title = model.prefix + model.title} UI layer View{ textView = localModel.title }Copy the code

The LocalModel acts as an intermediate layer, isolating the data layer from the view layer through the adapter pattern.

After the introduction of Data Mapper in the front end, the development can be separated from the back end. As long as the requirements are clear, the development of the view layer can be done, and there is no need to worry about the structure and field returned by the back end. And this approach is once and for all, if the back end needs to be adjusted for certain fields, we can go straight to the data layer without thinking, the adjustment involved 100% does not affect the view layer

Note:

At present, in order to separate the front and back ends more thoroughly, some companies provide the structure of Java beans (equivalent to LocalModel) by front-end developers. The benefits are also obvious. More businesses are concentrated in the back end, which greatly improves the flexibility of the business. We don’t really need to write a Data Mapper for this situation. So any architectural design should be combined with the actual situation, the best is suitable for their own.

1.5 The Service logic cannot be installed

Business logic is actually a very general concept, even can call any line of code business logic, so broad concept how do we understand? I will roughly divide it into two aspects:

  • Interface interaction logic: The interaction logic of the view layer, such as gesture control, roof suspension and so on, is implemented according to business needs, so strictly speaking, this part is also business logic. But this part of theThe business logicGenerally implemented at the view layer.
  • Data logic: This part is often referred to as business logic, which belongs to strong business logic. For example, according to different types of users to obtain different Data, display different interfaces, and add Data Mapper a series of operations is actually to help the back end, to help them complete the rest of the logic. For the sake of understanding what I’m going to doData logicCollectively referred to asThe business logic.

As we said earlier, Android development should have a data layer and a view layer. Which layer is the right one for business logic? For example, in MVVM mode, people say put the business logic into the ViewModel, which is not a big problem, but if the interface is complex enough then the ViewModel code can be hundreds or thousands of lines, which can look bloated and very difficult to read. The most important point is that these businesses are very difficult to write unit test cases.

I recommend writing a separate Use Case for the business logic.

Use Case is usually placed between ViewModel/Presenter and the Data layer. Business logic and Data Mapper should be placed in the use Case. Each behavior corresponds to a use Case. This solves the ViewModel/Presenter bloat and makes it easier to write test cases.

Note:

Good design is all about solving a specific problem for a specific scenario. Overdesign will not only solve any problems but increase development costs. In my experience, at least half of the scenarios of Android development are simple: request –> Get Data –> Render view and at most a Data Mapper. The process is simple and may not change much later. In this case, there is no need to write a use case, the Data Mapper is just thrown into the Data layer.

2. Layering is the foundation for a data-driven UI

Conclusion: The nature of data-driven UIs is inversion of control

2.1 What is Inversion of Control?

Control is the control of the process, generally undertaken by us developers, this process for control. At this time, the mature framework can be responsible for the entire process. Programmers only need to add their own business code on the extension points reserved by the framework, and then they can use the framework to drive the execution of the entire process. This process is called inversion.

The inversion of control concept is similar to dependency inversion in the design principle, except for one less dependency abstraction.

For example:

There is an existing demand for HTTP requests, if you want to maintain your own HTTT links, manage your own TCP sockets, and handle your own HTTP cache….. If the whole HTTP protocol is encapsulated by itself, let alone whether the project can be implemented by individuals, even if the implementation is full of bugs, then we can change the way of thinking: OkHttp to implement, OkHttp is a mature framework with which it is basically error-free. The process of the individual encapsulating the HTTP protocol to use the OkHttp framework reverses the role of controlling HTTP. The individual –> the mature OkHttp framework is inversion of control, and the benefit is clear: the framework has a much lower probability of error than the individual.

2.2 What is data-driven UI?

Basically, when the data changes, the corresponding UI changes, and conversely, when you change the UI, you just change the corresponding data. Popular UI frameworks such as Flutter, Compose, and Vue all implement data-driven UIs based on functional programming in nature, with the common goal of solving data and UI consistency problems.

In current Android, you can use DataBinding to achieve the same effect. Take Jetpack MVVM as an example: ViewModel obtains data from Repository and temporarily stores it to the observablefield corresponding to ViewModel to implement data-driven UI, but only if the data obtained from Repository can be used directly. If the Activity or Adapter performs secondary data processing before notifying the UI, it violates the core idea of data-driven UI. So to achieve data-driven UI must have a reasonable layer (the Data obtained by THE UI layer can be used directly without processing), Data Mapper just solves this problem, but also can avoid the current situation of writing a large number of BindAdapter.

DataBinding is not functional programming; it just generates intermediate code from AbstractProcessor to map data to XML

2.3 Why the underlying idea of data-driven UI is Inversion of Control?

Currently, there are only two frameworks for implementing DataBinding UI in the Android ecosystem :DataBinding and Compose(not discussed yet).

Rendering a piece of data before DataBinding is introduced usually takes two steps, as follows:

Var title = "iOS" fun setTitle(){var title = "iOS" fun setTitle(){Copy the code

There are two steps to change the data source and the UI. If you forget to modify one of the data source and UI, there will be a BUG. Do not say “I will not forget to modify both”. This problem can be solved by DataBinding. Simply changing the corresponding ObservableFiledUI will synchronize the changes, and the control UI state will be reversed from the individual to the DataBinding, but the DataBinding will not do anything that the individual has neglected.

So the underlying idea of data-driven UIs is inversion of control

2.4 Why is Diff introduced?

Before introducing DIff:

RecyclerView want to achieve dynamic delete, add, update, need to manually update the data and UI respectively, so a and update data respectively inserted in the middle and the UI has violated the above data driven UI, and we want is whether there was only one entrance to delete, add or update, just change the data source will drive the UI update, Want to meet this principle can only change the data source after RecyclerView to do all refresh, but this will cause performance problems, complex interface will feel obvious card.

After the introduction of DIFF:

Diff algorithm through the oldItem and newItem differentiation comparison, will automatically update the changed item, at the same time to support delete, add the animation effect, this feature to solve the RecyclerView needs to achieve data-driven UI performance problem

3. Why do I recommend functional programming

3.1 What is functional Programming?

  • One entrance, one exit.
  • Does not perform an operation within the function chain that is independent of the operation itself
  • Do not use external variables inside the function chain.

The common point is that given an initial value, a target value will be obtained after the operation of the function chain. In the process of operation, the external does not have the permission to intervene, and at the same time, the operation does not have anything to do with itself, which fundamentally solves the unexpected error.

Here's an example:

ListOf (10, 20).map {it + 1}.foreach {log.i ("list", "$it")}Copy the code

The above chaining is standard functional programming, with the developer having no input to output (i.e. Log.i(..)). RxJava, Flow, and chained high order functions are all standard functional programming that addresses data security issues from a canonical level. So I recommend using chained higher-order functions whenever you encounter data processing in Kotlin (as do RxJava and Kotlin Flow).

In fact, the core idea of functional programming is the facade pattern and Demeter's law

3.2 Android view development can learn from the idea of functional programming

Android view development generally follows the following process: Request -> Process data -> Render UI. This process can be modeled from functional programming, with request as entry and render as exit, and try not to do anything irrelevant to the current behavior in this process (this also requires that functions in ViewModel and Repository conform to the principle of simplicity). That’s a bit of a generalization, but here’s a counterexample:

View{// Refresh fun refresh(){viewModel.load (true)} // Load more fun loadMore(){viewModel.load (false)}} ViewModel{// Load data Load (isRefresh){if (isRefresh){// refresh}else{// load more}}}Copy the code

The View layer has two actions: refresh and load. Load (isRefresh) has one entry and two exits. The problem is obvious. Changes that refresh or load more will affect the other side, which violates the open closed principle (close for changes: do not modify the source code if the behavior has not changed), resulting in unexpected problems. We can learn from the idea of functional programming to improve it, and divide the load function of ViewModel into refresh and loadMore. In this way, refresh and load more two behaviors, two entrances and two exits do not interfere with each other, and two independent business chains can be formed through the connection of functions.

Functional programming can constrain us to write canonical code, and in situations where we can’t use functional programming, we can try to constrain ourselves toward functional programming, which can achieve roughly the same effect.

From what has been discussed above

  • Reasonable layering can improve reusability and reduce coupling between modules
  • Data Mapper allows the view layer to be developed away from the back end
  • Complex business logic should be written into use cases
  • The nature of data-driven UIs is inversion of control
  • More secure code can be written through functional programming

If you are interested in Jetpack MVVM, please leave a comment and I can write my opinion in the next post..