MVVM 架构解析及 Jetpack 架构组件的使用

栏目: IOS · Android · 发布时间: 5年前

内容简介:谈到的使用和理解,我将架构图扩展成下面这样:组件需要多用到的。

谈到 MVVM 架构,不得不祭出官方的架构图,架构图能帮助我们更好地理解,如下所示:

MVVM 架构解析及 Jetpack 架构组件的使用
在实践中,根据对架构组件 paging

的使用和理解,我将架构图扩展成下面这样:

MVVM 架构解析及 Jetpack 架构组件的使用
有背景颜色的3处是 paging

组件需要多用到的。

MVVM 和 MVP 的区别

MVPV 层和 P 层互相持有对方的引用,在 V 层调用 P 层逻辑后, P 层回调 V 层的相应方法更新 UI

而在 MVVM 中,上层只依赖直接下层,不能跨层持有引用,那 View 层调用 ViewModel 处理数据后,又如何更新自己呢?

答案就在 ViewModel 中的 LiveData ,这是一种可观察的数据类型,在 View 层中观察者 Observer 对需要的数据进行订阅,当数据发生变化后,观察者 Observer 的回调方法 onChanged() 中会收到新的数据,从而可以更新 UI

LiveData 的相关代码如下:

//package androidx.lifecycle.LiveData;

……
……
……

@MainThread
protected void setValue(T value) {
    assertMainThread("setValue");
    mVersion++;
    mData = value;
    dispatchingValue(null);
}

@SuppressWarnings("WeakerAccess") /* synthetic access */
void dispatchingValue(@Nullable ObserverWrapper initiator) {
    if (mDispatchingValue) {
        mDispatchInvalidated = true;
        return;
    }
    mDispatchingValue = true;
    do {
        mDispatchInvalidated = false;
        if (initiator != null) {
            considerNotify(initiator);
            initiator = null;
        } else {
            for (Iterator<Map.Entry<Observer<? super T>, ObserverWrapper>> iterator =
                    mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
                considerNotify(iterator.next().getValue());
                if (mDispatchInvalidated) {
                    break;
                }
            }
        }
    } while (mDispatchInvalidated);
    mDispatchingValue = false;
}

private void considerNotify(ObserverWrapper observer) {
    if (!observer.mActive) {
        return;
    }
    if (!observer.shouldBeActive()) {
        observer.activeStateChanged(false);
        return;
    }
    if (observer.mLastVersion >= mVersion) {
        return;
    }
    observer.mLastVersion = mVersion;
    observer.mObserver.onChanged((T) mData);
}
复制代码

MVVM 架构解析

整个架构解析如下:

  1. View 层调用 ViewModel 获取数据
  2. ViewModel 调用 Repository 获取数据
  3. Repository 是数据仓库,根据实际业务,再通过 Dao 访问本地数据库或者 Retrofit 访问服务器。
  4. ViewModel 中的 LiveData 类型数据得到更新
  5. View 层的观察者 Observer 的回调方法 onChanged() 中收到新的数据,更新 UI
  6. 如果需要使用 paging 组件,就多了上图中的3处调用

Jetpack 架构组件

JetpackGoogle 为我们提供的架构组件,对于这些组件,我有以下理解和使用心得:

paging

  • 适用于列表页面,可以配置每页加载的数据量和预加载距离
  • 需要使用 PagedListAdapterPagedList
  • 加载下一页的逻辑就在 PagedListAdapter 调用 getItem() 时,这里会调用 PagedListloadAround() 方法
  • 相关参数要求: mEnablePlaceholderstruemPrefetchDistance 大于 0

DataBinding

适用于数据繁杂的页面,可以减少大量 java 代码,在列表页面不必使用。

Navigation

  • 适用于能触发两个明确页面之间跳转的操作
  • 不适用不能确定从哪个页面来或去往哪个页面的操作

ViewModel

  • 管理 ActivityFragment 的数据
  • 创建于 ActivityFragment 内,页面被销毁前, ViewModel 会一直存在
  • 如果因配置变化导致页面销毁, ViewModel 不会销毁,它会被用于新的页面实例
  • 一般在 ViewModel 中配合 LiveData 使用
  • 一般用 ViewModelProviders 获取 ViewModelProvider ,再用它的 get() 方法获取 ViewModel
  • get() 方法中会调用 Factorycreate() 方法创建 ViewModel
  • 创建的 ViewModel 被存入 ViewModelStoreHashMap 中,以便下次直接获取,不用再创建
  • ViewModelStore 是通过 ActivityFragment 获取的
  • ComponentActivity 的构造函数中有这么一段代码
