MVC, MVP, and MVVM are three common architectural design patterns. Currently, MVP and MVVM are relatively widely used. Of course, MVC is not outdated. The so-called componentization means that the application is divided into various modules for development according to business requirements, and each module can be compiled into an independent APP for development. Theoretically, componentization is not the same as the first three architectural designs. The relationship between them is that each component of componentization can be designed using the first three architectural designs. Only after we understand the characteristics of these architectural designs can we choose the architectural pattern that is suitable for our project during development, which is also the purpose of this article.

1, the MVC

MVC (Model-view-controller), the standard MVC looks like this:

  • Model layer: the data Model corresponding to business logic, which is not related to View but related to business.
  • View layer: XML or Java is used to describe the interface.
  • Controllor: In Android, usually refers to activities and fragments, or business classes controlled by them.

An Activity is not a standard Controller. It is used to control the layout and write business code in the Activity, making the Activity look both like a View and a Controller.

In Android development, it means to use activities directly and write business logic in them. Obviously, the Activity itself is a view and is responsible for handling the business logic, so the logic can be messy.

This approach is not well suited to Android development.

2, the MVP

2.1 Concept Combing

MVP (Model-View-Presenter) is an evolutionary version of MVC. The main parts are as follows:

  • Model layer: Provides data access function.
  • View layer: Handles user events and views. In Android, this can be an Activity, Fragment, or View.
  • Presenter: Is responsible for accessing book data through the Model, connecting the View to the Model, and fetching data from the Model to the View.

So, here’s what we need to say about the MVP architecture:

  1. The Model here is used to access data, that is, to get data from a specific data source, don’t think of it as an MVC Model. In MVC, the Model is the data Model, and in MVP, we use beans to represent the data Model.
  2. The Model and View do not interact directly; they interact via a Presenter. In real development, we can define specifications with interfaces, then have our views and models implement them, and interact with them with a Presenter.

To illustrate the MVP design pattern, let’s present a sample program. You can get the source code at Github.

2.2 Sample program

In this example, we use:

  1. Eye-opening video API as data source;
  2. Retrofit for data access;
  3. Use ARouter for routing;
  4. Use the MVP design pattern as the program architecture.

Here is the basic package structure for this module:

The core code here is the MVP section.

Here we first define the top View and Presenter in MVP mode. In this case, it is BaseView and BasePresenter. They are two empty interfaces in this project.

Then, we defined HomeContract. It is an abstract interface that acts as a layer protocol that specifies which methods a View and Presenter should have for a given function. Usually, for different functions, we need to implement a separate MVP, and each MVP will have a Contract. The author thinks that its advantage lies in that it defines the interface of View and Presenter in one interface, which is more centralized. The ways they need to be implemented are also laid out in front of us.

Here, according to our business scenario, the interface is defined as follows:

    public interface HomeContract {

        interface IView extends BaseView {
            void setFirstPage(List<HomeBean.IssueList.ItemList> itemLists);
            void setNextPage(List<HomeBean.IssueList.ItemList> itemLists);
            void onError(String msg);
        }

        interface IPresenter extends BasePresenter {
            void requestFirstPage(a);
            void requestNextPage(a); }}Copy the code

HomeContract specifies the actions that views and presenters should have. In this case, it specifies the View and Presenter methods of the home page. As you can see, IView and IPresenter implement BaseView and BasePresenter respectively.

Above, we defined the specifications for V and P, and MVP also has a Model, which is used to get data from the network. Here we save network related specific code, you only need to know APIRetrofit. GetEyepetizerService () is used to obtain corresponding Service Retrofit, GetMoreHomeData () and getFirstHomeData() are used to retrieve data from the specified interface. Here is the definition of HomeModel:

public class HomeModel { public Observable<HomeBean> getFirstHomeData() { return APIRetrofit.getEyepetizerService().getFirstHomeData(System.currentTimeMillis()); } public Observable<HomeBean> getMoreHomeData(String url) { return APIRetrofit.getEyepetizerService().getMoreHomeData(url); }}Copy the code

OK, that completes the definition of the Model and the specification of the View and Presenter. Next, we need to implement the View and Presenter.

First, Presenter. Here’s our definition of HomePresenter. In the following code, I have omitted some irrelevant code to make the logic clearer:

public class HomePresenter implements HomeContract.IPresenter {

    private HomeContract.IView view;

    private HomeModel homeModel;

    private String nextPageUrl;

    // Pass in the View and instantiate the Model
    public HomePresenter(HomeContract.IView view) {
        this.view = view;
        homeModel = new HomeModel();
    }

    // Use the Model to request the data, and call the View method to call back when the request result is received
    @Override
    public void requestFirstPage(a) {
        Disposable disposable = homeModel.getFirstHomeData()
                / /...
                .subscribe(itemLists -> { view.setFirstPage(itemLists); },
                        throwable -> { view.onError(throwable.toString()); });
    }

