Do you really understand the structure and application of Android?

Preface

Regarding the Android architecture, many people may have always been illusory in their hearts. They seem to understand but not understand, use them for use, and use them everywhere. The meaning of this situation is really limited. I have experience in refactoring multiple projects, and I happen to be more interested in the design field. Today I will share my understanding of architecture and design with you without any reservation.
This article will not specifically talk about what MVC, MVP, and MVVM are, but the points I describe should be the cornerstones of these patterns. In essence, I understand why this is done and what are the benefits of doing so. With the support of these underlying ideas Look at the corresponding architecture model, I believe it will give you a new feeling.
Knowledge reserve: You need to master the Java object-oriented, six design principles, if you don’t understand it, it’s okay, I try to describe the design principles used in detail

table of Contents

  • 1. What is the significance of modularity?
  • 1.1 Basic concepts and underlying ideas
  • 1.2 Based on what features should we make modularization?
  • 1.3 How does Android do hierarchical processing?
  • 1.4 Data Mapper may be the antidote
  • 1.5 Nowhere to put business logic
  • 2. Reasonable layering is to pave the way for data-driven UI
  • 2.1 What is inversion of control?
  • 2.2 What is a data-driven UI?
  • 2.3 Why is it said that 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 functional programming ideas

1. What is the significance of modularity?

1.1 Basic concepts and underlying ideas

All modularization is to meet the single design principle (the literal meaning can be understood), a function or a class or a module, the more single the responsibility, the stronger the reusability, and at the same time can indirectly reduce the coupling
in the background of software engineering If you make changes, you may make mistakes. Don't say "I will not make mistakes if I pay attention", because people are not machines. What we can do is to make the module as single as possible. The more single the responsibility, the less likely it will affect the outer module, and the lower the probability of error.
So the core idea of ​​modularity is: single design principle

1.2 Based on what features should we make modularization?

When doing modular processing, try to perform functional characteristics and business characteristics based on two characteristics.
Functional characteristics

Network, image loading, etc. can all be called functional characteristics. For example, network: we can write the integration, packaging, etc. of the 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, facilitate maintenance and make it safer . At the same time, the module can be hosted in a remote such as maven library, which can be used for multiple projects to further improve the reusability.
Business features
can be understood literally, which is the business we often write.
Why do we need to divide modules based on business features ? Say that business features have priority over functional features?
For example, as shown in the figure below: I

believe that many people have seen or are using this subcontracting method. Is it reasonable to put all Adapter, Presenter, Activity, etc. in the corresponding package in the business layer? Let me say that the answer is unreasonable. First of all, this is already at the business layer. Everything we do is actually serving the business layer. Therefore, the priority of the business should be the highest. We should put the corresponding classes into the In the same package.
The core of the functional module is the function, which should be divided into modules by function. The core of the business module is the business, which should be divided into modules by business first, and then by functions.

1.3 How does Android do hierarchical processing?

Front-end development is actually doing data handling before displaying it in the view. Data and view are two different concepts. In order to improve reusability and maintainability, we should process the two hierarchically according to a single design principle, so whether it is MVC, MVP or MVVM, the core point is Layer data and views.
stumbling block:

Generally speaking, the data structures we get through network requests are all defined by the back-end, which means that the view layer has to directly use the fields defined by the back-end. Once the back-end makes business adjustments, we will force the front-end to start from the data layer – >The view layer will make corresponding changes, as shown in the following pseudo code:

//原始逻辑
数据层
Model{
    
    
    title
}
UI层
View{
    
    
    textView = model.title
}

//后端调整后
数据层
Model{
    
    
    title
    prefix
}
UI层
View{
    
    
    textView = model.prefix + model.title
}

At first, our textView displayed the title in the model, but after the back-end adjustment, we need to add a prefix field to the model, and the textView display content also needs a string splicing. The view layer is passively modified due to changes in the data layer. Now that we have done layering, what we want is definitely that the view and data do not interfere with each other, how to solve it? Look down...

1.4 Data Mapper may be the antidote

Data Mapper is a commonly used concept in the backend. Under normal circumstances, they will not directly use the fields in the database, but add a Data Mapper (data mapping) to convert the database table to Java Bean on demand. The benefits of this are also Obviously, the table structure will not affect the business layer code regardless of how to toss it.
For the front-end, I think we can properly introduce Data Mapper to convert the back-end data into a local model. The local model only corresponds to the design drawing, completely isolating the back-end business from the view. This also solves the problem faced by 1.3, the specific method is as follows:

数据层
Model{
    
    
    title
    prefix
}
本地模型(与设计图一一对应)
LocalModel{
    
    
    //将后端模型转换为本地模型
    title = model.prefix + model.title
}
UI层
View{
    
    
    textView = localModel.title
}

