美文网首页
闲扯Android的MVVM开发模式

闲扯Android的MVVM开发模式

作者: 超级绿茶 | 来源:发表于2019-12-07 20:51 被阅读0次

网上关于怎么用MVVM搭建Android项目的文章已经非常普遍了,很多都是有深度有内涵的长篇或连续剧文章。我的目的不是要效仿那些前辈高人那种写学术性的文章,而是想用一种简单快速或者说不靠谱的方式来阐述下Android的MVVM是怎么回事。

首先咱们先BB几句,模式本身没有好坏之分,Android开发一直是按MVC模式设计,包括Java Swing、JavaFX、JavaEE的Spring都是采用的是MVC模式。只是说在Android中Activity和Fragment即是V层又是C层,业务逻辑和视图表现混合在一起,耦合度很高(我也看到有说XML布局才是V层的说法)。

所以为了解耦才有了MVP模式,MVP也流行过一段日子,把业务逻辑单独放到P层,然后通过接口和V层M层操作,V层和M层无法相互操作。这模式的缺点就是项目写起来太费劲,文件特别多,尤其是接口文件一堆一堆的(曾经接盘一个别人写的项目,刚开始那哥么挻认真的用MVP,但怎奈业务不断的迭代,后期改为MVC了,再后来这锅就砸我手上了……)。

后来就有了MVVM模式,但那句话怎么说来着:一千个人心中有一千个哈姆雷特。所以一千个人写MVVM就有一千个MVVM。正当MVVM也要走上MVP的结局时;谷歌出手了,在2017年时干了两件事,一是扶正了Kotlin成为首席语言,二是推出了一套Android Architecture Components,以后简称AAC。AAC是用来干什么的?简单的说:就是为规范MVVM的写法,免得你们这帮开发者八仙过海各显神通,写出五花八门的MVVM。(话说回来,如果你是学术派的话最好还是自己去看看官网上关于AAC的定义)

现在就说一下Android的MVVM要怎么搭建;先简单说一个MVVM:

  • M-Model层:数据模式层,就是项目里定义的各种实体类JavaBean。
  • V-View层:视图控件层,就是Activity、Fragment和各种定义的View控件。
  • VM-ViewModel层:视图和数据的交互层,就是在这里写业务逻辑的地方,通俗的讲就是在这里调接口、读写文件然后刷新数据。

那么AAC又是怎么规范我们的MVVM的?这要从AAC的组件说起:

  • Lifecycles:生命周期管理组件。主要是可以订阅Activity或Fragment的各种生命周期。
  • LiveData:基于观察者模式的数据容器。容器的目的是为了将数据和界面的生命周期进行绑定,同时以观察者模式对数据进行订阅,当数据发现更改时可以立即刷新界面。
  • ViewModel:让Activity和LiveData产生关联的组件。我们在这个组件里进行各种业务逻辑的处理。组件是直接绑定在Activity上的(暂且这样理解,其实是绑定在一个隐藏的Fragment上的),并将这个Activity作为作用域,并在作用域内以单例形式存在,这样就方便在这个Activity的多个Fragment或控件之间共享数据。

多说无益,这里还是结合实例来说明,继续拿天气预报的项目来演示。

先引入些必要的东西:

    // Anko
    implementation "org.jetbrains.anko:anko:0.10.8"
    implementation "org.jetbrains.anko:anko-commons:0.10.8"
    // gson解析
    implementation 'com.google.code.gson:gson:2.8.5'
    // 协程
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2'
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2"
    // AAC
    implementation "androidx.lifecycle:lifecycle-extensions:2.1.0"
    implementation group: 'androidx.lifecycle', name: 'lifecycle-viewmodel-ktx', version: '2.2.0-rc03'
    // 多用途adapter
    implementation 'com.github.mcxtzhang:all-base-adapter:V1.8.0'
    // RecyclerView
    implementation 'androidx.recyclerview:recyclerview:1.1.0'

至于布局界面我这里就不贴出来了,直接放张截图,你们应该一看就明白。


image.png

界面分上下两部分,上部放三个RecyclerView列表,分别显示省、市、区,然后根据选择的省市区在下方显示对应的当地天气情况,就这么简单。

现在进入重点环节,我们要自定义一个名为WeatherModel的类,用来处理业务逻辑,这个类继承自ViewModel。ViewModel是一个抽象类,定义如下 :

public abstract class ViewModel {
    ...
}

