美文网首页
从0搭建一个实用的MVVM框架

从0搭建一个实用的MVVM框架

作者: 蜗牛是不是牛 | 来源:发表于2022-03-19 16:19 被阅读0次

    结合Jetpack,构建快速开发的MVVM框架。

    项目使用Jetpack:LiveData、ViewModel、Lifecycle、Navigation组件。

    支持动态加载多状态布局:加载中、成功、失败、标题;

    支持快速生成ListActivity、ListFragment;

    支持使用插件快速生成适用于本框架的Activity、Fragment、ListActivity、ListFragment。

    完整文章前往Github浏览

    前言

    随着GoogleJetpack的完善,对于开发者来说,MVVM显得越来越高效与方便。

    对于使用MVVM的公司来说,都有一套自己的MVVM框架,但是我发现有些只是对框架进行非常简单的封装,导致在开发过程中会出现很多没必要的冗余代码。

    这篇文章主要就是分享如何从0搭建一个高效的MVVM框架。

    基于MVVM进行快速开发, 上手即用。(重构已完成,正在编写SampleApp)

    对基础框架进行模块分离, 分为 MVVM Library--MVVM Navigation Library--MVVM Network Library 可基于业务需求使用 MVVM LibraryMVVM Navigation LibraryMVVM Network Library

    已开发一键生成代码模板, 创建适用于本框架的Activity和Fragment. 具体查看AlvinMVVMPlugin_4_3

    如何集成

    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' }
        }
    }
    
    

    Step 2. Add the dependency

    dependencies {
        // MVVM 基类
        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'
    }
    
    
    说明 依赖地址 版本号
    MVVM 基类 implementation 'com.github.Chen-Xi-g.MVVMFramework:mvvm_framework:Tag' Jetpack 1.0.7
    MVVM Network implementation 'com.github.Chen-Xi-g.MVVMFramework:mvvm_network:Tag' Jetpack 1.0.7
    MVVM Navigation implementation 'com.github.Chen-Xi-g.MVVMFramework:mvvm_navigation:Tag' Jetpack 1.0.7

    依赖引入后,需要初始化依赖,下面是模块化初始化流程。

    1.继承BaseApplication

    创建你的Application类,继承BaseApplication,并且需要在onCreate函数中进行配置和初始化相关参数,可以在这里配置网络请求框架的参数和UI全局参数。比如拦截器和多域名,全局Activity和Fragment属性。

    // 全局Activity设置
    GlobalMVVMBuilder.initSetting(BaseActivitySetting(), BaseFragmentSetting())
    
    private fun initHttpManager() {
        // 参数拦截器
        HttpManager.instance.setting {
            // 设置网络属性
            setTimeUnit(TimeUnit.SECONDS) // 时间类型 秒, 框架默认值 毫秒
            setReadTimeout(30) // 读取超时 30s, 框架默认值 10000L
            setWriteTimeout(30) // 写入超时 30s, 框架默认值 10000L
            setConnectTimeout(30) // 链接超时 30s,框架默认值 10000L
            setRetryOnConnectionFailure(true) // 超时自动重连, 框架默认值 true
            setBaseUrl("https://www.wanandroid.com") // 默认域名
            // 多域名配置
            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 请求参数",
                responseTag = "HTTP Response 返回参数"
            )
            // 添加拦截器
            setInterceptorList(hashSetOf(ResponseInterceptor(), ParameterInterceptor()))
        }
    }
    
    // 需要重写,传入当前是否初始Debug模式。
    override fun isLogDebug(): Boolean {
        // 是否显示日志
        return BuildConfig.DEBUG
    }
    
    

    2.创建ViewModel扩展函数

    所有模块需要依赖的base模块创建ViewModel相关的扩展函数VMKxt和Json实体类壳BaseEntity

    /**
     * 过滤服务器结果,失败抛异常
     * @param block 请求体方法,必须要用suspend关键字修饰
     * @param success 成功回调
     * @param error 失败回调 可不传
     * @param isLoading 是否显示 Loading 布局
     * @param loadingMessage 加载框提示内容
     */
    fun <T> BaseViewModel.request(
        block: suspend () -> BaseResponse<T>,
        success: (T?) -> Unit,
        error: (ResponseThrowable) -> Unit = {},
        isLoading: Boolean = false,
        loadingMessage: String? = null
    ): Job {
        // 开始执行请求
        httpCallback.beforeNetwork.postValue(
            // 执行Loading逻辑
            LoadingEntity(
                isLoading,
                loadingMessage?.isNotEmpty() == true,
                loadingMessage ?: ""
            )
        )
        return viewModelScope.launch {
            kotlin.runCatching {
                //请求体
                block()
            }.onSuccess {
                // 网络请求成功, 结束请求
                httpCallback.afterNetwork.postValue(false)
                //校验请求结果码是否正确,不正确会抛出异常走下面的onFailure
                kotlin.runCatching {
                    executeResponse(it) { coroutine ->
                        success(coroutine)
                    }
                }.onFailure { error ->
                    // 请求时发生异常, 执行失败回调
                    val responseThrowable = ExceptionHandle.handleException(error)
                    httpCallback.onFailed.value = responseThrowable.errorMsg ?: ""
                    responseThrowable.errorLog?.let { errorLog ->
                        LogUtil.e(errorLog)
                    }
                    // 执行失败的回调方法
                    error(responseThrowable)
                }
            }.onFailure { error ->
                // 请求时发生异常, 执行失败回调
                val responseThrowable = ExceptionHandle.handleException(error)
                httpCallback.onFailed.value = responseThrowable.errorMsg ?: ""
                responseThrowable.errorLog?.let { errorLog ->
                    LogUtil.e(errorLog)
                }
                // 执行失败的回调方法
                error(responseThrowable)
            }
        }
    }
    
    /**
     * 不过滤服务器结果
     * @param block 请求体方法,必须要用suspend关键字修饰
     * @param success 成功回调
     * @param error 失败回调 可不传
     * @param isLoading 是否显示 Loading 布局
     * @param loadingMessage 加载框提示内容
     */
    fun <T> BaseViewModel.requestNoCheck(
        block: suspend () -> T,
        success: (T) -> Unit,
        error: (ResponseThrowable) -> Unit = {},
        isLoading: Boolean = false,
        loadingMessage: String? = null
    ): Job {
        // 开始执行请求
        httpCallback.beforeNetwork.postValue(
            // 执行Loading逻辑
            LoadingEntity(
                isLoading,
                loadingMessage?.isNotEmpty() == true,
                loadingMessage ?: ""
            )
        )
        return viewModelScope.launch {
            runCatching {
                //请求体
                block()
            }.onSuccess {
                // 网络请求成功, 结束请求
                httpCallback.afterNetwork.postValue(false)
                //成功回调
                success(it)
            }.onFailure { error ->
                // 请求时发生异常, 执行失败回调
                val responseThrowable = ExceptionHandle.handleException(error)
                httpCallback.onFailed.value = responseThrowable.errorMsg ?: ""
                responseThrowable.errorLog?.let { errorLog ->
                    LogUtil.e(errorLog)
                }
                // 执行失败的回调方法
                error(responseThrowable)
            }
        }
    }
    
    /**
     * 请求结果过滤,判断请求服务器请求结果是否成功,不成功则会抛出异常
     */
    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()
                    )
                }
            }
        }
    }
    
    

    以上代码封装了快速的网络请求扩展函数,并且可以根据自己的情况,选择脱壳或者不脱壳的回调处理。 调用示例:

    /**
     * 加载列表数据
     */
    fun getArticleListData(page: Int, pageSize: Int) {
        request(
            {
                filterArticleList(page, pageSize)
            }, {
                // 成功操作
                it?.let {
                    _articleListData.postValue(it.datas)
                }
            }
        )
    }
    
    

    完成上面的操作,你就可以进入愉快的开发工作了。

    3.引入一键生成代码插件(可选)

    每次创建Activity、Fragment、ListActivity、ListFragment都是重复的工作,为了可以更高效的开发,减少这些枯燥的操作,特地编写的快速生成MVVM代码的插件,该插件只适用于当前MVVM框架,具体使用请前往AlvinMVVMPlugin。集成后你就可以开始像创建EmptyActivity这样创建MVVMActivity

    框架结构

    mvvm

    该组件对Activity和Fragment进行常用属性封装

    • base包下封装了MVVM的基础组件。
      • activity实现DataBinding + ViewModel的封装,以及一些其他功能。
      • adapter实现DataBinding + Adapter的封装。
      • fragment实现DataBinding + ViewModel的封装,以及一些其他功能。
      • livedata实现LiveData的基础功能封装,如基本数据类型的非空返回值。
      • view_model实现BaseViewModel的处理。
    • help包下封装了组件的辅助类,在BaseApplication中进行全局Actiivty、Fragment属性赋值。
    • manager包下封装了对Activity的管理。
    • utils包下封装了LogUtil工具类,通过BaseApplication进行初始化。

    Activity封装

    1. AbstractActivityActivity的抽象基类,这个类里面的方法适用于全部Activity的需求。 该类中封装了所有Activity必须实现的抽象方法。
    2. BaseActivity封装了基础的Activity功能,主要用来初始化Activity公共功能:DataBinding的初始化、沉浸式状态栏、AbstractActivity抽象方法的调用、屏幕适配、空白区域隐藏软键盘。具体功能可以自行新增。
    3. BaseDialogActivity只负责显示Dialog Loading弹窗,一般在提交请求或本地流处理时使用。也可以扩展其他的Dialog,比如时间选择器之类。
    4. BaseContentViewActivity是对布局进行初始化操作的Activity,他是我们的核心。这里处理了每个Activity的每个状态的布局,一般情况下有:
      • TitleLayout 公共标题
      • ContentLayout 主要的内容布局,使我们需要程序内容的主要容器。
      • ErrorLayout 当网络请求发生错误,需要对用户进行友好的提示。
      • LoadingLayout 正在加载数据的布局,给用户一个良好的体验,避免首次进入页面显示的布局没有数据。
    5. BaseVMActivity实现ViewModeActivity基类,通过泛型对ViewModel进行实例化。并且通过BaseViewModel进行公共操作。
    6. BaseMVVMActivity 所有Activity最终需要继承的MVVM类,通过传入DataBindingViewModel的泛型进行初始化操作,在构造参数中还需要获取Layout布局
    7. BaseListActivity适用于列表的Activity,分页操作、上拉加载、下拉刷新、空布局、头布局、底布局封装。

    Fragment封装

    根据你的需要进行不同的封装,我比较倾向于和Activity具有相同功能的封装,也就是Activity封装的功能我Fragment也要有。这样在使用Navigation的时候可以减少ActivityFragment的差异。这里直接参考Activity的封装

    Adapter封装

    每个项目中肯定会有列表的页面,所以还需要对Adapter进行DataBinding适配,这里使用的Adapter是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))
        }
    }
    
    

    LiveData封装

    LiveData在使用的时候会出现数据倒灌的情况,用简单的话来描述数据倒灌:A订阅1月1日新闻信息,B订阅1月15日新闻信息,但是B在1月15日同时收到了1月1日的信息,这明显不符合我们生活中的逻辑,所以需要对LiveData进行封装,详细的可以查看KunMinX的**UnPeek-LiveData**。

    Navigation封装

    通过重写 FragmentNavigator 将原来的 FragmentTransaction.replace() 方法替换为 hide()/Show()

    ViewModel封装

    BaseViewModel中封装一个网络请求需要用的LiveData,下面是一个简单的示例

    open class BaseViewModel : ViewModel() {
    
        // 默认的网络请求LiveData
        val httpCallback: HttpCallback by lazy { HttpCallback() }
    
        inner class HttpCallback {
    
            /**
             * 请求发生错误
             *
             * String = 网络请求异常
             */
            val onFailed by lazy { StringLiveData() }
    
            /**
             * 请求开始
             *
             * LoadingEntity 显示loading的实体类
             */
            val beforeNetwork by lazy { EventLiveData<LoadingEntity>() }
    
            /**
             * 请求结束后框架自动对 loading 进行处理
             *
             * false 关闭 loading or Dialog
             * true 不关闭 loading or Dialog
             */
            val afterNetwork by lazy { BooleanLiveData() }
        }
    }
    
    

    辅助类封装

    大部分的ActivityFragment样式基本相同,比如布局中的TitleLayoutLoadingLayout这些都是统一样式。所以可以封装全局的辅助类来对Activity中的属性进行抽离。

    • 定义接口ISettingBaseActivity添加抽离的方法,并且赋于默认值。
    • 定义接口ISettingBaseFragment添加抽离的方法,并且赋于默认值。
    • 创建ISettingBaseActivityISettingBaseFragment的实现类,进行默认的自定义操作。
    • 创建GlobalMVVMBuilder进行赋值

    管理类封装

    通过Lifecycle结合AppManager对Activity的进出栈管理。

    mvvm_navigation

    分离Navigation,通过重写 FragmentNavigator 将原来的 FragmentTransaction.replace() 方法替换为 hide()/Show()。

    mvvm_network

    使用 Retrofit + OkHttp + Moshi 对网络请求进行封装,使用密封类自定义异常处理。

    推荐

    相关文章

      网友评论

          本文标题:从0搭建一个实用的MVVM框架

          本文链接:https://www.haomeiwen.com/subject/iktndrtx.html