Build an MVVM framework for rapid development with Jetpack.

The project uses Jetpack: LiveData, ViewModel, Lifecycle, Navigation components.

Support dynamic loading multi-state layout: loading, success, failure, title;

Support fast generation of ListActivity, ListFragment;

Support the use of plug-ins to quickly generate activities, fragments, listActivities, listFragments suitable for the framework.

The full article can be viewed on Github

preface

As Google improves Jetpack, MVVM is becoming more efficient and convenient for developers.

Companies that use MVVM have their own MVVM framework, but I’ve found that some simply encapsulate the framework, resulting in a lot of unnecessary redundant code during development.

This article focuses on sharing how to build an efficient MVVM framework from scratch.

Rapid development based on MVVM, ready to use. (Refactoring complete, writing SampleApp)

Module separation of the basic framework, MVVM Navigation Library, MVVM Navigation Library and MVVM Network Library can be used based on business requirements Network Library

A one-click generation code template has been developed to create activities and fragments suitable for this framework. For details, see AlvinMVVMPlugin_4_3

How to integrate

To get a Git project into your build:

Step 1. Add the JitPack repository to your build file

Add it in your root build.gradle at the end of repositories:

allprojects {
	repositories {
		...
		maven { url 'https://jitpack.io' }
	}
}
Copy the code

Step 2. Add the dependency

Implementation 'com.github. Chen-xi-g. Mvvm_framework: mvVM_framework :Tag Implementation 'com.github. Chen-xi-g. Mvvmframework: mvVM_network :Tag' // MVVM Navigation component is implemented 'com.github.Chen-Xi-g.MVVMFramework:mvvm_navigation:Tag' }Copy the code
instructions Depend on the address The version number
The MVVM base class implementation ‘com.github.Chen-Xi-g.MVVMFramework:mvvm_framework:Tag’
MVVM Network implementation ‘com.github.Chen-Xi-g.MVVMFramework:mvvm_network:Tag’
MVVM Navigation implementation ‘com.github.Chen-Xi-g.MVVMFramework:mvvm_navigation:Tag’

After a dependency is introduced, it needs to be initialized. The following is the modular initialization process.

1. The BaseApplication inheritance

Create your Application class, inherit BaseApplication, and configure and initialize parameters in the onCreate function, where you can configure parameters for the network request framework and UI global parameters. Such as interceptors and multi-domain, global Activity and Fragment properties.

// Global Activity Settings
GlobalMVVMBuilder.initSetting(BaseActivitySetting(), BaseFragmentSetting())

private fun initHttpManager(a) {
    // Parameter interceptor
    HttpManager.instance.setting {
        // Set network properties
        setTimeUnit(TimeUnit.SECONDS) // The time type is seconds. The framework default value is milliseconds
        setReadTimeout(30) // Read timeout 30s, frame default value 10000L
        setWriteTimeout(30) // Write timeout 30s, frame default value 10000L
        setConnectTimeout(30) // The link timeout is 30s. The framework default value is 10000L
        setRetryOnConnectionFailure(true) // Automatic reconnection after timeout. The framework default value is true
        setBaseUrl("https://www.wanandroid.com") // Default domain name
        // Multi-domain configuration
        setDomain {
            Constant.domainList.forEach { map ->
                map.forEach {
                    if (it.key.isNotEmpty() && it.value.isNotEmpty()) {
                        put(it.key, it.value)
                    }
                }
            }
        }
        setLoggingInterceptor(
            isDebug = BuildConfig.DEBUG,
            hideVerticalLine = true,
            requestTag = "HTTP Request parameters",
            responseTag = "HTTP Response return parameter"
        )
        // Add interceptor
        setInterceptorList(hashSetOf(ResponseInterceptor(), ParameterInterceptor()))
    }
}

// Need to override, pass in whether the current initial Debug mode.
override fun isLogDebug(a): Boolean {
    // Whether to display logs
    return BuildConfig.DEBUG
}
Copy the code

2. Create the ViewModel extension function

All modules need to rely on the base module to create viewModel-related extension functions VMKxt and Json entity shell BaseEntity.