LocalModel is equivalent to an intermediate layer, which isolates the data layer from the view layer through the adapter pattern.
After the data mapper is introduced in the front end, it can be developed away from the back end. As long as the requirements are clear, the development of the view layer can be done. There is no need to worry about the structure and fields returned by the back end. And this approach is done once and for all. For example, the backend needs to adjust certain fields. We can go straight to the data layer without thinking. 100% of the adjustments involved will not affect the view layer.
Note:

In order to separate the front-end and the back-end more thoroughly, some companies currently provide the structure of Java Bean (equivalent to LocalModel) by front-end developers. The benefits are also obvious. More business is cohesive to the back-end, which greatly improves the flexibility of the business. Sex, after all, the cost of publishing a version of the App is still relatively large. Faced with this situation, we actually don't need to write Data Mapper. Therefore, any architecture design must be combined with the actual situation, and the one that suits you is the best.

1.5 Nowhere to put business logic

The business logic is actually a very general concept. You can even call any line of code business logic. How do we understand such a broad concept? Let me roughly divide it into two aspects:

Interface interaction logic: The interaction logic of the view layer, such as gesture control, ceiling suspension, etc., is implemented according to business needs, so strictly speaking, this part is also business logic. But this part of the business logic is generally implemented in the view layer.

Data logic: This part is the business logic that everyone often talks about. It is a strong business logic, such as obtaining different data according to different user types, displaying different interfaces, and adding a series of Data Mapper operations to the backend to help them make up the rest. It's just logic. In order to facilitate your understanding, I will collectively refer to data logic as business logic below.
We mentioned earlier that Android development should have a data layer and a view layer. Which layer is more appropriate for business logic? For example, in the MVVM mode, everyone says that the business logic is handled in the ViewModel, so it is not a big problem, but if an interface is complex enough, the corresponding ViewModel code may have hundreds or thousands of lines, which may seem bloated. Readability is also very poor. The most important point is that these businesses are difficult to write unit test cases.
Regarding business logic, I suggest writing a separate use case for processing.
The use case is usually placed between the ViewModeler 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 problem of bloated ViewModeler and makes it easier to write test cases.
Points to note:
Good design is to solve specific problems in specific scenarios. Over-designing will not only solve any problems but will increase development costs. According to my current experience, at least half of the scenes of Android development are very simple: request -> get data -> render view and add a Data Mapper at most. The process is very simple and the later changes may not be too large. This is not the case. It is necessary to write a use case, and the Data Mapper can be thrown to the data layer.

2. Reasonable layering is to pave the way for data-driven UI

Let me start with the conclusion: the essence of data-driven UI is inversion of control

2.1 What is inversion of control?

Control refers to the control of the program flow, which is generally undertaken by our developers. This process is control. But the developer is a human, so errors are inevitable. At this time, the role can be reversed and the mature framework is responsible for the entire process. The programmer only needs to add his own business code to the extension points reserved by the framework. Using the framework to drive the execution of the entire program flow, this process is reversed.
The concept of inversion of control is similar to the dependency inversion in design principles, except that a dependency abstraction is missing.
To make an analogy:

There is an existing HTTP request requirement. If you want to maintain the HTTT link yourself, manage the TCP Socket yourself, and handle the HTTP cache yourself...that is, the entire HTTP protocol is encapsulated by yourself. Let alone whether this project can be implemented by individuals, even if it is implemented, it is full of loopholes. At this point, you can change your thinking: through OkHttp to achieve, OkHttp is a mature framework, basically there is no error with it. Individuals encapsulate the HTTP protocol to use the OkHttp framework. This process has undergone a reversal in the role of controlling HTTP. The individual —> mature framework OkHttp is the inversion of control. The benefits are also obvious. The probability of framework errors is much lower than that of individuals.

2.2 What is a data-driven UI?

In layman's terms, when the data changes, the corresponding UI must also change. Conversely, when you need to change the UI, you only need to change the corresponding data. The most popular UI frameworks such as Flutter, Compose, and Vue are essentially based on functional programming to implement data-driven UI. Their common purpose is to solve the problem of data and UI consistency.
In the current Android, you can use DataBinding to achieve the same effect. Take Jetpack MVVM as an example: ViewModel gets the data from the Repository and temporarily stores it to the ObservableFiled corresponding to the ViewModel to realize the data-driven UI, but the premise is that the data obtained from the Repository can be directly Use, if you do secondary data processing in Activity or Adapter and then notify UI, it has violated the core idea of ​​data-driven UI. Therefore, if you want to implement a data-driven UI, you must have a reasonable layering (the data obtained by the UI layer does not need to be processed and can be used directly). Data Mapper just solves this problem, and it can also avoid the current situation of writing a large number of BindAdapters.

