Android | MVVM 设计模式的一种实现方式

前言


首先表明,这篇博客 80% 的内容是翻译自 Christopher Elias 的文章 《Understanding MVVM pattern for Android in 2021》。作者的原文题目翻译过来是 《理解 2021 年适用于 Android 的 MVVM 模式》,怕被喷标题党(因为感觉这个题目略大),所以我不太敢直接用原文题目Orz 

本着尊重原创的精神,我是征得原作者同意后才敢翻译的,喏↓↓↓↓↓↓↓↓

image.png

网上介绍 MVVM 的文章有很多,讲得也都很棒!既然网上已经有那么多介绍 MVVM 的文章了,为什么我还是想要翻译这篇呢?

这篇文章它最吸引我的地方在于,作者在数据的获取到将数据渲染到界面的过程中抽象出了一个 State 类,将获取数据后的所有可能结果都封装到这个 State 类中,有很好的高内聚低耦合性,并且结合 Jetpack 组件中的 ViewModel、LiveData 简直不要太好用!所以我想要将这篇文章翻译成中文,一来是希望通过笔记的形式加深自己的印象,二来呢也是希望能让更多人看到这一优秀的实现方式。

基于我的理解,实现了一个小 demo。需求很简单,打开 APP,模拟从网络获取数据(一个水果名 List),并渲染到界面上,如下图。这里给出我的实现

Animation.gif

这里同时贴出 Christopher Elias 的 实现。这是一个大的项目,其中包含了这种实现方式,如果只是想要理解这种设计方式,我觉得看我的实现应该就足够了。Chris 的代码对于不熟悉 Kotlin 的人(譬如我)可能有点难以理解,他用到很多 Kotlin 的高级特性,代码写的非常漂亮,读一读大佬的实现还是可以学到一些东西的。

好啦,那废话不多说,我们开始。

以下内容不做特别说明均是原文翻译。

最后给出了我的实现。


我几乎可以 100% 确定你一定听过 MVC、MVP、MVVM、MVI、MV...。为了能够理解 MVVM 我们需要了解一些基础知识(别担心,我会直接挑重点讲的)。

 

问题是什么?


当我们开发 Android 应用程序时,我们倾向于将所有逻辑放进 Activitys、Fragments、Views 等等。

所以到最后,我们的视图做的就不仅仅是渲染 UI 了。他们可以将数据保存到 SharedPreferences、数据库中,甚至可以发起网络请求,并在一个地方处理所有这些额外的任务。

 

软件设计模式

软件设计模式是在软件设计中,对于给定上下文的常见问题的通用、可重用的解决方案。
维基百科

以上是一个关于软件设计模式的非常简短的定义,如果你想更深入了解,网上有很多资源可供参考。

好的,我们已经知道了问题所在,并且我们也知道有方法可以去解决它。

 


MVVM


V 表示 View,它可以是一个 Activity、Fragment,现在它甚至可以是 Composables 了!ViewModel 表示 Jetpack 组件中的 ViewModel,它是一个可以不受界面配置变化影响而存在的类。

OK,然后让我们把它们组装在一起,我们的 View 去订阅 ViewModel,然后对 Model 的变化做出响应。最后,轮到了 M,M 表示 Model,它是 emmmm 我的 model 是......

等等,什么是 "Model"?网上有很多文章告诉你说 Model 是你获取数据的“地方”,是你的仓库(repository)的所在地,等等。我认为这是错的,我将告诉你为什么。

Model 其实并不是什么新的概念,它最初是由 Trygve Reenskaug 于1979年定义的,作为 MVC 体系结构的一部分。

 “Model 对表示状态、结构和用户心理模型负责。”

 “View 负责展示它从一个或者多个 Model 获取的数据。”

“使 View 依赖于 Model,并且 Model 在发生改变的时候发送适当的信息给它的依赖者。”

可以用下面这个图做个总结:

image.png

模型应该代表着视图当前的状态,可以是加载、成功,或者一个失败的状态。然后视图需要根据当前的状态去渲染 UI。

 

代码


假设我们需要在应用中展示一个电影列表。我们可以用下面这个类来表示状态:

 

/**
 * Represents the state to render the UI in MovieListFragment.
 *
 * @param isLoading if true we have to show a progress bar, else hide the progress bar.
 * @param movies this list will be submited into recyclerview adapter.
 * @param error OneTimeEvent that wraps a failure object for display a Toast, Snackbar, etc only once.
 */
data class MovieListUiState(
    val isLoading: Boolean = false,
    val movies: List<MovieUi> = emptyList(),
    val error: OneTimeEvent<Failure>? = null
)


 

啥是 OneTimeEvent?它只是一个普通的功能类,可以使我们只消耗一个对象一次,这样就可以避免当用户回到屏幕时显示 snackbars、toast 两次。

啥是 Failure?它其实是一个密封类(Sealed Class),可以表示任何类型的错误,你可以使用 Exception、String 等类型的错误表示,只要是可以清楚地告诉你的代码出了什么问题就行。

接下来的问题是,我们如何优雅地渲染界面?

class MovieListFragment : Fragment(R.layout.fragment_movie_list) {