我们定义的WeatherModel类如下:

class WeatherModel : ViewModel() { 
}

由于ViewModel内没有用abstract修饰的方法名,所以我们定义出来的类也空的,以后再把我们需要的业务逻辑填写进去。现在再来说下这个WeatherModel要怎么实例化。

千万不要不直接new出一个对象,正规的操作应该是这样的:

val model = ViewModelProviders.of(this).get(WeatherModel::class.java)
ViewModelProviders其实是一个工厂模式,用于生产一个继承自ViewModel类的实例。在这里我们用它来生产一个WeatherModel的实例。

  • of()方法需要传一个Activity或Fragment的实例。
  • get()方法用来指定要生产的ViewModel的子类实例。

这里要注意的是ViewModelProviders只能运行在主线程上,且生产出来的ViewModel实例在整个Activity的作用域内保持单例(Fragment是依附于Activity的),也就说我们在同一个Activity内多次调用ViewModelProviders的get方法返回的始终是同一个实例。

好了,现在我们来讲一下LiveData,LiveData也是一个抽象类,但一般我们不直接用这个类,而是用它的子类MutableLiveData。可以直接new出一个实例使用。例如这样:

var errorMessage = MutableLiveData<String>()

构造器里的泛型指的就是我们需要放到LiveData里的数据类型,毕竟LiveData就是一个装数据的容器。我们可以通过setValue或postValue把数据(或对象实例)放进去。setValue只能在主线程调用,postValue即可以在主线程也可以子线程调用。

前文本说过LiveData是基于观察者模式的,也就是说我们可以对LiveData里数据的进行订阅,然后当我们用setValue或postValue修改容器里的值时就会被观察到,这里我们对之前的errorMessage容器订阅,如果容器里的值被修改的话就在弹一个toast:

errorMessage.observe(this, Observer {
        toast(it)
})

熟悉RxJava的应该对这种代码非常亲切吧!这里我们看一下observe方法的定义:

@MainThread
public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer)

从定义我们可以看出observe方法必须运行在主线程上,需要传入一个生命周期管理组件和一个类型为Observer的接口实例。Observer定义如下:

public interface Observer<T> {
    void onChanged(@Nullable T t);
}

当容器里的值变更后onChanged方法就会被触发。我们前的toast就是写在onChanged方法内部的,只是这个方法名被Kotlin给简化掉了。

到此你应该知道LiveData是个什么东西了吧,那现在有个问题,我们应该把LiveData类的实例写在哪里?还记得我们的WeatherModel类还空着吗?典型的作法就是把LiveData类定义成ViewModel的类属性,然后在ViewModel中执行业务操作,把得到的值放到LiveData的实例当中,而这在此之前就已经在视图层Activity或Fragment里已经订阅了LiveData的事件,当值变动后就能刷新到界面上了。

现在就把完成后的WeatherModel放出来

import android.util.Log
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.Exception

//接口地址
private const val CITY_URL = "http://v.juhe.cn/weather/citys"
private const val INDEX_URL = "http://v.juhe.cn/weather/index"
private const val KEY = "3bc829216bb4ede1e846fe91b3df5543"

class WeatherModel : ViewModel() {
    private var showLoading = MutableLiveData<Boolean>() // 是否显示loading
    private var errorMessage = MutableLiveData<String>() // 出错时的错误信息
    private var listProvince = MutableLiveData<List<String>>() // 省列表
    private var listCity = MutableLiveData<List<String>>() // 市列表
    private var listDistrict = MutableLiveData<List<String>>() // 区列表
    private var detail = MutableLiveData<String>() // 城市天气详情

    fun isShowLoading() = showLoading
    fun getErrorMessage() = errorMessage
    fun getProvinceList() = listProvince
    fun getCityList() = listCity
    fun getDistrictList() = listDistrict
    fun getDetail() = detail

    private var listResult = mutableListOf<CityBean.ResultBean>()
    private var selectedProvince: String? = null
    private var selectedCity: String? = null
    private var selectedDistrict: String? = null

    /**
     * 调接口获取全部的省市区列表
     */
    fun requestCityList() = viewModelScope.launch {
        showLoading.value = true
        var response: String? = null
        // 在子线程上执行网络操作
        withContext(Dispatchers.IO) {
            response = try {
                SimpleHttpUtils.get(CITY_URL, mapOf("key" to KEY))
            } catch (e: Exception) {
                e.printStackTrace()
                //在子线程上给LiveData赋值必须用这个方法,否则出错
                errorMessage.postValue(e.message) 
                null
            }
        }
        showLoading.value = false
        if (!response.isNullOrEmpty()) {
            Log.i("123", response)
            parseResponse(response!!) // 对接口的返回值进行解析
        }
    }