getLifecycle().addObserver(new GenericLifecycleObserver() {
      @Override
      public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
          if (event == Lifecycle.Event.ON_DESTROY) {
              if (!isChangingConfigurations()) {
                  getViewModelStore().clear();
              }
          }
      }
  });
复制代码

可见当不是配置变化导致 Activity 销毁时,会调用 ViewModelStoreclear() 方法:

public final void clear() {
      for (ViewModel vm : mMap.values()) {
          vm.clear();
      }
      mMap.clear();
  }
复制代码

这里会调用 ViewModelclear() 方法,其中又会调用 onCleared() 方法,我们可以在这个方法中取消订阅,以防内存泄漏。

MVVM 案例实战

下面根据我的开源项目 WanAndroid-MVVM 进一步讲解 MVVM 架构的运用,以下所有代码均来自于该项目。

不同的 UI 状态

首先对于数据加载,一般有【加载中、加载成功、加载失败】这3种状态, UI 上需要有对应的变化。

不同于 MVPP 层回调 V 层的相应方法更新 UI 的方式, MVVMView 层只能通过观察数据的方式来更新 UI

所以需要一种数据结构来表示不同的数据加载状态,并在 View 层对其进行观察和响应,定义这种数据结构如下:

package com.sbingo.wanandroid_mvvm.base

/**
 * Author: Sbingo666
 * Date:   2019/4/12
 */

enum class Status {
    LOADING,
    SUCCESS,
    ERROR,
}

data class RequestState<out T>(val status: Status, val data: T?, val message: String? = null) {
    companion object {
        fun <T> loading(data: T? = null) = RequestState(Status.LOADING, data)

        fun <T> success(data: T? = null) = RequestState(Status.SUCCESS, data)

        fun <T> error(msg: String? = null, data: T? = null) = RequestState(Status.ERROR, data, msg)
    }

    fun isLoading(): Boolean = status == Status.LOADING
    fun isSuccess(): Boolean = status == Status.SUCCESS
    fun isError(): Boolean = status == Status.ERROR
}
复制代码

可以看到, RequestState 对应了3种数据加载状态,接着看它的具体使用:

package com.sbingo.wanandroid_mvvm.repository

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.sbingo.wanandroid_mvvm.base.RequestState
import com.sbingo.wanandroid_mvvm.data.http.HttpManager
import com.sbingo.wanandroid_mvvm.model.Chapter
import com.sbingo.wanandroid_mvvm.utils.asyncSubscribe

/**
 * Author: Sbingo666
 * Date:   2019/4/22
 */
class WeChatRepository(private val httpManager: HttpManager) {

    fun getWXChapters(): LiveData<RequestState<List<Chapter>>> {
        val liveData = MutableLiveData<RequestState<List<Chapter>>>()
        //数据加载中
        liveData.value = RequestState.loading()	
        httpManager.wanApi.getWXChapters()
            .asyncSubscribe({
           		 //数据加载成功
                liveData.postValue(RequestState.success(it.data))
            }, {
            	//数据加载失败
                liveData.postValue(RequestState.error(it.message))
            })
        return liveData
    }
}
复制代码

这里将 RequestState 作为 LiveData 的泛型参数,这样 View 层就可以对这个 LiveData 进行观察了。

为了简化代码,统一处理重复逻辑,我将观察代码写入了 base 中:

protected fun <T> handleData(liveData: LiveData<RequestState<T>>, action: (T) -> Unit) =
    liveData.observe(this, Observer { result ->
        if (result.isLoading()) {
            showLoading()
        } else if (result?.data != null && result.isSuccess()) {
            finishLoading()
            action(result.data)
        } else {
            finishLoading()
        }
    })

fun showLoading() {
}

fun finishLoading() {
}
复制代码

根据自己的业务需求,方便地实现 showLoading()finishLoading() 的逻辑,数据处理就在每个页面传入的 action 中。

到这里,完整的数据加载显示流程就走通了!!!

异步加载数据

本项目中使用了 RxJava2 来异步加载数据,调用的代码很简单。

如果对线程切换的原理感兴趣,可以看我之前的一篇文章: 【源码分析】RxJava 1.2.2 实现简单事件流的原理

但每个调用的地方都要异步切换也挺麻烦的,因此我对 Observable 做了一个扩展,如下:

package com.sbingo.wanandroid_mvvm.utils

import com.sbingo.wanandroid_mvvm.data.http.RxHttpObserver
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers

/**
 * Author: Sbingo666
 * Date:   2019/4/23
 */

fun <T> Observable<T>.async(): Observable<T> {
    return this.subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
}

fun <T> Observable<T>.asyncSubscribe(onNext: (T) -> Unit, onError: (Throwable) -> Unit) {
    this.async()
        .subscribe(object : RxHttpObserver<T>() {
            override fun onNext(it: T) {
                super.onNext(it)
                onNext(it)
            }

            override fun onError(e: Throwable) {
                super.onError(e)
                onError(e)
            }
        })
}
复制代码

这两个方法都可以使用,具体就看是否想用 RxHttpObserver 这个自定义的观察者咯。

如果使用 asyncSubscribe() 方法,调用方只需传入数据加载成功和失败的逻辑,非常简单,就像这样:

httpManager.wanApi.getWXChapters()
    .asyncSubscribe({
        liveData.postValue(RequestState.success(it.data))
    }, {
        liveData.postValue(RequestState.error(it.message))
    })
复制代码

统一处理接口数据

刚才说到自定义的观察者 RxHttpObserver ,这又是啥呢?

package com.sbingo.wanandroid_mvvm.data.http

import com.sbingo.wanandroid_mvvm.R
import com.sbingo.wanandroid_mvvm.WanApplication
import com.sbingo.wanandroid_mvvm.utils.ExecutorUtils
import com.sbingo.wanandroid_mvvm.utils.NetUtils
import com.sbingo.wanandroid_mvvm.utils.ToastUtils
import io.reactivex.Observer
import io.reactivex.disposables.Disposable

abstract class RxHttpObserver<T> : Observer<T> {

    override fun onSubscribe(d: Disposable) {
        if (!NetUtils.isConnected(WanApplication.instance)) {
            onError(RuntimeException(WanApplication.instance.getString(R.string.network_error)))
        }
    }

    override fun onError(e: Throwable) {
        e.message?.let {
            ExecutorUtils.main_thread(Runnable { ToastUtils.show(it) })
        }
    }

    override fun onNext(it: T) {
        //业务失败
        val result = it as? HttpResponse<*>
        if (result?.errorCode != 0) {
            onError(
                RuntimeException(
                    if (result?.errorMsg.isNullOrBlank())
                        WanApplication.instance.getString(R.string.business_error)
                    else {
                        result?.errorMsg
                    }
                )
            )
        }
    }

    override fun onComplete() {
    }
}
复制代码

这个自定义的观察者,主要干了三件事:

errorCode

加入 paging 组件

之前提到过,如果加入了 paging 组件,架构流程略微不同。

paging 组件主要用于列表页面,根据列表页面的特性,我对其进行了一些封装,主要封装逻辑如下:

package com.sbingo.wanandroid_mvvm.base.paging

import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel


/**
 * Author: Sbingo666
 * Date:   2019/4/12
 */
open class BasePagingViewModel<T>(repository: BasePagingRepository<T>) : ViewModel() {

    private val pageSize = MutableLiveData<Int>()
    private val repoResult = Transformations.map(pageSize) {
        repository.getData(it)
    }
    val pagedList = Transformations.switchMap(repoResult) { it.pagedList }
    val networkState = Transformations.switchMap(repoResult) { it.networkState }
    val refreshState = Transformations.switchMap(repoResult) { it.refreshState }

    fun refresh() {
        repoResult.value?.refresh?.invoke()
    }

    fun setPageSize(newSize: Int = 10): Boolean {
        if (pageSize.value == newSize)
            return false
        pageSize.value = newSize
        return true
    }

    fun retry() {
        repoResult.value?.retry?.invoke()
    }
}
复制代码

BasePagingViewModel 中的逻辑很好理解, repoResult 根据 pageSize 变化,其他数据又根据 repoResult 变化,最后在 View 层对这些数据进行观察就可以了。

package com.sbingo.wanandroid_mvvm.base.paging

import androidx.lifecycle.Transformations
import androidx.paging.Config
import androidx.paging.toLiveData

/**
 * Author: Sbingo666
 * Date:   2019/4/12
 */
abstract class BasePagingRepository<T> {