    ...

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        initView()
        collectUiState()
    }

    private fun initView() {
        binding.rvMovies.adapter = MovieListAdapter()
    }

    private fun collectUiState() {
        viewLifecycleOwner.lifecycleScope.launch {
            moviesViewModel.uiState.collect { state ->
                renderUiState(state)
            }
        }
    }

    private fun renderUiState(state: MovieListUiState) {
        with(state) {
            // Progress
            binding.progressBarMovies.isVisible = isLoading

            // Bind movies.
            (binding.rvMovies.adapter as MovieListAdapter)
                .submitList(movies)

            // Empty view
            binding.tvMoviesEmpty.isVisible = !isLoading && movies.isEmpty()

            // Display error if any. Only once.
            error?.let {
                it.consumeOnce { failure ->
                    Toast.makeText(
                        requireContext(),
                        "$failure",
                        Toast.LENGTH_LONG
                    ).show()
                }
            }
        }
    }
  
  ...
}


 

这里解释一下上面代码中的 3 个方法。

  • initView() 只负责初始化 RecyclerView 的 Adapter。
  • collectUiState() 获取 UI 状态 Flow。除了 Flow 以外,也可以使用 LiveData,这不重要。
  • renderUiState(state: MovieListUiState) 负责根据当前状态渲染界面。 


最后,那 ViewModel 呢?ViewModel 是准备数据的(数据可以来自于你的 Repository 等),并且使用返回的结果去修改状态。

好了,以上就是全部啦!现在你应该知道了在你的 MVVM 架构中究竟啥是 Model。

接下来的几天我将上传更多的内容,以帮助您在 2021 年开发出很优秀的 Android 应用。这是一个我的 Playground 项目,并且我将在接下来的博客中写到其中所用到的知识。


我的实现

好啦,以上就是原作者的原文翻译。原作者的实现非常漂亮简洁,其中的 OneTimeEvent 是我第一次了解到,我觉得这是一个非常好的值得借鉴的地方,以后可以用在自己的项目中。

接下来,我将贴出基于我的理解实现的 demo。

image.png

上图是我的项目结构,非常的一目了然吧(狗头)。

我画了个不太标准的示意图:


非依赖倒置.png


首先看一下我这里的 State :
 

data class MainActivityUIState (
    val isLoading: Boolean = false,
    val fruits: List<Fruit> = emptyList(),
    val error: String? = null
)

跟 Chris 实现一样,只是这里简单的使用一个 String 来表示错误信息。

接下来是我的 Model 部分的实现:

class FruitRepository {

    fun getFruitsFromRemote(onGetFruitsListener: OnGetFruitsListener) {

        Thread.sleep(1500)

        onGetFruitsListener.onSuccess(generateFruits())
    }

    private fun generateFruits(): List<Fruit> {
        val fruits: MutableList<Fruit> = ArrayList()

        fruits.apply {
            add(Fruit("apple"))
            add(Fruit("orange"))
            add(Fruit("watermelon"))
            add(Fruit("banana"))
            add(Fruit("peach"))
            add(Fruit("pineapple"))
            add(Fruit("strawberry"))
            add(Fruit("pear"))
        }

        return fruits
    }

    interface OnGetFruitsListener {

        fun onSuccess(fruits: List<Fruit>)

        fun onFailed(error: String)
    }
}


 

getFruitsFromRemote() 方法中通过 Thread.sleep(1500) 模拟网络请求的过程。代码也非常好理解。

接下来是 ViewModel 中,获取 UIState 部分的代码:

    private fun initMainActivityUIState() {
        mainActivityUIState.value = MainActivityUIState(isLoading = true, fruits = emptyList(), error = null)
        Thread(Runnable { kotlin.run {

            fruitRepository.getFruitsFromRemote(object : FruitRepository.OnGetFruitsListener{
                override fun onSuccess(fruits: List<Fruit>) {
                   // mainActivityUIState.value = MainActivityUIState(isLoading = false, fruits = fruits, error = null)
                    mainActivityUIState.postValue(MainActivityUIState(isLoading = false, fruits = fruits, error = null))
                }

                override fun onFailed(error: String) {
                   // mainActivityUIState.value = MainActivityUIState(isLoading = false, fruits = emptyList(), error = error)
                    mainActivityUIState.postValue(MainActivityUIState(isLoading = false, fruits = emptyList(), error = error))
                }
            })

        } }).start()
    }


也非常好理解,就是开启了一个新线程去请求数据。这里需要注意的是在子线程中修改 LiveData 的值必须使用 postValue。最后是 View 部分的代码啦:
 

    private fun initView() {
        activityMainBinding.rvFruits.layoutManager = LinearLayoutManager(this)
        mainActivityViewModel.getMainActivityUIState().observe(this,
            Observer<MainActivityUIState> { t -> renderUIState(t) })
    }

    private fun renderUIState(state: MainActivityUIState?) {
        Log.e(TAG, "render UI")
        with(state!!) {
            activityMainBinding.progressBar.isVisible = isLoading

            activityMainBinding.tvEmpty.isVisible = !isLoading && fruits.isEmpty()

            activityMainBinding.rvFruits.adapter = FruitAdapter(fruits)
        }
    }


 

关键方法就是上面的这两个,初始化界面和渲染界面,也是非常的好理解,并且代码非常简洁。


好啦,以上就是全部内容了!

非常感谢 Christopher Elias 同意我翻译这篇优秀的文章,从他的文章和代码中我也学到了很多。


欢迎关注我呀~

扫码_搜索联合传播样式-白色版.png

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值