    // Use the Model to request the data, and call the View method to call back when the request result is received
    @Override
    public void requestNextPage(a) {
        Disposable disposable = homeModel.getMoreHomeData(nextPageUrl)
                / /....subscribe(itemLists -> { view.setFirstPage(itemLists); }, throwable -> { view.onError(throwable.toString()); }); }}Copy the code

As you can see from the above, the View and Model need to be linked in the Presenter. We need to pass in the View at initialization and instantiate a Model. A Presenter retrieves data from the Model and notifies the View layer when it retrieves data.

Next, there is the code for our View layer. Again, I have truncated the code:

@Route(path = BaseConstants.EYEPETIZER_MENU)
public class HomeActivity extends CommonActivity<ActivityEyepetizerMenuBinding> implements HomeContract.IView {

    // Instantiate Presenter
    private HomeContract.IPresenter presenter;
    {
        presenter = new HomePresenter(this);
    }

    @Override
    protected int getLayoutResId(a) {
        return R.layout.activity_eyepetizer_menu;
    }

    @Override
    protected void doCreateView(Bundle savedInstanceState) {
        // ...
        // Use Presenter to request data
        presenter.requestFirstPage();
        loading = true;
    }

    private void configList(a) {
        // ...
        getBinding().rv.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                    // Request data for next pagepresenter.requestNextPage(); }}}); }// When a result is requested, it is processed on the page and displayed on the page
    @Override
    public void setFirstPage(List<HomeBean.IssueList.ItemList> itemLists) {
        loading = false;
        homeAdapter.addData(itemLists);
    }

    // When a result is requested, it is processed on the page and displayed on the page
    @Override
    public void setNextPage(List<HomeBean.IssueList.ItemList> itemLists) {
        loading = false;
        homeAdapter.addData(itemLists);
    }

    @Override
    public void onError(String msg) {
        ToastUtils.makeToast(msg);
    }

    // ...
}
Copy the code

From the code above, we can see that the View also maintains an instance of a Presenter. The methods of the instance will be used to request data, so during development we need to define interface methods in the Presenter based on the data requested.

In fact, the principle of MVP is that the View retrieves data from a Presenter and then calls back to the View to display the data.

2.3 The difference between MVC and MVP

  1. MVC allows the Model and View to interact with each other. In MVP, the Model and View interact with each other by Presenter.
  2. The MVP pattern is to define P as an interface and then call the methods of the interface to handle each event that is triggered. That is to put the logic into P and call the methods of P when something needs to be done.

2.4 MVP pros and cons

Advantages:

  1. Reduce the coupling degree, realize the real Model and View completely separated, can modify the View without affecting Modle;
  2. Module responsibilities are clearly divided and hierarchical;
  3. Hide data;
  4. Presenters can be reused. A Presenter can be used for multiple views without changing the logic of the Presenter.
  5. In favor of test-driven development, previous Android development was difficult to unit test;
  6. Views can be componentized. In MVP, the View does not depend on the Model.

Disadvantages:

  1. In addition to application logic, Presenter also has a large number of View->Model, Model->View synchronization logic, which makes Presenter cumbersome and difficult to maintain.
  2. Because the rendering of the view is in a Presenter, the view interacts with the Presenter too frequently;
  3. If a Presenter renders too many views, it tends to become too closely associated with a particular view, and if the view needs to change, the Presenter needs to change too.

3. MVVM (Breakup Guru)

3.1 Basic Concepts

MVVM is short for Model-view-ViewModel. It’s essentially an improved version of MVC. MVVM abstracts the state and behavior of its views, allowing us to separate the View UI from the business logic.

  • Model layer: responsible for obtaining data from various data sources;
  • View layer: in Android, corresponding to activities and fragments, used to display to users and handle user interaction, will drive the ViewModel to get data from the Model;
  • ViewModel layer: used to associate the Model with the View. We can use the ViewModel in the View to get data from the Model. When the data is retrieved, the results are automatically flushed to the interface through an automatic binding, such as DataBinding.

Using Google’s official Android Architecture Components, we can easily apply MVVM to our applications. Next, let’s use it to show MVVM in action. You can get the source code at Github.

3.2 Sample Program

In this project, we used:

  1. The API of Guokernet serves as the data source;
  2. Using Retrofit for network data access;
  3. Use ViewMdeol as the overall architecture design.

The package structure of the project is shown below:

The classes below model.data are the data entities that correspond to the network, which are automatically generated by JSON and will not be described in detail here. The two classes below model.repository are used to retrieve data information from the network, and we ignore its definition.

The above is the definition of our Model, but there is not much content, which is basically consistent with MVP.

Below is the ViewModel code, and we have chosen one of the methods to illustrate it. When we define the ViewModel, we inherit from the ViewModel class.

public class GuokrViewModel extends ViewModel {