    fun getData(pageSize: Int): Listing<T> {

        val sourceFactory = createDataBaseFactory()
        val pagedList = sourceFactory.toLiveData(
            config = Config(
                pageSize = pageSize,
                enablePlaceholders = false,
                initialLoadSizeHint = pageSize * 2
            )
        )
        val refreshState = Transformations.switchMap(sourceFactory.sourceLivaData) { it.refreshStatus }
        val networkStatus = Transformations.switchMap(sourceFactory.sourceLivaData) { it.networkStatus }

        return Listing(
            pagedList,
            networkStatus,
            refreshState,
            refresh = {
                sourceFactory.sourceLivaData.value?.invalidate()
            },
            retry = {
                sourceFactory.sourceLivaData.value?.retryFailed()
            }
        )
    }

    abstract fun createDataBaseFactory(): BaseDataSourceFactory<T>
}
复制代码

BasePagingRepository 中对 PagedList 配置了每页数据量大小,初始加载量等参数,最后包装成数据结构 Listing 返回,这种结构如下:

package com.sbingo.wanandroid_mvvm.base.paging

import androidx.lifecycle.LiveData
import androidx.paging.PagedList
import com.sbingo.wanandroid_mvvm.base.RequestState


/**
 * Author: Sbingo666
 * Date:   2019/4/12
 */
data class Listing<T>(
    //数据
    val pagedList: LiveData<PagedList<T>>,
    //上拉加载更多状态
    val networkState: LiveData<RequestState<String>>,
    //下拉刷新状态
    val refreshState: LiveData<RequestState<String>>,
    //刷新逻辑
    val refresh: () -> Unit,
    //重试逻辑,刷新或加载更多
    val retry: () -> Unit
)

复制代码

而数据源来自 BaseDataSourceFactory :

package com.sbingo.wanandroid_mvvm.base.paging

import androidx.lifecycle.MutableLiveData
import androidx.paging.DataSource

/**
 * Author: Sbingo666
 * Date:   2019/4/12
 */
abstract class BaseDataSourceFactory<T> : DataSource.Factory<Int,T>() {

    val sourceLivaData = MutableLiveData<BaseItemKeyedDataSource<T>>()

    override fun create(): BaseItemKeyedDataSource<T> {
        val dataSource: BaseItemKeyedDataSource<T> = createDataSource()
        sourceLivaData.postValue(dataSource)
        return dataSource
    }

    abstract fun createDataSource(): BaseItemKeyedDataSource<T>

}
复制代码

这里的 sourceLivaDataBaseItemKeyedDataSource 作为值,而 BaseItemKeyedDataSource 才是真正获取数据的地方:

package com.sbingo.wanandroid_mvvm.base.paging

import androidx.lifecycle.MutableLiveData
import androidx.paging.ItemKeyedDataSource
import com.sbingo.wanandroid_mvvm.base.RequestState
import com.sbingo.wanandroid_mvvm.utils.ExecutorUtils


/**
 * Author: Sbingo666
 * Date:   2019/4/12
 */
abstract class BaseItemKeyedDataSource<T> : ItemKeyedDataSource<Int, T>() {
    private var retry: (() -> Any)? = null
    private var retryExecutor = ExecutorUtils.NETWORK_IO

     val networkStatus by lazy {
        MutableLiveData<RequestState<String>>()
    }

    val refreshStatus by lazy {
        MutableLiveData<RequestState<String>>()
    }

    fun retryFailed() {
        val preRetry = retry
        retry = null
        preRetry.let {
            retryExecutor.execute {
                it?.invoke()
            }
        }
    }

	//初始加载(包括刷新)时,系统回调此方法
    override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<T>) {
        refreshStatus.postValue(RequestState.loading())
        onLoadInitial(params, callback)
    }

	//加载更多时,系统回调此方法
    override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<T>) {
        networkStatus.postValue(RequestState.loading())
        onLoadAfter(params, callback)
    }

    override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<T>) {
    }

    fun refreshSuccess() {
        refreshStatus.postValue(RequestState.success())
        retry = null
    }

    fun networkSuccess() {
        retry = null
        networkStatus.postValue(RequestState.success())
    }

    fun networkFailed(msg: String?, params: LoadParams<Int>, callback: LoadCallback<T>) {
        networkStatus.postValue(RequestState.error(msg))
        retry = {
            loadAfter(params, callback)
        }
    }

    fun refreshFailed(msg: String?, params: LoadInitialParams<Int>, callback: LoadInitialCallback<T>) {
        refreshStatus.postValue(RequestState.error(msg))
        retry = {
            loadInitial(params, callback)
        }
    }


    override fun getKey(item: T) = setKey(item)

    abstract fun setKey(item: T): Int

    abstract fun onLoadAfter(params: LoadParams<Int>, callback: LoadCallback<T>)

    abstract fun onLoadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<T>)
}
复制代码

