Android Development Architecture

If the development process is fragmented and there is no uniform specification, over time the project code will become chaotic and difficult to maintain later. When using a uniform architectural pattern, there are many benefits, such as:

  • Unified development specification, make code clean, standard, easy to maintain and expand
  • Improve development efficiency (especially if the team is large)
  • Modules have a single responsibility, allowing modules to focus on their own (object-oriented), decoupling between modules

In a word, the development architecture is a set of effective development mode summed up by predecessors, the purpose is to achieve high cohesion, low coupling effect, make the project code more robust, easy to maintain.

The common architecture patterns in Android include MVC (Model-View-Controller), MVP (Model-View-Presenter), and MVVM (Model-view-ViewModel). Let’s take a look at their characteristics:

MVC

MVC (Model-View-Controller) is an early architectural pattern, and the overall pattern is relatively simple.

The MVC pattern divides the application into three parts:

  • Model Layer: business related data (such as network request data, local database data, etc.) and its processing of data
  • View View layer: The page View (which writes the View layer through an XML layout) is responsible for receiving user input, initiating data requests, and presenting the resulting page
  • Controller Controller layer: bridge between M and V, responsible for service logic

MVC features:

  • Simple and easy to use: The figure above shows the whole process of data:ViewReceiving user operations, passControllerTo process the business logic and passModelTo get/update the data and thenModel layerAnd send back the latest dataViewLayer for page display.
  • The flip side of simple architecture is often a side effect: Due to the weak ABILITY of XML layout, many operations of our View layer are written in the Activity/Fragment, and most of the code of Controller and Model layer is also written in the Activity/Fragment, which leads to a problem. When the business logic is complicated, The amount of code in an Activity/Fragment can be very large, which violates the single responsibility of the class and is not conducive to subsequent extension and maintenance. Especially if you’re working on a new project and a single Activity/Fragment class has thousands of lines of code in it, it can be quite refreshing:

Of course, if the business is simple, the MVC pattern is still a good choice.

MVP

MVP (Model-view-Presenter) architecture diagram is as follows:

Responsibilities of each MVP module are as follows:

  • Model: Business-related data (such as network request data, local database data, etc.) and data processing
  • View: An Activity/Fragment that receives user input, initiates data requests, and displays the result page
  • Presenter: A bridge between M and V, responsible for business logic

MVP features: The View layer receives user operations, and handles business logic and requests data through its Presenter. The Presenter layer then fetches the data through the Model, which in turn sends the latest data back to the Presenter layer, which in turn holds a reference to the View layer and passes the data to the View layer for display.

A few changes from MVP to MVC:

  • The View layer no longer interacts with the Model layer, but with presenters
  • In essence, MVP is interface oriented programming. The responsibilities of each Model/View/Presenter layer are clearly divided. When business is complex, the logic of the whole process is also very clear

Of course, MVPS aren’t perfect, and they have their own problems:

  • ViewThe layers will be abstractedIViewInterface, and inIViewDeclare columns inViewRelated methods; In the same way,PresenterWill be abstracted intoIPresenterInterface and some of its methods, whenever the implementation of a function, need to write multiple interfaces and their corresponding methods, the implementation is relatively cumbersome, and every time there is a change, the corresponding interface method will basically change again.
  • ViewLayer andPresenterLayers hold each other whenViewLayer closes when due toPresenterLayers are not lifecycle aware and can result in memory leaks or even crashes.

Ps: If you use RxJava in your project, you can use AutoDispose to automatically unbind it.

MVVM

MVVM (Model - View - ViewModel), the architecture diagram is as follows:

The responsibilities of MVVM are as follows:

  • Model: Business-related data (such as network request data, local database data, etc.) and data processing
  • View: An Activity/Fragment that receives user input, initiates data requests, and displays the result page
  • ViewModel: bridge between M and V, responsible for business logic

The MVVM features:

  • View layer receives user operations, and through the holding of the ViewModel to process business logic, request data;
  • The ViewModel layer fetches the data from the Model, and the Model passes the latest data back to the ViewModel layer, so the ViewModel does basically the same thing as the Presenter. But the ViewModel does not and cannot hold references to the View layer. Instead, the View layer listens for changes in the ViewModel layer through observer mode. When new data is available, the View layer automatically receives new data and refreshes the interface.

Ui-driven vs. data-driven