DataBinding is not functional programming, it just generates intermediate code through AbstractProcessor to map data to XML

2.3 Why is it said that the underlying idea of ​​data-driven UI is inversion of control?

Currently, there are only two frameworks in the Android ecosystem that can implement data binding UI: DataBinding and Compose (not discussed yet).
Rendering a piece of data before introducing DataBinding usually requires two steps, as follows:

var title = "iOS"
fun setTitle(){
    
    
     //第一步更改数据源
     title = "Android"
     //第二个更改UI
     textView = title
}

A total of two steps are required to change the data source and change the UI. If one of the data source and the UI forgets to modify, there will be a BUG. Do not say: "I will not forget to modify both". When faced with complex logic and more than a dozen or even Dozens of data sources are difficult to guarantee error-free. This kind of problem can be solved by DataBinding. You only need to change the corresponding ObservableFiledUI and it will be modified synchronously. The control UI state is also reversed from the individual to the DataBinding. Personal negligence is not possible with the DataBinding.
So the underlying idea of ​​data-driven UI is inversion of control

2.4 Why is Diff introduced?

Before the introduction of diff:

RecyclerView wants to achieve dynamic deletion, addition, and update. You need to manually update the data and UI, so that inserting a piece in the middle and updating the data and UI separately has violated the aforementioned data-driven UI, and what we want is regardless of deletion, There is only one entry for adding or updating. As long as you change the data source, it will drive the UI to update. To meet this principle, you can only refresh the RecyclerView after changing the data source, but this will cause performance problems, and the complex interface will feel obvious Caton.
After the introduction of diff: The
Diff algorithm will automatically update the changed item by comparing the difference between oldItem and newItem. At the same time, it supports deleting and adding animation effects. This feature solves the performance problem of RecyclerView needing to implement data-driven UI.

3 Why do I recommend functional programming

3.1 What is functional programming?

  • One entrance, one exit.
  • Do not perform operations that are not related to the operation itself within the function chain
  • Do not use external variables inside the function chain (in fact, this one is difficult to comply with, and it can be broken appropriately) The
    popular point is that given an initial value, a target value will be obtained after the operation of the function chain, and there is no external intervention during the operation. Permission, and at the same time, do not do operations that are not related to itself, which fundamentally solves the occurrence of unexpected errors.
    for example:
//Kotlin代码

listOf(10, 20).map {
    
    
   it + 1
}.forEach {
    
    
   Log.i("list", "$it")
}

The above chain programming is standard functional programming. Developers have no chance to intervene between input and output (ie, before Log.i(...), developers have no permission to process the list), so the entire process is 100% safe. , RxJava, Flow, and chained higher-order functions are all standard functional programming. They solve data security issues from the specification level. So I suggest using chained higher-order functions as much as possible when dealing with data in Kotlin (RxJava, Kotlin Flow also).

3.2 Android view development can learn from functional programming ideas

Android view development mostly follows the following process: request -> processing data -> rendering UI, this process can learn from functional programming, taking the request as the entrance and rendering as the exit, and try not to do anything unrelated to the current behavior in this process ( This also requires the functions in ViewModel and Repository to conform to the single principle). It’s a bit general to say that, here is a counter-example:

    View{
    
    
        //刷新
        fun refresh(){
    
    
            ViewModel.load(true)
        }
        //加载更多
        fun loadMore(){
    
    
            ViewModel.load(false)
        }
    }

    ViewModel{
    
    
        //加载数据
        load(isRefresh){
    
    
            if (isRefresh){
    
    
                //刷新
            }else{
    
    
                //加载更多
            }
        }
    }

The View layer has two behaviors: refresh and load more. Load (isRefresh) has one entry and two exits. The problem is obvious. Modifying refresh or loading more will have an impact on the other party. It violates the closing of the opening and closing principle (close to modification: the source code is not allowed to be modified if the behavior has not changed), resulting in unpredictable problems. You can learn from functional programming ideas to improve it, split the load function of ViewModel into refresh and loadMore, so that refresh and load more two kinds of behaviors, two entrances, two exits do not interfere with each other, and the connection of functions forms two An independent business chain.
Functional programming can constrain us to write standardized code. In the face of scenarios where functional programming cannot be used, we can try to constrain ourselves to move closer to functional programming, and roughly the same effect can be achieved.

In summary

  • Reasonable layering can improve reusability and reduce coupling between modules
  • Data Mapper can make the view layer separate from the back-end for development
  • Complex business logic should be written in the use case
  • The essence of data-driven UI is inversion of control
  • More secure code can be written through functional programming

Guess you like

Origin blog.csdn.net/A_pyf/article/details/115218115