Android MVVM架构设计从原理到实战

576 阅读7分钟

MVVM图解说明

1. MVVM介绍

  • Model-View-ViewModel,View指绿色的Activity/Fragment,主要负责界面显示,不负责任何业务逻辑和数据处理。Model指的是Repository 包含的部分,主要负责数据获取,来组本地数据库或者远程服务器。ViewModel指的是图中蓝色部分,主要负责业务逻辑和数据处理,本身不持有View层 引用,通过LiveData向View层发送数据。Repository统一了数据入口,不管来自数据库,还是服务器,统一打包给ViewModel。

1.1 View

  • View层做的就是和UI相关的工作,我们只在XML、Activity和Fragment写View层的代码,View层不做和业务相关的事,也就是我们在Activity不写 业务逻辑和业务数据相关的代码,更新UI通过数据绑定实现,尽量在ViewModel里面做(更新绑定的数据源即可),Activity要做的事就是初始化一些控件 (如控件的颜色,添加RecyclerView的分割线),View层可以提供更新UI的接口(但是我们更倾向所有的UI元素都是通过数据来驱动更改UI),View层可以 处理事件(但是我们更希望UI事件通过Command来绑定)。简单地说:View层不做任何业务逻辑、不涉及操作数据、不处理数据,UI和数据严格的分开。

1.2 ViewModel

  • ViewModel层做的事情刚好和View层相反,ViewModel只做和业务逻辑和业务数据相关的事,不做任何和UI相关的事情,ViewModel层不会持有任何控件 的引用,更不会在ViewModel中通过UI控件的引用去做更新UI的事情。ViewModel就是专注于业务的逻辑处理,做的事情也都只是对数据的操作(这些数据绑定 在相应的控件上会自动去更改UI)。同时DataBinding框架已经支持双向绑定,让我们可以通过双向绑定获取View层反馈给ViewModel层的数据,并对这些 数据上进行操作。关于对UI控件事件的处理,我们也希望能把这些事件处理绑定到控件上,并把这些事件的处理统一化,为此我们通过BindingAdapter对一些 常用的事件做了封装,把一个个事件封装成一个个Command,对于每个事件我们用一个ReplyCommand去处理就行了,ReplyCommand会把你可能需要的数据 带给你,这使得我们在ViewModel层处理事件的时候只需要关心处理数据就行了。再强调一遍: ViewModel不做和UI相关的事。

1.3 Model

  • Model层最大的特点是被赋予了数据获取的职责,与我们平常Model层只定义实体对象的行为截然不同。实例中,数据的获取、存储、数据状态变化都是Model层 的任务。Model包括实体模型(Bean)、Retrofit的Service,获取网络数据接口,本地存储(增删改查)接口,数据变化监听等。Model提供数据获取接口 供ViewModel调用,经数据转换和操作并最终映射绑定到View层某个UI元素的属性上。

2. MVVM的使用

2.1 启用databinding

  • 在主工程app的build.gradle的android {}中加入:
dataBinding {
    enabled true
}

2.2 快速上手,以SquareFragment为例

  • 在square_fragment_square.xml中关联SquareViewModelImpl。
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">
    
    <data>
        <variable
            name="viewModel"
            type="com.phone.module_square.view_model.SquareViewModelImpl" />

        <variable
            name="subDataSquare"
            type="com.phone.library_common.bean.SubDataSquare" />

        <import type="android.view.View" />
    </data>
    
    .....

</layout>

variable - type:类的全路径
variable - name:变量名

2.3 SquareFragment继承BaseMvvmRxFragment,继承基类传入相关泛型,第一个泛型为你创建的SquareViewModelImpl,第二个泛型为ViewDataBind,

保存square_fragment_square.xml后databinding会生成一个SquareFragmentSquareBinding类。(如果没有生成,试着点击Build->Clean Project)

BaseMvvmRxFragment:

abstract class BaseMvvmRxFragment<VM : BaseViewModel, DB : ViewDataBinding> : RxFragment(),
    IBaseView {

    companion object {
        private val TAG = BaseMvvmRxFragment::class.java.simpleName
    }

    //该类绑定的ViewDataBinding
    protected lateinit var mDatabind: DB
    protected lateinit var mViewModel: VM
    protected lateinit var mRxAppCompatActivity: RxAppCompatActivity
    protected lateinit var mBaseApplication: BaseApplication
    // 是否第一次加载
    protected var isFirstLoad = true

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        mDatabind = DataBindingUtil.inflate(inflater, initLayoutId(), container, false)
        mDatabind.lifecycleOwner = viewLifecycleOwner
        return mDatabind.root
    }

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

        mRxAppCompatActivity = activity as RxAppCompatActivity
        mBaseApplication = mRxAppCompatActivity.application as BaseApplication
        mViewModel = initViewModel()
        initData()
        initObservers()
        initViews()
    }

    protected abstract fun initLayoutId(): Int

    protected abstract fun initViewModel(): VM

    protected abstract fun initData()

    protected abstract fun initObservers()

    protected abstract fun initViews()

    protected abstract fun initLoadData()

    override fun onResume() {
        super.onResume()
        initLoadData()
    }

    override fun showLoading() {
        ...
    }

    override fun hideLoading() {
        ...
    }

    override fun onDestroy() {
        mDatabind.unbind()
        viewModelStore.clear()
        super.onDestroy()
    }
}

SquareFragment:

class SquareFragment : BaseMvvmRxFragment<SquareViewModelImpl, SquareFragmentSquareBinding>() {

    companion object {
        private val TAG: String = SquareFragment::class.java.simpleName
    }

    override fun initLayoutId() = R.layout.square_fragment_square

    /**
     * 这里ViewModelProvider的参数要使用this,不要使用rxAppCompatActivity
     */
    override fun initViewModel() = ViewModelProvider(this).get(SquareViewModelImpl::class.java)

    override fun initData() {
        ...
    }

    override fun initObservers() {
        ...
    }

    override fun initViews() {
        ...
    }

    override fun initLoadData() {
        if (isFirstLoad) {
            initSquareData("$currentPage")
            isFirstLoad = false
        }
    }

    fun squareDataSuccess(success: List<SubDataSquare>) {
        ...
    }

    fun squareDataError(error: String) {
        ...
    }

    private fun initSquareData(currentPage: String) {
        ...
    }
}

2.4 SquareViewModelImpl继承BaseViewModel,在ViewModel中发起请求,所有请求都是在mJob上启动,请求会发生在IO线程,最终回调在主线程上,当页面销毁的时候,请求需要在onCleared方法取消,避免内存泄露的风险

BaseViewModel:

open class BaseViewModel : ViewModel() {

    companion object {
        private val TAG: String = BaseViewModel::class.java.simpleName
    }

    /**
     * 在协程或者挂起函数里调用,挂起函数里必须要切换到线程(这里切换到IO线程)
     */
    protected suspend fun <T> executeRequest(block: suspend () -> ApiResponse<T>): ApiResponse<T> =
        withContext(Dispatchers.IO) {
            var response = ApiResponse<T>()
            runCatching {
                block()
            }.onSuccess {
                response = it
            }.onFailure {
                it.printStackTrace()
                val apiException = getApiException(it)
                response.errorCode = apiException.errorCode
                response.errorMsg = apiException.errorMessage
                response.error = apiException
            }.getOrDefault(response)
        }

    /**
     * 捕获异常信息
     */
    private fun getApiException(e: Throwable): ApiException {
        return when (e) {
            is UnknownHostException -> {
                ApiException("网络异常", -100)
            }

            is JSONException -> {//|| e is JsonParseException
                ApiException("数据异常", -100)
            }

            is SocketTimeoutException -> {
                ApiException("连接超时", -100)
            }

            is ConnectException -> {
                ApiException("连接错误", -100)
            }

            is HttpException -> {
                ApiException("http code ${e.code()}", -100)
            }

            is ApiException -> {
                e
            }
            /**
             * 如果协程还在运行,个别机型退出当前界面时,viewModel会通过抛出CancellationException,
             * 强行结束协程,与java中InterruptException类似,所以不必理会,只需将toast隐藏即可
             */
            is CancellationException -> {
                ApiException("取消请求异常", -10)
            }

            else -> {
                ApiException("未知错误", -100)
            }
        }
    }

    override fun onCleared() {
        LogManager.i(TAG, "onCleared")
        super.onCleared()
    }
}

SquareViewModelImpl:

class SquareViewModelImpl : BaseViewModel(), ISquareViewModel {

    companion object {
        private val TAG: String = SquareViewModelImpl::class.java.simpleName
    }

    private var mModel = SquareModelImpl()
    //也可以是直接使用viewModelScope.launch{}启动一个协程
    private var mJob: Job? = null 

    //1.首先定义两个SingleLiveData的实例
    val dataxRxFragment = MutableLiveData<State<List<SubDataSquare>>>()

