Android Jetpack系列之MVVM使用及封装

简介: MVVM使用及封装详解

Android开发架构

如果开发过程中大家各自为战,没有统一规范,久而久之,项目代码会变得混乱且后续难以维护。当使用统一的架构模式后,有很多的好处,如:

  • 统一开发规范,使得代码整洁、规范,后续易于维护及扩展
  • 提高开发效率(尤其在团队人员较多时)
  • 模块单一职责,使得模块专注自己内部(面向对象),模块间解耦

总之,开发架构是前人总结出来的一套行之有效的开发模式,目的是达到高内聚,低耦合的效果,使得项目代码更健壮、易维护。

Android中常见的架构模式有MVC(Model-View-Controller)MVP(Model-View-Presenter)MVVM(Model-View-ViewModel),一起来看下各自的特点:

MVC

MVC(Model-View-Controller)是比较早期的架构模式,模式整体也比较简单。

mvc.png
MVC模式将程序分成了三个部分:

  • Model模型层:业务相关的数据(如网络请求数据、本地数据库数据等)及其对数据的处理
  • View视图层:页面视图(通过XML布局编写视图层),负责接收用户输入、发起数据请求及展示结果页面
  • Controller控制器层:M与V之间的桥梁,负责业务逻辑

MVC特点:

  • 简单易用:上图表述了数据整个流程:View接收用户操作,通过Controller去处理业务逻辑,并通过Model去获取/更新数据,然后Model层又将最新的数据传回View层进行页面展示。
  • 架构简单的另一面往往是对应的副作用:由于XML布局能力弱,我们的View层的很多操作都是写在Activity/Fragment中,同时,Controller、Model层的代码也大都写在Activity/Fragment中,这就会导致一个问题,当业务逻辑比较复杂时,Activity/Fragment中的代码量会很大,其违背了类单一职责,不利于后续扩展及维护。尤其是后期你刚接手的项目,一个Activity/Fragment类中的代码动辄上千行代码,那感觉着实酸爽:

beng
当然,如果业务很简单,使用MVC模式还是一种不错的选择。

MVP

MVP(Model-View-Presenter),架构图如下:

mvp.png

MVP各模块职责如下

  • Model模型:业务相关的数据(如网络请求数据、本地数据库数据等)及其对数据的处理
  • View视图:页面视图(Activity/Fragment),负责接收用户输入、发起数据请求及展示结果页面
  • Presenter:M与V之间的桥梁,负责业务逻辑

MVP特点
View层接收用户操作,并通过持有的Presenter去处理业务逻辑,请求数据;接着Presenter层通过Model去获取数据,然后Model又将最新的数据传回Presenter层,Presenter层又持有View层的引用,进而将数据传给View层进行展示。

MVP相比MVC的几处变化

  • View层与Model层不再交互,而是通过Presenter去进行联系
  • 本质上MVP是面向接口编程,Model/View/Presenter每层的职责分工明确,当业务复杂时,整个流程逻辑也是很清晰的

当然,MVP也不是十全十美的,MVP本身也存在以下问题:

  • View层会抽象成IView接口,并在IView中声明一些列View相关的方法;同样的,Presenter会被抽象成IPresenter接口及其一些列方法,每当实现一个功能时,都需要编写多个接口及其对应的方法,实现起来相对比较繁琐,而且每次有改动时,对应的接口方法也基本都会再去改动。
  • View层与Presenter层相互持有,当View层关闭时,由于Presenter层不是生命周期感知的,可能会导致内存泄漏甚至是崩溃。

ps:如果你的项目中使用了RxJava,可以使用 AutoDispose 自动解绑。

MVVM

MVVM(Model-View-ViewModel),架构图如下:
mvvm.png

MVVM各职责如下

  • Model模型:业务相关的数据(如网络请求数据、本地数据库数据等)及其对数据的处理
  • View视图:页面视图(Activity/Fragment),负责接收用户输入、发起数据请求及展示结果页面
  • ViewModel:M与V之间的桥梁,负责业务逻辑

MVVM特点

  • View层接收用户操作,并通过持有的ViewModel去处理业务逻辑,请求数据;
  • ViewModel层通过Model去获取数据,然后Model又将最新的数据传回ViewModel层,到这里,ViewModel与Presenter所做的事基本是一样的。但是ViewModel不会也不能持有View层的引用,而是View层会通过观察者模式监听ViewModel层的数据变化,当有新数据时,View层能自动收到新数据并刷新界面。

UI驱动 vs 数据驱动

MVP中,Presenter中需要持有View层的引用,当数据变化时,需要主动调用View层对应的方法将数据传过去并进行UI刷新,这种可以认为是UI驱动;而MVVM中,ViewModel并不会持有View层的引用,View层会监听数据变化,当ViewModel中有数据更新时,View层能直接拿到新数据并完成UI更新,这种可以认为是数据驱动,显然,MVVM相比于MVP来说更加解耦。