子类只需要复写父类的 onLoadInitial()onLoadAfter() 方法就能执行刷新和加载更多的逻辑了。

这里实现了【重试】的逻辑和【加载中、加载成功、加载失败】这3种状态,这3种状态使用了之前提到的数据结构 RequestState ,不过加载成功后数据并不会在这里的 RequestState 中,这里的 RequestState 只表示加载状态。那数据怎么更新呢?

我们来看一个 BaseItemKeyedDataSource 的子类吧:

package com.sbingo.wanandroid_mvvm.paging.source

import com.sbingo.wanandroid_mvvm.base.paging.BaseItemKeyedDataSource
import com.sbingo.wanandroid_mvvm.data.http.HttpManager
import com.sbingo.wanandroid_mvvm.model.Article
import com.sbingo.wanandroid_mvvm.utils.asyncSubscribe

/**
 * Author: Sbingo666
 * Date:   2019/4/23
 */
class WXDataSource(private val httpManager: HttpManager, private val wxId: Int) : BaseItemKeyedDataSource<Article>() {

    var pageNo = 1

    override fun setKey(item: Article) = item.id

    override fun onLoadAfter(params: LoadParams<Int>, callback: LoadCallback<Article>) {
        httpManager.wanApi.getWXArticles(wxId, pageNo)
            .asyncSubscribe({
                pageNo += 1
                networkSuccess()
                callback.onResult(it.data?.datas!!)
            }, {
                networkFailed(it.message, params, callback)
            })
    }

    override fun onLoadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Article>) {
        httpManager.wanApi.getWXArticles(wxId, pageNo)
            .asyncSubscribe({
                pageNo += 1
                refreshSuccess()
                callback.onResult(it.data?.datas!!)
            }, {
                refreshFailed(it.message, params, callback)
            })
    }
}
复制代码

可以看到和之前 WeChatRepository 中类似,数据也是从服务器上获取的,只不过获取的数据是通过 callback.onResult() 方法返回给 View 层的。

View 层这边,列表的【重试】按钮是封装在 BasePagingAdapter 中的, 根据观察到的 networkState ,动态设置按钮的显示与隐藏,相关代码如下:

private fun hasFooter() =
    if (requestState == null)
        false
    else {
        !requestState?.isSuccess()!!
    }

override fun getItemViewType(position: Int): Int {
    return if (hasFooter() && position == itemCount - 1) {
        TYPE_FOOTER
    } else {
        TYPE_ITEM
    }
}

override fun getItemCount(): Int {
    return super.getItemCount() + if (hasFooter()) 1 else 0
}
    
fun setRequestState(newRequestState: RequestState<Any>) {
    val previousState = this.requestState
    val hadExtraRow = hasFooter()
    this.requestState = newRequestState
    val hasExtraRow = hasFooter()
    if (hadExtraRow != hasExtraRow) {
        if (hadExtraRow) {
            notifyItemRemoved(super.getItemCount())
        } else {
            notifyItemInserted(super.getItemCount())
        }
    } else if (hasExtraRow && previousState != newRequestState) {
        notifyItemChanged(itemCount - 1)
    }
}
复制代码

根据这些封装类,在业务中实现它们的子类,就能轻松使用 paging 组件啦!!!

到这里, MVVM 架构的理论与实践都已打通!


以上所述就是小编给大家介绍的《MVVM 架构解析及 Jetpack 架构组件的使用》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

父与子的编程之旅

父与子的编程之旅

桑德 (Warren Sande)、桑德 (Carter Sande) / 苏金国、易郑超 / 人民邮电出版社 / 2014-10-1 / CNY 69.00

本书是一本家长与孩子共同学习编程的入门书。作者是一对父子,他们以Python语言为例,详尽细致地介绍了Python如何安装、字符串和操作符等程序设计的基本概念,介绍了条件语句、函数、模块等进阶内容,最后讲解了用Python实现游戏编程。书中的语言生动活泼,叙述简单明了。 为了让学习者觉得编程有趣,本书编排了很多卡通人物及场景对话,让学习者在轻松愉快之中跨入计算机编程的大门。 第 2 版新增内......一起来看看 《父与子的编程之旅》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

随机密码生成器
随机密码生成器

多种字符组合密码