    /**
     * 解析接口的反馈值
     */
    private fun parseResponse(response: String) {
        val result = CityBean.objectFromData(response)
        if (result.resultcode == "200") {
            listResult = result.result
            getAllProvince()
        } else {
            errorMessage.value = result.reason
        }
    }

    private fun getAllProvince() {
        val provinces = listResult.groupBy { it.province }.keys.toList()
        listProvince.value = provinces
    }

    fun setProvince(province: String) {
        selectedProvince = province
        getCitysByProvince(province)
    }

    fun getProvince() = selectedProvince

    private fun getCitysByProvince(province: String) {
        val citys = listResult.filter { it.province == province }
            .distinctBy { it.city }
            .map { it.city }
            .toList()
        listCity.value = citys
    }

    fun getCity() = selectedCity

    fun setCity(city: String) {
        selectedCity = city
        getDistrictByCity(city)
    }

    private fun getDistrictByCity(city: String) {
        val districts = listResult.filter { it.city == city }
            .map { it.district }
            .toList()
        listDistrict.value = districts
    }

    fun getDistrict() = selectedDistrict

    fun setDistrict(district: String) {
        selectedDistrict = district
        requestDetail()
    }

    /**
     * 调接口获取城市区域的天气情况
     */
    private fun requestDetail() = viewModelScope.launch {
        showLoading.value = true
        val response = try {
            withContext(Dispatchers.IO) {
                SimpleHttpUtils.get(
                    INDEX_URL, mapOf(
                        "format" to "1",
                        "cityname" to selectedDistrict!!,
                        "key" to KEY
                    )
                )
            }
        } catch (e: Exception) {
            e.printStackTrace()
            errorMessage.postValue(e.message)
            null
        }

        showLoading.value = false
        if (!response.isNullOrEmpty()) {
            Log.i("123", response)
            updateDetail(response)
        }
    }

    private fun updateDetail(response: String) {
        val bean = WeatherBean.objectFromData(response).result
        val result = "${bean.today.city} ${bean.today.date_y} ${bean.today.week} " +
                "\n今日温度:${bean.today.temperature}" +
                "\n今日天气:${bean.today.weather}\n" +
                "\n最新实况${bean.sk.time}实时发布" +
                "\n温度:${bean.sk.temp} 湿度:${bean.sk.humidity}" +
                "\n风向:${bean.sk.wind_direction} 风力:${bean.sk.wind_strength}"
        detail.value = result
    }
}

然后是MainActivity的内容:

    private lateinit var model: WeatherModel
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        。。。
        initModel()
    }
    
    private fun initModel() {
        // 获取ViewModel的实例
        model = ViewModelProviders.of(this).get(WeatherModel::class.java)
        // 订阅ViewModel中的LiveData的变更事件
        model.let { vm ->
            vm.isShowLoading().observe(this, Observer {
                if (it) {
                    if (dlg == null || !dlg!!.isShowing)
                        dlg = indeterminateProgressDialog("Loading...")
                } else { dlg?.dismiss() }
            })
            vm.getErrorMessage().observe(this, Observer { toast(it) })
            vm.getProvinceList().observe(this, Observer { updateProvince(it) })
            vm.getCityList().observe(this, Observer { updateCity(it) })
            vm.getDistrictList().observe(this, Observer { updateDistrict(it) })
            vm.getDetail().observe(this, Observer { tvDetail.text = it })
        }
    }
    
    override fun onStart() {
        super.onStart()
        model.requestCityList() // 通过ViewModel调接口
    }

MainActivity持有ViewModel的实例,并通过实例对ViewModel内的LiveData实例进行订阅,当业务逻辑更改了LiveData内的值后就能很方便的刷新界面了,从而实现了业务和视图的解耦。

最后放一下这个实例的源代码下载:
MVVMDemo.rar: https://t00y.com/file/22686471-412632153

点击链接加入群聊【口袋里的安卓】:https://jq.qq.com/?_wv=1027&k=5z4fzdT
或关注微信公众号:

相关文章

网友评论

      本文标题:闲扯Android的MVVM开发模式

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