MVVM的具体实现

上面介绍了MVC/MVP/MVVM的各自特点,其中MVC/MVP的具体使用方式,本文不再展开实现,接下来主要聊一下MVVM的使用及封装,MVVM也是官方推荐的架构模式。

Jetpack MVVM

Jetpack是官方推出的一系列组件库,使用组件库开发有很多好处,如:

  • 遵循最佳做法:采用最新的设计方法构建,具有向后兼容性,可以减少崩溃和内存泄漏
  • 消除样板代码:开发者可以更好地专注业务逻辑
  • 减少不一致:可以在各种Android版本中运行,兼容性更好。

为了实现上面的MVVM架构模式,Jetpack提供了多个组件来实现,具体来说有Lifecycle、LiveData、ViewModel(这里的ViewModel是MVVM中ViewModel层的具体实现),其中Lifecycle负责生命周期相关;LiveData赋予类可观察,同时还是生命周期感知的(内部使用了Lifecycle);ViewModel旨在以注重生命周期的方式存储和管理界面相关的数据,针对这几个库的详细介绍及使用方式不再展开,有兴趣的可以参见前面的文章:

通过这几个库,就可以实现MVVM了,官方也发布了MVVM的架构图:

Jetpack MVVM架构图.png

其中Activity/FragmentView层,ViewModel+LiveDataViewModel层,为了统一管理网络数据及本地数据数据,又引入了Repository中间管理层,本质上是为了更好地管理数据,为了简单把他们统称为Model层吧。

使用举例

  • View层代码:
//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", true)
    }

    override fun init() {
        //获取ViewModel实例,注意这里不能直接new,因为ViewModel的生命周期比Activity长
        mViewModel = ViewModelProvider(this).get(WanViewModel::class.java)

        mBtnQuest.setOnClickListener {
            //请求数据
            mViewModel.getWanInfo()
        }

        //ViewModel中的LiveData注册观察者并监听数据变化
        mViewModel.mWanLiveData.observe(this) { 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()
        }
    }
}
  • ViewModel层代码:
//WanViewModel.kt
class WanViewModel : ViewModel() {
    //LiveData
    val mWanLiveData = MutableLiveData<List<WanModel>>()
    //loading
    val loadingLiveData = SingleLiveData<Boolean>()
    //异常
    val errorLiveData = SingleLiveData<String>()

    //Repository中间层 管理所有数据来源 包括本地的及网络的
    private val mWanRepo = WanRepository()

    fun getWanInfo(wanId: String = "") {
        //展示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)
            }
        }
    }
}
  • Repository层(Model层)代码:
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) {
            //正确
            baseData.state = State.Success
        } else {
            //错误
            baseData.state = State.Error
        }
        return baseData
    }
}

这里只通过Retrofit请求了网络数据 玩Android 开放API,如果需要添加本地数据,只需要在方法里添加本地数据处理即可,即 Repository是数据的管理中间层,对数据进行统一管理,ViewModel层中不需要关心数据的来源,大家各司其职即可,符合单一职责,代码可读性更好,同时也更加解耦。在View层点击按钮请求数据,执行结果如下:

MVVM
以上就完成了一次网络请求,相比于MVPMVVM既不用声明多个接口及方法,同时ViewModel也不会像Presenter那样去持有View层的引用,而是生命周期感知的,MVVM方式更加解耦。

封装

上一节介绍了Jetpack MVVM的使用例子,可以看到有一些代码逻辑是可以抽离出来封装到公共部分的,那么本节就尝试对其做一次封装。

首先,请求数据时可能会展示Loading,请求完后可能是空数据、错误数据,对应下面的IStatusView接口声明:

interface IStatusView {
    fun showEmptyView() //空视图
    fun showErrorView(errMsg: String) //错误视图
    fun showLoadingView(isShow: Boolean) //展示Loading视图
}

因为ViewModel是在Activity中初始化的,所以可以封装成一个Base类:

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()
    }

    /**
     * 获取ViewModel 子类可以复写,自行初始化
     */
    protected open fun getViewModel(): VM? {
        //当前对象超类的Type
        val type = javaClass.genericSuperclass
        //ParameterizedType表示参数化的类型
        if (type != null && type is ParameterizedType) {
            //返回此类型实际类型参数的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() {
       ......
    }

    //错误视图 并且可以重试
    override fun showErrorView(errMsg: String) {
       .......
    }

    private fun registerEvent() {
       //接收错误信息
       mViewModel.errorLiveData.observe(this) { errMsg ->
           showErrorView(errMsg)
       }
       //接收Loading信息
       mViewModel.loadingLiveData.observe(this, { isShow ->
           showLoadingView(isShow)
       })
    }

    abstract fun init()
}