    public LiveData<Resource<GuokrNews>> getGuokrNews(int offset, int limit) {
        MutableLiveData<Resource<GuokrNews>> result = new MutableLiveData<>();
        GuokrRetrofit.getGuokrService().getNews(offset, limit)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Observer<GuokrNews>() {
                    @Override
                    public void onError(Throwable e) {
                        result.setValue(Resource.error(e.getMessage(), null));
                    }

                    @Override
                    public void onComplete(a) {}@Override
                    public void onSubscribe(Disposable d) {}@Override
                    public void onNext(GuokrNews guokrNews) { result.setValue(Resource.success(guokrNews)); }});returnresult; }}Copy the code

The ViewModel here from android. The arch. The lifecycle. The ViewModel, so, in order to use it, we also need to add the following:

api "android.arch.lifecycle:runtime:$archVersion"
api "android.arch.lifecycle:extensions:$archVersion"
annotationProcessor "android.arch.lifecycle:compiler:$archVersion"
Copy the code

In the definition of the ViewModel, we use Retrofit directly to fetch data from the network. Then when we get the data, we use the LiveData method to encapsulate the data into an object and return it to the View layer. At the View level, we just need to call this method and “listen” on the returned LiveData. Here, we encapsulate the error message and the returned data, and we encapsulate an enumeration that represents the current state. You can refer to the source code for details.

Now that we’ve defined the Model and the ViewModel, let’s look at the definition of the View layer and how to use the ViewModel in the View layer.

@Route(path = BaseConstants.GUOKR_NEWS_LIST)
public class NewsListFragment extends CommonFragment<FragmentNewsListBinding> {

    private GuokrViewModel guokrViewModel;

    private int offset = 0;

    private final int limit = 20;

    private GuokrNewsAdapter adapter;

    @Override
    protected int getLayoutResId(a) {
        return R.layout.fragment_news_list;
    }

    @Override
    protected void doCreateView(Bundle savedInstanceState) {
        // ...

        guokrViewModel = ViewModelProviders.of(this).get(GuokrViewModel.class);

        fetchNews();
    }

    private void fetchNews(a) {
        guokrViewModel.getGuokrNews(offset, limit).observe(this, guokrNewsResource -> {
            if (guokrNewsResource == null) {
                return;
            }
            switch (guokrNewsResource.status) {
                case FAILED:
                    ToastUtils.makeToast(guokrNewsResource.message);
                    break;
                case SUCCESS:
                    adapter.addData(guokrNewsResource.data.getResult());
                    adapter.notifyDataSetChanged();
                    break; }}); }}Copy the code

So that’s the definition of our View layer, and we’re using it here

Here we have the view.fragment class that corresponds to the actual page. Here we use the ViewModelProviders method to get the ViewModel we want to use. And to return the result of “listening” can be.

This is the basic use of MVVM. Of course, we don’t use DataBinding to directly bind the returned list information, it is more used in the entire Fragment layout.

3.3 Advantages and disadvantages of MVVM

The MVVM pattern, like the MVC pattern, aims to separate views and models. It has several advantages:

  1. Low coupling: Views can be changed and modified independently of the Model, a ViewModel can be bound to different views, the Model can be unchanged when the View changes, and the View can be unchanged when the Model changes.
  2. Reusability: You can put some view logic in a ViewModel and have many views reuse that view logic.
  3. Independent development: Developers can focus on business logic and data development (ViewModel), and designers can focus on page design.
  4. Testable: Interface elements are more difficult to test, but tests can now be written against the ViewModel.

4. Componentization

4.1 Basic Concepts

The so-called componentization is generally understood as dividing a project into modules, which are decoupled from each other, and can be independently developed and compiled into an independent APP for debugging, and then all modules can be combined to form a complete APP. Its advantage is when the project is relatively large, facilitate the division of labor between each developer collaboration, synchronous development; The divided modules can be shared between projects, so as to achieve the purpose of reuse. Componentization has many benefits, especially for larger projects.

With a brief look at componentization, let’s take a look at how to implement componentization development. You may have heard of componentized development before, or you may have been intimidated by its lofty name, but it’s not complicated in practice, at least not with the help of existing frameworks. Here, let’s first comb through the problems that need to be solved when applying componentization:

  1. How is it divided into modules? We can split them according to business. Large functional modules can be used as one module of the application. However, we should also pay attention to not dividing too many modules, or it may slow down the compilation speed and increase the difficulty of maintenance.
  2. How to share and communicate data between modules? We can divide the data that needs to be shared into a separate module to hold the common data. For data communication between each module, we can use ARouter of Ali for page jump, and use RxJava after encapsulation as EventBus for global data communication.
  3. How to package each module into a single APP for debugging? The androidmanifest.xml file can be configured in each module’s Gradle file, and a separate Application and startup class can be configured for each Application.
  4. How can I prevent resource name conflicts? The problem of resource name conflicts can be avoided by following naming conventions.
  5. How to solve the problem of duplicate library dependencies and third-party version number control for SDKS and dependencies? It is possible to configure the version of the dependencies shared by each module into settings.gradle, and to create a common module to configure the required dependencies.