    override fun squareData(rxFragment: RxFragment, currentPage: String) {
        LogManager.i(TAG, "squareData thread name*****${Thread.currentThread().name}")

        mJob?.cancel()
        mJob =
            GlobalScope.launch(Dispatchers.Main) {
                val apiResponse = executeRequest { mModel.squareData(currentPage) }

                if (apiResponse.data != null && apiResponse.errorCode == 0) {
                    val responseData = apiResponse.data?.datas ?: mutableListOf()
                    if (responseData.size > 0) {
                        dataxRxFragment.value = State.SuccessState(responseData)
                    } else {
                        dataxRxFragment.value =
                            State.ErrorState(ResourcesManager.getString(R.string.library_no_data_available))
                    }
                } else {
                    dataxRxFragment.value = State.ErrorState(apiResponse.errorMsg)
                }
            }
        
//        //或者直接使用
//        //在Android MVVM架构的ViewModel中启动一个新协程(如果你的项目架构是MVVM架构,则推荐在ViewModel中使用),
//        //该协程默认运行在UI线程,协程和ViewModel的生命周期绑定,组件销毁时,协程一并销毁,从而实现安全可靠地协程调用。
//        //调用viewModelScope.launch{} 或 viewModelScope.async{} 方法的时候可以指定运行线程(根据指定的线程来,不指定默认是UI线程)。
//        viewModelScope.launch {
//            val apiResponse = executeRequest { mModel.squareData(currentPage) }
//
//            if (apiResponse.data != null && apiResponse.errorCode == 0) {
//                val responseData = apiResponse.data?.datas ?: mutableListOf()
//                if (responseData.size > 0) {
//                    dataxRxFragment.value = State.SuccessState(responseData)
//                } else {
//                    dataxRxFragment.value =
//                        State.ErrorState(ResourcesManager.getString(R.string.library_no_data_available))
//                }
//            } else {
//                dataxRxFragment.value = State.ErrorState(apiResponse.errorMsg)
//            }
//        }
    }

    override fun onCleared() {
        mJob?.cancel()
        super.onCleared()
    }
}

2.5 SquareModelImpl

SquareViewModelImpl:

class SquareModelImpl() : ISquareModel {

    override suspend fun squareData(currentPage: String): ApiResponse<DataSquare> {
        return RetrofitManager.instance().mRetrofit
            .create(SquareRequest::class.java)
            .getSquareData(currentPage)
    }
}

2.6 封装网络请求(Retrofit+协程)

SquareRequest:

interface SquareRequest {

    @Headers("urlname:${ConstantData.TO_PROJECT_FLAG}")
    @GET(ConstantUrl.SQUARE_URL)
    suspend fun getSquareData(
        @Path("currentPage") currentPage: String
    ): ApiResponse<DataSquare>
}

RetrofitManager:

class RetrofitManager private constructor() {

    private val TAG = RetrofitManager::class.java.simpleName

    @JvmField
    val mRetrofit: Retrofit

    /**
     * 私有构造器 无法外部创建
     * 初始化必要对象和参数
     */
    init {
        //缓存
        val cacheFile = File(BaseApplication.instance().externalCacheDir, "cache")
        val cache = Cache(cacheFile, 1024 * 1024 * 10) //10Mb
        val rewriteCacheControlInterceptor = RewriteCacheControlInterceptor()
        val loggingInterceptor = HttpLoggingInterceptor()
        // 包含header、body数据
        loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
        //        HeaderInterceptor headerInterceptor = new HeaderInterceptor()

        // 初始化okhttp
        val client = OkHttpClient.Builder()
            .connectTimeout((15 * 1000).toLong(), TimeUnit.MILLISECONDS) //连接超时
            .readTimeout((15 * 1000).toLong(), TimeUnit.MILLISECONDS) //读取超时
            .writeTimeout((15 * 1000).toLong(), TimeUnit.MILLISECONDS) //写入超时
            .cache(cache)
            .addInterceptor(CacheControlInterceptor())
            .addInterceptor(AddAccessTokenInterceptor()) //拦截器用于设置header
            .addInterceptor(ReceivedAccessTokenInterceptor()) //拦截器用于接收并持久化cookie
            .addInterceptor(BaseUrlManagerInterceptor())
            .addInterceptor(rewriteCacheControlInterceptor) //                .addNetworkInterceptor(rewriteCacheControlInterceptor)
            //                .addInterceptor(headerInterceptor)
//            .addInterceptor(loggingInterceptor)
            //                .addInterceptor(new GzipRequestInterceptor()) //开启Gzip压缩
            .sslSocketFactory(SSLSocketManager.sslSocketFactory()) //配置
            .hostnameVerifier(SSLSocketManager.hostnameVerifier()) //配置
            //                .proxy(Proxy.NO_PROXY)
            .build()

        // 初始化Retrofit
        mRetrofit = Retrofit.Builder()
            .client(client)
            .baseUrl(ConstantUrl.BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create()).build()
    }