In MVP, the Presenter needs to hold a reference to the View layer. When data changes, the Presenter needs to actively call the corresponding method of the View layer to pass the data and perform UI refresh. This can be considered as UI-driven. In MVVM, the ViewModel does not hold a reference to the View layer. The View layer listens for data changes. When there is an update in the ViewModel, the View layer can directly take the new data and complete the UI update.

MVVM concrete implementation

The characteristics of MVC/MVP/MVVM are introduced above. The specific use of MVC/MVP will not be implemented in this article. Next, we will mainly talk about the use and encapsulation of MVVM, which is also the official recommended architecture mode.

Jetpack MVVM

Jetpack is an official series of component libraries. Using component libraries for development has many benefits, such as:

  • Follow best practices: Build with the latest design approach, with backward compatibility, which reduces crashes and memory leaks
  • Eliminate boilerplate code: Developers can better focus on business logic
  • Reduce inconsistencies: It runs on a variety of Android versions for better compatibility.

In order to implement the MVVM architectural pattern above, Jetpack provides several components to implement, specifically Lifecycle, LiveData, ViewModel(here ViewModel is the concrete implementation of the ViewModel layer in MVVM), Where Lifecycle is responsible for Lifecycle correlation; LiveData makes classes observable and life-cycle aware (Lifecycle is used internally); The ViewModel is designed to store and manage data related to the interface in a life-cycle manner. The details of these libraries and how to use them are not described in detail. If you are interested, refer to the previous article:

  • Android Jetpack series Lifecycle
  • LiveData for Android Jetpack
  • ViewModel for Android Jetpack series

Through these libraries, MVVM can be implemented, the official release of MVVM architecture diagram:

The Activity/Fragment layer is the View layer, and ViewModel+LiveData layer is the ViewModel layer. In order to manage network data and local data in a unified manner, the middle layer of Repository is introduced. In essence, it is to better manage data. Call them collectively the Model layer for simplicity.

Using an example

  • View layer code:
//MvvmExampleActivity.kt class MvvmExampleActivity : BaseActivity() { private val mTvContent: TextView by id(R.id.tv_content) private val mBtnQuest: Button by id(R.id.btn_request) private val mToolBar: Toolbar by id(R.id.toolbar) override fun getLayoutId(): Int { return R.layout.activity_wan_android } override fun initViews() { initToolBar(mToolBar, "Jetpack MVVM", } override fun init() {override fun init() { MViewModel = ViewModelProvider(this).get(WanViewModel::class.java) MBtnQuest. SetOnClickListener {/ / request data mViewModel getWanInfo ()} / / in the ViewModel LiveData registered observers and to monitor data changes MViewModel. MWanLiveData. Observe (this) - > {list val builder = StringBuilder () for (the index in the list. The indices) {/ / each fold line shows the data  if (index ! = list.size - 1) { builder.append(list[index]) builder.append("\n\n") } else { builder.append(list[index]) } } mTvContent.text = builder.toString() } } }Copy the code
  • ViewModel layer code:
//WanViewModel.kt class WanViewModel : ViewModel() { //LiveData val mWanLiveData = MutableLiveData<List<WanModel>>() //loading val loadingLiveData = SingleLiveData<Boolean>() // Val errorLiveData = SingleLiveData<String>() //Repository middle layer manages all data sources including local and network private val mWanRepo = WanRepository() fun getWanInfo(wanId: String = "") {/ / display Loading loadingLiveData. PostValue (true) viewModelScope. Launch (Dispatchers. IO) {try {val result = mWanRepo.requestWanData(wanId) when (result.state) { State.Success -> mWanLiveData.postValue(result.data) State.Error ->  errorLiveData.postValue(result.msg) } } catch (e: Exception) { error(e.message ? : "") } finally { loadingLiveData.postValue(false) } } } }Copy the code
  • Repository layer code:
Class WanRepository {suspend fun requestWanData(drinkId: String): BaseData<List<WanModel>> { val service = RetrofitUtil.getService(DrinkService::class.java) val baseData = Service.getbanner () if (basedata.code == 0) {// Correct basedata.state = state.success} else {// Error basedata.state = State.Error } return baseData } }Copy the code

If you need to add local data, you just need to add local data processing in the method. That is, Repository is the middle layer of data management, which manages data uniformly. In ViewModel layer, you don’t need to care about the source of data. The code is more readable and decoupled by keeping everyone to a single responsibility. Click the button in the View layer to request data, and the result is as follows:

This completes a network request compared toMVP.MVVMYou don’t have to declare multiple interfaces and methods, and at the same timeViewModelAlso don’t likePresenterThat way to holdViewLayer references, but life cycle aware,MVVMThe pattern is more decoupled.

encapsulation

In the Jetpack MVVM usage example shown in the previous section, you can see that there is some code logic that can be pulled out and encapsulated into the common part, so this section tries to encapsulate it.

The IStatusView interface states that Loading may be displayed when Loading data is requested.

Interface IStatusView {fun showEmptyView() // empty view fun showErrorView(errMsg: String) // error view fun showLoadingView(isShow: Boolean) // Display Loading view}Copy the code

Because the ViewModel is initialized in the Activity, it can be encapsulated as a Base class:

abstract class BaseMvvmActivity<VM : BaseViewModel> : BaseActivity(), IStatusView { protected lateinit var mViewModel: VM protected lateinit var mView: View private lateinit var mLoadingDialog: LoadingDialog override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) mLoadingDialog = LoadingDialog(this, false) mViewModel = getViewModel()!! Init () registerEvent()} /** * getViewModel subclass can be overridden */ protected getViewModel(): VM? {/ / the object of the superclass Type val Type = javaClass. GenericSuperclass / / ParameterizedType said if parameterized types (Type! = null && type is ParameterizedType) {// Returns an array of type objects for the actual type parameters of this type val actualTypeArguments = type.actualTypeArguments val tClass = actualTypeArguments[0] return ViewModelProvider(this).get(tClass as Class<VM>) } return null } override fun showLoadingView(isShow: Boolean) { if (isShow) { mLoadingDialog.showDialog(this, false) } else { mLoadingDialog.dismissDialog() } } override fun showEmptyView() { ...... } // Error view and can retry Override fun showErrorView(errMsg: String) {....... } private fun registerEvent () {/ / receiving error message mViewModel. ErrorLiveData. Observe (this) {errMsg - > showErrorView (errMsg)} / / receive Loading information mViewModel. LoadingLiveData. Observe (this, {isShow - > showLoadingView (isShow)})} the abstract fun init ()}Copy the code

The ViewModel can be initialized from the Base class or from the official activity-ktx or fragment-ktx extension libraries: val Model: VM by viewModels().

The subclass inherits as follows:

class MvvmExampleActivity : BaseMvvmActivity<WanViewModel>() { private val mTvContent: TextView by id(R.id.tv_content) private val mBtnQuest: Button by id(R.id.btn_request) private val mToolBar: Toolbar by id(R.id.toolbar) override fun getLayoutId(): Int { return R.layout.activity_wan_android } override fun initViews() { initToolBar(mToolBar, "Jetpack MVVM", True)} override fun init () {mBtnQuest. SetOnClickListener {/ / request data mViewModel getWanInfo ()} / * * * USES the extension function here, Equivalent mViewModel. MWanLiveData. Observe (this) {} * / observe (mViewModel. MWanLiveData) {list - > val builder = StringBuilder () For (index in list.indices) {// If (index! = list.size - 1) { builder.append(list[index]) builder.append("\n\n") } else { builder.append(list[index]) } } mTvContent.text = builder.toString() } } }Copy the code

We put the initialization of the ViewModel into the parent class, and the code looks much simpler. To monitor data changes mViewModel. MWanLiveData. Observe (this) {} means to observe (mViewModel. MWanLiveData) {}, less a LifecycleOwner, actually this is an extension function, As follows:

fun <T> LifecycleOwner.observe(liveData: LiveData<T>, observer: (t: T) -> Unit) {
    liveData.observe(this, { observer(it) })
}
Copy the code

Private val mBtnQuest: Button by id(R.i.b.btn_request);

fun <T : View> Activity.id(id: Int) = lazy {
    findViewById<T>(id)
}
Copy the code

Do not write Java code as always to think of empty, at the same time will only be used when the initialization, very practical!

Again, the ViewModel layer is wrapped, baseViewModel.kt:

abstract class BaseViewModel : ViewModel() {//loading val loadingLiveData = SingleLiveData<Boolean>( SingleLiveData<String>() /** * @param Request Normal logic * @param error exception handling * @param showLoading Whether to display Loading */ fun when requesting network launchRequest( showLoading: Boolean = true, error: Suspend (String) -> Unit = {errMsg -> // Default exception handling, subclasses can override errorLiveData.postValue(errMsg)}, request: Suspend () -> Unit) {// Whether to display Loading if (showLoading) {loadStart()} // Use viewModelscope.launch to start the coroutine viewModelScope.launch(Dispatchers.IO) { try { request() } catch (e: Exception) { error(e.message ? : "") } finally { if (showLoading) { loadFinish() } } } } private fun loadStart() { loadingLiveData.postValue(true) } private fun loadFinish() { loadingLiveData.postValue(false) } }Copy the code

When executing a network request, use viewModelScope.launch to launch the coroutine.

Implementation 'androidx. Lifecycle: lifecycle - livedata - KTX: 2.2.0'Copy the code

This allows you to start the coroutine directly in the ViewModel and automatically close it when the ViewModel life cycle ends. Avoid globalScope.launch {} or MainScope(). Launch {} and close the coroutine yourself. If you are using coroutines in activities/fragments or liveData, you can also introduce them on demand:

Implementation 'androidx. Lifecycle: lifecycle - runtime - KTX: 2.2.0' implementation 'androidx. Lifecycle: lifecycle - livedata - KTX: 2.2.0'Copy the code

See the official article on using Kotlin coroutines with lifecycle aware components.

In addition, careful readers may notice that Loading and Error information monitoring above is SingleLiveData.

/** * When multiple observers exist, Only one Observer can receive data updates * https://github.com/android/architecture-samples/blob/dev-todo-mvvm-live/todoapp/app/src/main/java/com/example/android/ar chitecture/blueprints/todoapp/SingleLiveEvent.java */ class SingleLiveData<T> : MutableLiveData<T>() { companion object { private const val TAG = "SingleLiveEvent" } private val mPending = AtomicBoolean(false) @MainThread override fun observe(owner: LifecycleOwner, observer: Observer<in T>) { if (hasActiveObservers()) { Log.w(TAG, "Multiple observers registered but only one will be notified of changes.") } // Observe the internal MutableLiveData Super.observe (owner) {t -> // If expect is true, update to false and return true. If (mPending.compareAndSet(true, false)) { observer.onChanged(t) } } } override fun setValue(@Nullable value: T?) {//AtomicBoolean set to true mpending.set (true) super.setValue(value)} /** * Used for cases where T is Void, to make calls cleaner. */ @MainThread fun call() { value = null } }Copy the code

SingleLiveData is also derived from MutableLiveData. The difference is that only one Observer can receive data updates when multiple observers exist. In essence, CAS has been added during observe().

The subclass inherits as follows:

class WanViewModel : BaseViewModel() {//LiveData val mWanLiveData = MutableLiveData<List<WanModel>>() //Repository middle layer manages all data sources including local and network private val mWanRepo = WanRepository() fun getWanInfo(wanId: String = "") { launchRequest { val result = mWanRepo.requestWanData(wanId) when (result.state) { State.Success -> mWanLiveData.postValue(result.data) State.Error -> errorLiveData.postValue(result.msg) } } } }Copy the code

Finally, there is the encapsulation of the Model layer, baserepository.kt:

open class BaseRepository { suspend fun <T : Any> executeRequest( block: suspend () -> BaseData<T> ): BaseData<T> {val BaseData = block.invoke() if (basedata.code == 0) {// Correct basedata.state = state.success} else {// error baseData.state = State.Error } return baseData } }Copy the code

Base class basedata.kt:

class BaseData<T> {
    @SerializedName("errorCode")
    var code = -1
    @SerializedName("errorMsg")
    var msg: String? = null
    var data: T? = null
    var state: State = State.Error
}

enum class State {
    Success, Error
}
Copy the code

The subclass inherits as follows:

class WanRepository : BaseRepository() {
    suspend fun requestWanData(drinkId: String): BaseData<List<WanModel>> {
        val service = RetrofitUtil.getService(DrinkService::class.java)
        return executeRequest {
            service.getBanner()
        }
    }
}
Copy the code

At this point, you’re almost done. There are a few details that aren’t posted in the code. See Jetpack MVVM for the full code

reference

[1] Android Application Architecture Guide [2] www.php.cn/faq/417265….