/** * Filter server result, failed to throw exception *@paramThe block request body method must be decorated with the suspend keyword@paramSuccess Callback *@paramError The callback is not transmitted@paramIsLoading Whether to display Loading layout *@paramLoadingMessage Indicates the contents of the load box */
fun <T> BaseViewModel.request(
    block: suspend() - >BaseResponse<T>,
    success: (T?). ->Unit,
    error: (ResponseThrowable) - >Unit = {},
    isLoading: Boolean = false,
    loadingMessage: String? = null
): Job {
    // Start executing the request
    httpCallback.beforeNetwork.postValue(
        // Execute Loading logicLoadingEntity( isLoading, loadingMessage? .isNotEmpty() ==true, loadingMessage ? :""))return viewModelScope.launch {
        kotlin.runCatching {
            / / request body
            block()
        }.onSuccess {
            // Network request successful, end request
            httpCallback.afterNetwork.postValue(false)
            // Verify that the request result code is correct. If it is incorrect, an exception will be thrown to onFailure
            kotlin.runCatching {
                executeResponse(it) { coroutine ->
                    success(coroutine)
                }
            }.onFailure { error ->
                // An exception occurred while requesting the callback
                valresponseThrowable = ExceptionHandle.handleException(error) httpCallback.onFailed.value = responseThrowable.errorMsg ? :""responseThrowable.errorLog? .let { errorLog -> LogUtil.e(errorLog) }// Execute the failed callback method
                error(responseThrowable)
            }
        }.onFailure { error ->
            // An exception occurred while requesting the callback
            valresponseThrowable = ExceptionHandle.handleException(error) httpCallback.onFailed.value = responseThrowable.errorMsg ? :""responseThrowable.errorLog? .let { errorLog -> LogUtil.e(errorLog) }// Execute the failed callback method
            error(responseThrowable)
        }
    }
}

/** * does not filter server results *@paramThe block request body method must be decorated with the suspend keyword@paramSuccess Callback *@paramError The callback is not transmitted@paramIsLoading Whether to display Loading layout *@paramLoadingMessage Indicates the contents of the load box */
fun <T> BaseViewModel.requestNoCheck(
    block: suspend() - >T,
    success: (T) - >Unit,
    error: (ResponseThrowable) - >Unit = {},
    isLoading: Boolean = false,
    loadingMessage: String? = null
): Job {
    // Start executing the request
    httpCallback.beforeNetwork.postValue(
        // Execute Loading logicLoadingEntity( isLoading, loadingMessage? .isNotEmpty() ==true, loadingMessage ? :""))return viewModelScope.launch {
        runCatching {
            / / request body
            block()
        }.onSuccess {
            // Network request successful, end request
            httpCallback.afterNetwork.postValue(false)
            // Successful callback
            success(it)
        }.onFailure { error ->
            // An exception occurred while requesting the callback
            valresponseThrowable = ExceptionHandle.handleException(error) httpCallback.onFailed.value = responseThrowable.errorMsg ? :""responseThrowable.errorLog? .let { errorLog -> LogUtil.e(errorLog) }// Execute the failed callback method
            error(responseThrowable)
        }
    }
}

/** * Request result filter, determine whether the request server request result is successful, unsuccessful will throw an exception */
suspend fun <T> executeResponse(
    response: BaseResponse<T>,
    success: suspend CoroutineScope. (T?). ->Unit
) {
    coroutineScope {
        when {
            response.isSuccess() -> {
                success(response.getResponseData())
            }
            else- > {throw ResponseThrowable(
                    response.getResponseCode(),
                    response.getResponseMessage(),
                    response.getResponseMessage()
                )
            }
        }
    }
}
Copy the code

The code above encapsulates the fast network request extension function, and you can choose to handle the callback unwrapped or unwrapped, depending on your situation. Call example:

/** * Load list data */
fun getArticleListData(page: Int, pageSize: Int) {
    request(
        {
            filterArticleList(page, pageSize)
        }, {
            // Successful operationit? .let { _articleListData.postValue(it.datas) } } ) }Copy the code

Complete the above steps and you are ready for the fun of development.

3. Introduction of one-click code generation plug-in (optional)

Each time you create an Activity, Fragment, ListActivity, ListFragment is a repetitive work. In order to make the development more efficient and reduce these boring operations, specially written plug-in for fast generation of MVVM code, this plug-in is only applicable to the current MVVM framework. For details, go to the AlvinMVVMPlugin. Once integrated, you can start creating MVVMActivity as you did with EmptyActivity.

Frame structure

mvvm

This component encapsulates commonly used properties for activities and fragments

  • baseIt’s wrapped under the packageMVVMThe basic components of.
    • activityimplementationDataBinding + ViewModelPackage, and some other features.
    • adapterimplementationDataBinding + AdapterThe encapsulation.
    • fragmentimplementationDataBinding + ViewModelPackage, and some other features.
    • livedataimplementationLiveDataEncapsulates basic functionality, such as non-null return values for primitive data types.
    • view_modelimplementationBaseViewModelProcessing.
  • helpThe component’s auxiliary classes are encapsulated under the package, and the global Actiivty and Fragment attributes are assigned in the BaseApplication.
  • managerThe package encapsulates the management of activities.
  • utilsThe LogUtil utility class is wrapped under the package and initialized with BaseApplication.

The Activity to encapsulate

  1. AbstractActivityisActivityAbstract base class, which contains methods that apply to allActivityThe demand. This class encapsulates the abstract methods that all activities must implement.
  2. BaseActivityEncapsulates the baseActivityFunction, mainly used for initializationActivityPublic functions:DataBindingInitialization, immersive status bar,AbstractActivityAbstract method calls, screen adaptation, white space hidden soft keyboard. Specific functions can be added.
  3. BaseDialogActivityOnly displayDialog LoadingPopover, commonly used when submitting requests or local stream processing. You can also extend othersDialog“Like a time picker or something.
  4. BaseContentViewActivityIs used to initialize the layoutActivityHe is our core. So we’re dealing with each of theseActivityThe layout of each state is generally:
    • TitleLayoutPublic title
    • ContentLayoutThe main content layout makes us need the main container for the program content.
    • ErrorLayoutWhen a network request error occurs, a user – friendly prompt is required.
    • LoadingLayoutThe layout is loading data to give the user a good experience, avoiding the first time the page displays a layout without data.
  5. BaseVMActivityimplementationViewModetheActivityBase class, via generic pairsViewModelInstantiate. And throughBaseViewModelPerform common operations.
  6. BaseMVVMActivityallActivityEventually you need to inheritMVVMClass, passed inDataBindingandViewModelIn the construction parameter, also need to getLayoutlayout
  7. BaseListActivityApplicable to listsActivity, pagination operation, pull-up load, pull-down refresh, empty layout, head layout, bottom layout encapsulation.

Fragments encapsulation

Depending on your needs, I prefer a Fragment that has the same functionality as an Activity. That is, the Fragment also has the same functionality as an Activity. This can reduce the difference between activities and fragments when using Navigation. This is a direct reference to the Activity encapsulation

Adapter encapsulation

There are definitely pages of lists in each project, so you also need to DataBinding the Adapter, which is BRVAH.

abstract class BaseBindingListAdapter<T, DB : ViewDataBinding>(
    @LayoutRes private val layoutResId: Int
) : BaseQuickAdapter<T, BaseViewHolder>(layoutResId) {

    abstract fun convert(holder: BaseViewHolder, item: T, dataBinding: DB?).

    override fun convert(holder: BaseViewHolder, item: T) {
        convert(holder, item, DataBindingUtil.bind(holder.itemView))
    }
}
Copy the code

LiveData encapsulation

When LiveData is used, there will be data inversion, which can be described in simple words: A subscribed to the news information on January 1st and B subscribed to the news information on January 15th, but B received the information on January 1st at the same time on January 15th. This obviously does not conform to the logic in our life, so we need to encapsulate LiveData. For details, please check KunMinX’s ** unpeek-Livedata **.

Navigation encapsulation

Through rewriting FragmentNavigator the original FragmentTransaction. The replace () method to replace to hide/Show () ()

The ViewModel encapsulation

Here is a simple example of wrapping LiveData needed for a network request in BaseViewModel

open class BaseViewModel : ViewModel() {

    // The default network request, LiveData
    val httpCallback: HttpCallback by lazy { HttpCallback() }

    inner class HttpCallback {

        /** * Request error ** String = Network request exception */
        val onFailed by lazy { StringLiveData() }

        /** * request started ** LoadingEntity displays loading entity class */
        val beforeNetwork by lazy { EventLiveData<LoadingEntity>() }

        /** * The framework automatically processes loading after the request ends ** false Disables loading or Dialog * true disables loading or Dialog */
        val afterNetwork by lazy { BooleanLiveData() }
    }
}
Copy the code

Auxiliary encapsulation

Most activities and fragments have the same style, such as TitleLayout and LoadingLayout in the layout. So you can encapsulate global helper classes to extract properties from an Activity.

  • Defines the interfaceISettingBaseActivityAdd the extraction method and assign the default value.
  • Defines the interfaceISettingBaseFragmentAdd the extraction method and assign the default value.
  • createISettingBaseActivityandISettingBaseFragmentImplementation class for default custom operations.
  • createGlobalMVVMBuilderFor the assignment

Management encapsulation

Lifecycle is integrated with the stack management of activities in and out of the AppManager.

mvvm_navigation

Separation of Navigation, by rewriting FragmentNavigator the original FragmentTransaction. The replace () method to replace to hide/Show () ().

mvvm_network

Network requests are encapsulated using Retrofit + OkHttp + Moshi and custom exception handling with sealed classes.