    /**
     * 保证只有一个实例
     *
     * @return
     */
    companion object {
        @Volatile
        private var instance: RetrofitManager? = null
            get() {
                if (field == null) {
                    field = RetrofitManager()
                }
                return field
            }

        //Synchronized添加后就是线程安全的的懒汉模式
        @Synchronized
        @JvmStatic
        fun instance(): RetrofitManager {
            return instance!!
        }

        /**
         * 查询网络的Cache-Control设置,头部Cache-Control设为max-age=0
         * (假如请求了服务器并在a时刻返回响应结果,则在max-age规定的秒数内,浏览器将不会发送对应的请求到服务器,数据由缓存直接返回)时则不会使用缓存而请求服务器
         */
        private const val CACHE_CONTROL_AGE = "max-age=0"

        /**
         * 设缓存有效期为两天
         */
        const val CACHE_STALE_SEC = (60 * 60 * 24 * 2).toLong()

        /**
         * 查询缓存的Cache-Control设置,为if-only-cache时只查询缓存而不会请求服务器,max-stale可以配合设置缓存失效时间
         * max-stale 指示客户机可以接收超出超时期间的响应消息。如果指定max-stale消息的值,那么客户机可接收超出超时期指定值之内的响应消息。
         */
        private const val CACHE_CONTROL_CACHE = "only-if-cached, max-stale=$CACHE_STALE_SEC"

        fun buildSign(secret: String, time: Long): String {
            //        Map treeMap = new TreeMap(params)// treeMap默认会以key值升序排序
            val stringBuilder = StringBuilder()
            stringBuilder.append(secret)
            stringBuilder.append(time)
            stringBuilder.append("1.1.0")
            stringBuilder.append("861875048330495")
            stringBuilder.append("android")
            Log.d("GlobalConfiguration", "sting:$stringBuilder")
            val md5: MessageDigest
            var bytes: ByteArray? = null
            try {
                md5 = MessageDigest.getInstance("MD5")
                bytes = md5.digest(stringBuilder.toString().toByteArray(charset("utf-8"))) // md5加密
            } catch (e: NoSuchAlgorithmException) {
                e.printStackTrace()
            } catch (e: UnsupportedEncodingException) {
                e.printStackTrace()
            }
            // 将MD5输出的二进制结果转换为小写的十六进制
            val sign = StringBuilder()
            bytes?.let {
                for (i in 0 until it.size) {
                    val hex = Integer.toHexString((it[i] and 0xFF.toByte()).toInt())
                    if (hex.length == 1) {
                        sign.append("0")
                    }
                    sign.append(hex)
                }
            }
            Log.d("GlobalConfiguration", "MD5:$sign")
            return sign.toString()
        }

        fun getCacheControl(): String {
            return if (isNetworkAvailable()) CACHE_CONTROL_AGE else CACHE_CONTROL_CACHE
        }

        /**
         * 判断网络是否可用
         *
         * @return
         */
        fun isNetworkAvailable(): Boolean {
            val connectivityManager =
                BaseApplication.instance().applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
            //如果仅仅是用来判断网络连接
            //connectivityManager.getActiveNetworkInfo().isAvailable()
            val info = connectivityManager.allNetworkInfo
            //            LogManager.i(TAG, "isNetworkAvailable*****" + info.toString())
            for (i in info.indices) {
                if (info[i].state == NetworkInfo.State.CONNECTED) {
                    return true
                }
            }
            return false
        }
    }
}

2.6 数据绑定,拥有databinding框架自带的双向绑定,也有扩展

2.6.1 传统绑定
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center_horizontal"
    android:includeFontPadding="false"
    android:text="@={subDataSquare.title}"
    android:textColor="@color/library_color_FFE066FF"
    android:textSize="@dimen/base_sp_18"
    android:visibility="@{subDataSquare.collect? View.GONE:View.VISIBLE}" />