Base类中初始化ViewModel,还可以通过官方activity-ktxfragment-ktx扩展库,初始化方式:val model: VM by viewModels()

子类中继承如下:

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 {
            //请求数据
            mViewModel.getWanInfo()
        }
        /**
         * 这里使用了扩展函数,等同于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()
        }
    }
}

我们把ViewModel的初始化放到了父类里进行,代码看上去更简单了。监听数据变化mViewModel.mWanLiveData.observe(this) {} 方式改成observe(mViewModel.mWanLiveData) {}方式,少传了一个LifecycleOwner,其实这是一个扩展函数,如下:

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

ps:我们初始化View控件时,如 private val mBtnQuest: Button by id(R.id.btn_request),依然使用了扩展函数,如下:

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

不用像写java代码中那样时刻要想着判空,同时只会在使用时才会进行初始化,很实用!

说回来,接着是ViewModel层的封装,BaseViewModel.kt

abstract class BaseViewModel : ViewModel() {
    //loading
    val loadingLiveData = SingleLiveData<Boolean>()
    //异常
    val errorLiveData = SingleLiveData<String>()

    /**
     * @param request 正常逻辑
     * @param error 异常处理
     * @param showLoading 请求网络时是否展示Loading
     */
    fun launchRequest(
        showLoading: Boolean = true,
        error: suspend (String) -> Unit = { errMsg ->
            //默认异常处理,子类可以进行覆写
            errorLiveData.postValue(errMsg)
        }, request: suspend () -> Unit
    ) {
        //是否展示Loading
        if (showLoading) {
            loadStart()
        }
      
        //使用viewModelScope.launch开启协程
        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)
    }
}

扩展一下
1、上面执行网络请求时,使用viewModelScope.launch来启动协程,引入方式:

implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'

这样就可以直接在ViewModel中启动协程并且当ViewModel生命周期结束时协程也会自动关闭,避免使用GlobalScope.launch { }MainScope().launch { }还需自行关闭协程,
当然,如果是在Activity/Fragment、liveData中使用协程,也可以按需引入:

implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'

具体可以参见官方的 将 Kotlin 协程与生命周期感知型组件一起使用 这篇文章。

2、另外细心的读者可能观察到,上面我们的Loading、Error信息监听都是用的SingleLiveData,把这个类打代码贴一下:

/**
 * 多个观察者存在时,只有一个Observer能够收到数据更新
 * https://github.com/android/architecture-samples/blob/dev-todo-mvvm-live/todoapp/app/src/main/java/com/example/android/architecture/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 ->
            //如果expect为true,那么将值update为false,方法整体返回true,
            //即当前Observer能够收到更新,后面如果还有订阅者,不能再收到更新通知了
            if (mPending.compareAndSet(true, false)) {
                observer.onChanged(t)
            }
        }
    }

    override fun setValue(@Nullable value: T?) {
        //AtomicBoolean中设置的值设置为true
        mPending.set(true)
        super.setValue(value)
    }

    /**
     * Used for cases where T is Void, to make calls cleaner.
     */
    @MainThread
    fun call() {
        value = null
    }
}

可以看到SingleLiveData还是继承自MutableLiveData,区别是当多个观察者存在时,只有一个Observer能够收到数据更新,本质上是在observe()时通过CAS加了限制,注释已经很详细了,不再赘述。

子类中继承如下:

class WanViewModel : BaseViewModel() {
    //LiveData
    val mWanLiveData = MutableLiveData<List<WanModel>>()

    //Repository中间层 管理所有数据来源 包括本地的及网络的
    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)
            }
        }
    }
}

最后是对Model层的封装,BaseRepository.kt

open class BaseRepository {
    suspend fun <T : Any> executeRequest(
        block: suspend () -> BaseData<T>
    ): BaseData<T> {
        val baseData = block.invoke()
        if (baseData.code == 0) {
            //正确
            baseData.state = State.Success
        } else {
            //错误
            baseData.state = State.Error
        }
        return baseData
    }
}

数据基类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
}

子类中继承如下:

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

到此,基本上就完成了,代码中有一些细节没贴出来,完整代码参见:Jetpack MVVM

参考

【1】Android应用架构指南

【2】https://www.php.cn/faq/417265.html