Talk is cheap, let’s practice to apply componentization for development. You can get the source code at Github.

4.2 Componentization practices

Package structure

First, let’s look at the package structure of the entire application. As shown in the figure below, the division of the module is determined according to the function of each module. The white part on the right side of the figure is the file path of each module. I recommend using this method rather than placing each module under the app, because it looks clearer. To do this, all you need to do is to configure the paths of each module in settings.gralde as follows. Pay attention to the module path relationship in the actual application, don’t make a mistake.

Then, let’s introduce the Commons module here. It is used to store common resources and dependencies, and here we put both in one module to reduce the number of modules. Below is the partial configuration of gradle. Here we use the API to introduce the dependencies so that they can be used in other modules.

dependencies {
    api fileTree(include: ['*.jar'].dir: 'libs')
    // ...
    // router
    api 'com. Alibaba: arouter - API: 1.3.1'
    annotationProcessor 'com. Alibaba: arouter - compiler: 1.1.4'
    // walle
    api 'com. At meituan. Android. Walle: library: 1.1.6'
    // umeng
    api 'com. Umeng. SDK: common: 1.5.3'
    api 'com. Umeng. SDK: analytics: 7.5.3'
    api files('libs/pldroid - player - 1.5.0. Jar')}Copy the code

routing

Next, let’s look at the configuration of the routing framework. In this case, we used Ali’s ARouter to jump between pages. You can see how the framework is configured and used on Github. Here we will just explain what you need to pay attention to when componentized development. Note that ARouter uses annotations to configure the page and that its annotations are processed at compile time. Therefore, we need to introduce the Aroutter-compiler to use its compile-time processing capabilities. Note that we can use arouter’s API by adding an aroutter-API to a common module, but we need to introduce an Aroutter-compiler into each module to use compile-time annotations. That said, we need to add an aroutter-compiler dependency to every module.

Modules are independent

In order to compile each module into a separate APP, we need to do some configuration in Gradle.

First, we need to define some Boolean variables in Gradle. properties to determine whether each module is compiled as a library or an application. My configuration here is shown in the code below. That is, I define a Boolean variable for each module, but you can define only one variable and use the same variable in each module.

isGuokrModuleApp=false
isLiveModuleApp=false
isLayoutModuleApp=false
isLibraryModuleApp=false
isEyepetizerModuleApp=false
Copy the code

Then, let’s take a look at how gradle should be configured in each module. Here we take the eye-opening video function module as an example to explain. First, whether a module is a Library or an Application depends on the plugin it refers to, so we use the Boolean variable defined earlier to decide which plugin to use:

if (isEyepetizerModuleApp.toBoolean()) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}
Copy the code

If we want to use a module as a separate APP, then you definitely need to configure the startup class. This means you need two Androidmanifest.xml files, one for library state and one for application state. So, we can define another AndroidManifest.xml under main, and then we can specify not only the launch class in that configuration file, but also the Application we defined. Specifying Application is sometimes necessary, for example you need to initialize ARouter in each module and so on. Here we show how to specify AndroidManifest.xml in Gradle.

As shown below, we can determine which configuration file to use based on the Boolean values defined previously:

    sourceSets {
        main {
            jniLibs.srcDirs = ['libs']
            if (isEyepetizerModuleApp.toBoolean()) {
                manifest.srcFile "src/main/debug/AndroidManifest.xml"
            } else {
                manifest.srcFile "src/main/AndroidManifest.xml"}}}Copy the code

In addition, note that if we want to apply DataBinding and Java 8 features to each module, then you need to add the following configuration to each module:

    // use data binding
    dataBinding {
        enabled = true
    }
    // use java 8 language
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
Copy the code

Configuration such as compile-time annotations also need to be declared in each module.

After the above configuration is complete, we just need to modify the previously defined Boolean value to decide whether to compile the module as an APP or as a class library, depending on the type we want to compile.

So that’s componentization in Android development.

conclusion

MVC, MVP, and MVVM each have their own characteristics, and you can choose your own architectural pattern according to the needs of application development. The purpose of componentization is to keep each module independent so as to facilitate the division of labor and cooperation. The relationship between them is that you can apply one or more of the first three architectural patterns to each module of componentization.