2.6.2 自定义ImageView图片加载,url是图片路径,这样绑定后,这个ImageView就会去显示这张图片,不限网络图片还是本地图片。
<ImageView
    android:layout_width="@dimen/base_dp_95"
    android:layout_height="@dimen/base_dp_95"
    android:layout_marginStart="@dimen/base_dp_16"
    android:layout_gravity="center_horizontal"
    android:scaleType="centerCrop"
    app:imageUrl="@{dataBean.picUrl}"
    tools:ignore="ContentDescription" />
  • BindingAdapter中的实现
object CommonBindingAdapter {

    @JvmStatic
    val TAG = CommonBindingAdapter::class.java.simpleName

    /**
     * 加载图片
     */
    @JvmStatic
    @BindingAdapter("app:imageUrl")
    fun bindImage(imageView: ImageView, url: String?) {
        ImageLoaderManager.display(imageView.context, imageView, url)
    }
}
2.6.3 RecyclerView绑定,在ProjectAdapter中绑定
<TextView
    android:id="@+id/tev_name_data"
    style="@style/library_text_3"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@={dataBean.date}"
    app:layout_constraintBottom_toBottomOf="@+id/imv_title"
    app:layout_constraintLeft_toLeftOf="@+id/tev_title"
    tools:text="2020-03-14 | zskingking" />
<ImageView
    android:id="@+id/imv_collect"
    android:layout_width="@dimen/base_dp_30"
    android:layout_height="@dimen/base_dp_30"
    android:layout_marginEnd="@dimen/base_dp_16"
    android:paddingStart="@dimen/base_dp_10"
    android:paddingTop="@dimen/base_dp_10"
    android:scaleType="centerCrop"
    app:articleCollect="@{dataBean.collect}"
    app:layout_constraintBottom_toBottomOf="@id/imv_title"
    app:layout_constraintRight_toRightOf="parent"
    tools:ignore="ContentDescription" />
class ProjectAdapter(val context: Context, val list: MutableList<ArticleListBean>) :
    RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    companion object {
        private val TAG = ProjectAdapter::class.java.simpleName
    }

    fun clearData() {
        notifyItemRangeRemoved(0, this.list.size)
        notifyItemRangeChanged(0, this.list.size)
        this.list.clear()
    }

    fun addData(list: MutableList<ArticleListBean>) {
        notifyItemRangeInserted(this.list.size, list.size)
        this.list.addAll(list)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val binding: CustomViewItemProjectBinding = DataBindingUtil.inflate(
            LayoutInflater.from(context),
            R.layout.custom_view_item_project,
            parent,
            false
        )
        return ArticlePicViewHolder(binding.root)
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        holder.itemView.findViewById<View>(R.id.root)?.clickNoRepeat {
            onItemViewClickListener?.onItemClickListener(position, it)
        }
        //收藏
        holder.itemView.findViewById<View>(R.id.ivCollect)?.clickNoRepeat {
            onItemViewClickListener?.onItemClickListener(position, it)
        }

        val binding = DataBindingUtil.getBinding<CustomViewItemProjectBinding>(holder.itemView)?.apply {
            dataBean = list[position]
        }
        binding?.executePendingBindings()
    }

    override fun getItemCount(): Int {
        return list.size
    }

    override fun getItemId(position: Int): Long {
        return position.toLong()
    }

    /**
     * 防止重复点击
     * @param interval 重复间隔
     * @param onClick  事件响应
     */
    var lastTime = 0L
    fun View.clickNoRepeat(interval: Long = 400, onClick: (View) -> Unit) {
        setOnClickListener {
            val currentTime = System.currentTimeMillis()
            if (lastTime != 0L && (currentTime - lastTime < interval)) {
                return@setOnClickListener
            }
            lastTime = currentTime
            onClick(it)
        }
    }

    /**
     * 带图片viewHolder
     */
    class ArticlePicViewHolder constructor(itemView: View) :
        RecyclerView.ViewHolder(itemView) {

    }

    private var onItemViewClickListener: OnItemViewClickListener? = null

    fun setOnItemViewClickListener(onItemViewClickListener: OnItemViewClickListener) {
        this.onItemViewClickListener = onItemViewClickListener
    }
}
如对此有疑问,请联系qq1164688204。
  • 推荐组件化架构Android开源项目

  • 项目功能介绍:原本是RxJava2和Retrofit2项目,现已更新使用Kotlin+RxJava2+Retrofit2+MVP架构+组件化

和 Kotlin+Retrofit2+协程+MVVM架构+组件化,添加自动管理token功能,添加RxJava2生命周期管理,集成极光推送、阿里云Oss对象存储和高德地图定位功能。