相关文章
|
4月前
|
安全 Java Android开发
安卓开发中的新趋势:Kotlin与Jetpack的完美结合
【6月更文挑战第20天】在不断进化的移动应用开发领域,Android平台以其开放性和灵活性赢得了全球开发者的青睐。然而,随着技术的迭代,传统Java语言在Android开发中逐渐显露出局限性。Kotlin,一种现代的静态类型编程语言,以其简洁、安全和高效的特性成为了Android开发中的新宠。同时,Jetpack作为一套支持库、工具和指南,旨在帮助开发者更快地打造优秀的Android应用。本文将探讨Kotlin与Jetpack如何共同推动Android开发进入一个新的时代,以及这对开发者意味着什么。
|
22天前
|
编译器 Android开发 开发者
带你了解Android Jetpack库中的依赖注入框架:Hilt
本文介绍了Hilt,这是Google为Android开发的依赖注入框架,基于Dagger构建,旨在简化依赖注入过程。Hilt通过自动化的组件和注解减少了DI的样板代码,提高了应用的可测试性和可维护性。文章详细讲解了Hilt的主要概念、基本用法及原理,帮助开发者更好地理解和应用Hilt。
40 8
|
24天前
|
安全 Java Android开发
探索安卓应用开发的新趋势:Kotlin和Jetpack Compose
在安卓应用开发领域,随着技术的不断进步,新的编程语言和框架层出不穷。Kotlin作为一种现代的编程语言,因其简洁性和高效性正逐渐取代Java成为安卓开发的首选语言。同时,Jetpack Compose作为一个新的UI工具包,提供了一种声明式的UI设计方法,使得界面编写更加直观和灵活。本文将深入探讨Kotlin和Jetpack Compose的特点、优势以及如何结合使用它们来构建现代化的安卓应用。
34 4
|
2月前
|
存储 前端开发 Java
Android MVVM框架详解与应用
在Android开发中,随着应用复杂度的增加,如何有效地组织和管理代码成为了一个重要的问题。MVVM(Model-View-ViewModel)架构模式因其清晰的结构和高效的开发效率,逐渐成为Android开发者们青睐的架构模式之一。本文将详细介绍Android MVVM框架的基本概念、优势、实现流程以及一个实际案例。
|
3月前
|
存储 数据库 Android开发
🔥Android Jetpack全解析!拥抱Google官方库,让你的开发之旅更加顺畅无阻!🚀
【7月更文挑战第28天】在Android开发中追求高效稳定的路径?Android Jetpack作为Google官方库集合,是你的理想选择。它包含多个独立又协同工作的库,覆盖UI到安全性等多个领域,旨在减少样板代码,提高开发效率与应用质量。Jetpack核心组件如LiveData、ViewModel、Room等简化了数据绑定、状态保存及数据库操作。引入Jetpack只需在`build.gradle`中添加依赖。例如,使用Room进行数据库操作变得异常简单,从定义实体到实现CRUD操作,一切尽在掌握之中。拥抱Jetpack,提升开发效率,构建高质量应用!
53 4
|
2月前
|
Android开发 iOS开发
Android项目架构设计问题之将隐式跳转的逻辑进行抽象和封装如何解决
Android项目架构设计问题之将隐式跳转的逻辑进行抽象和封装如何解决
29 0
|
2月前
|
编解码 API 开发工具
Android平台轻量级RTSP服务模块二次封装版调用说明
本文介绍了Android平台上轻量级RTSP服务模块的二次封装实践,旨在简化开发流程,让开发者能更专注于业务逻辑。通过`LibPublisherWrapper`类提供的API,可在应用中轻松初始化RTSP服务、配置视频参数(如分辨率、编码类型)、启动与停止RTSP服务及流发布,并获取RTSP会话数量。此外,还展示了如何处理音频和视频数据的采集与推送。最后,文章提供了从启动服务到销毁资源的完整示例,帮助开发者快速集成实时流媒体功能。
|
3月前
|
存储 前端开发 测试技术
Android Kotlin中使用 LiveData、ViewModel快速实现MVVM模式
使用Kotlin实现MVVM模式是Android开发的现代实践。该模式分离UI和业务逻辑,借助LiveData、ViewModel和DataBinding增强代码可维护性。步骤包括创建Model层处理数据,ViewModel层作为数据桥梁,以及View层展示UI。添加相关依赖后,Model类存储数据,ViewModel类通过LiveData管理变化,而View层使用DataBinding实时更新UI。这种架构提升代码可测试性和模块化。
145 2
|
3月前
|
存储 移动开发 Android开发
使用kotlin Jetpack Compose框架开发安卓app, webview中h5如何访问手机存储上传文件
在Kotlin和Jetpack Compose中,集成WebView以支持HTML5页面访问手机存储及上传音频文件涉及关键步骤:1) 添加`READ_EXTERNAL_STORAGE`和`WRITE_EXTERNAL_STORAGE`权限,考虑Android 11的分区存储;2) 配置WebView允许JavaScript和文件访问,启用`javaScriptEnabled`、`allowFileAccess`等设置;3) HTML5页面使用`<input type="file">`让用户选择文件,利用File API;
|
4月前
|
Android开发
Jetpack Compose: Hello Android
Jetpack Compose: Hello Android
下一篇
无影云桌面