LifeClean : 基于Lifecycle 的MVP编码框架

2,491 阅读5分钟

LifeClean是一个变种的MVP框架, 适用于常见的UI业务, 也对Android常用的组件定义了一些使用规范, 主要具有以下特点:

  1. 规范MVP写法
  2. Presenter/View提供Lifecycle
  3. 及时释放RxJava Disposable,避免内容泄漏
  4. 规范RecyclerView.Adapter的使用方式
  5. 规范全局UI状态的刷新

规范MVP写法

个人认为MVP主要是用来做职责分离的, 即Presenter负责数据的加载逻辑, View负责数据的展示逻辑。

传统MVP的写法是将PresenterView都抽取出一个接口,然后实现类之间使用这两个接口做隔离。

LifeClean中不会对每一个Presenter都抽取一个接口, LifeClean规定:

  1. 所有的Presenter都应该遵守同一个约定(接口)
  2. 所有的View都应该使用Presenter接口来与Presenter交互

抽象的Presenter接口:

// view 向 presenter 发出的事件(信号)
interface Action

//页面需要的状态
interface State

interface Presenter {

    fun dispatch(action: Action)

    fun <T : State> getState(): T? {
        return null
    }

}

它定义了Presenter的能力:

  • dispatch(Action) : Presenter可以接收View发出的信号(Action)

  • getState():T : Presenter可以返回给View一些状态(State)

这些Action/StateLifeClean中都属于View, View应该在其所遵循的约定(接口)中定义这些Action/State, 比如:

基于RecyclerView来实现的页面的约定:

interface SimpleRvPageProtocol {

    //加载数据
    class LoadData(val searchWord: String, val isLoadMore: Boolean) : Action

    //查询数据状态
    class PageState(val currentPageSize: Int) : State

    //刷新页面数据
    fun refreshDatas(datas: List<Any>, isLoadMore: Boolean = false, extra: Any = Any())

    //刷新页面状态
    fun refreshPageStatus(status: String, extra: Any = Any())

}

结合Presenter的一个具体使用示例:

//View
class GitRepoMvpPage(activity: AppCompatActivity) : SimpleRvPageProtocol, FrameLayout(activity) {

    //类型为最抽象的Presenter
    private val presenter: Presenter = GithubPresenter(this)

    init {
         // 通知Presenter做数据的加载
         presenter.dispatch(SimpleRvPageProtocol.LoadData("Android", false))
    }

    override fun refreshDatas(datas: List<Any>, isLoadMore: Boolean, extra: Any) {
        //查询数据状态
        val currentPageSize =presenter.getState<SimpleRvPageProtocol.PageState>()?.currentPageSize ?: 0
        Toast.makeText(context, "当前页 : $currentPageSize", Toast.LENGTH_SHORT).show()
    }
    ...
}

//Presenter
class GithubPresenter(val view: SimpleRvPageProtocol) : Presenter {

    private var page = 0

    override fun dispatch(action: Action) {
        when (action) {
            is SimpleRvPageProtocol.LoadData -> {
                ...
                view.refreshDatas(list)
            }
        }
    }

    override fun <T : State> getState(): T? {
        return SimpleRvPageProtocol.PageState(page) as? T
    }
}

LifeClean中将View定义为业务的中心,将Presenter的能力(Action)都定义到了View(约定)中,Presenter可以自己选择性的处理这个Action, 即View完全解耦于Presenter

Presenter/View提供LifeCycle

为Presenter提供Activity的生命周期

一般会在Presenter中做资源的加载工作,比如使用RxJava进行网络请求,那么如何及时的释放Disposable来避免内存泄漏呢?

LifeCleanPresenter可以通过继承LifePresenter来观察Activity的生命周期:

class GithubPresenter(val view: SimpleRvPageProtocol) : LifePresenter() {

    override fun onActivityCreate() {
        Log.d(TAG, "onActivityCreate")
    }

}

即继承LifePresenter, 然后复写Activity相关生命周期方法, 那为什么LifePresenter拥有Activity的生命周期呢? 内部实现如下:

abstract class LifePresenter : Presenter, LifecycleObserver {

    private var lifeOwnerReference = WeakReference<AppCompatActivity>(null)

    fun injectLifeOwner(lifecycleOwner: AppCompatActivity) {
        lifeOwnerReference = WeakReference(lifecycleOwner)
        lifecycleOwner.lifecycle.addObserver(this)
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
    open fun onActivityCreate() {

    }
}

LifePresenter就是一个LifecycleObserver,它是LifeCycle的一个观察者, 那injectLifeOwner()这个方法在哪里调用的呢?

其实在LifeClean中如果你想让Presenter感知Activity的生命周期,那么必须继承LifePresenter, 并且使用LifeClean提供的模板方法来创建这个Presenter:

class GitRepoMvpPage(context: AppCompatActivity) : SimpleRvPageProtocol, FrameLayout(context) {

    val presenter: Presenter = LifeClean.createPresenter<GithubPresenter, SimpleRvPageProtocol>(context, this)

}

LifeClean.createPresenter()会通过反射来构造GithubPresenter并调用injectLifeOwner(),使GithubPresenter可以感知Activity的生命周期。

为View提供Activity的生命周期

这里的View特指使用ViewGroup实现的页面,不过由于多继承的问题,在LifeCleanView感知Activity的生命周期的用法与Presenter并不相同。

首先你的ViewGroup需要实现LifePage接口:

interface LifePage : LifecycleObserver

然后使用LifeClean的模板方法创建这个ViewGroup:

val lifePage = LifeClean.createPage<GitHubLifePage>(activity)

然后就可以感知Activity的生命周期了:

class GitHubLifePage(context: AppCompatActivity) : FrameLayout(context),LifePage {

   @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    fun onResume() {
        Toast.makeText(context, "接收到Activity的生命周期事件 onResume", Toast.LENGTH_SHORT).show()
    }

}

LifeClean会在View Dettach时自动解除对Activity生命周期的观察。

及时释放RxJava Disposable

LifeClean提供了自动释放Disposable的方法:

fun Disposable.disposeOnStop(lifeOwner: LifecycleOwner?): Disposable?

比如在LifePresenter中释放Disposable:

    apiService.searchRepos(query + IN_QUALIFIER, requestPage, PAGE_SIZE)
    .subscribe({...})
    .disposeOnDestroy(getLifeOwner())

disposeOnDestroy(getLifeOwner())会自动在LifeOwner Destroy时释放掉Disposable

规范RecyclerView.Adapter的使用方式 : 对象到View的映射

LifeCleanRecyclerView.Adapter应实现AdapterDataToViewMapping接口, 它定义了对象与View的映射关系:

interface AdapterDataToViewMapping<T> {
    //对象 ——> Type
    fun getItemType(data: T): Int

    // Type -> View
    fun createItem(type: Int): AdapterItemView<*>?
}

RecyclerViewItemView应实现AdapterItemView接口,这样ItemView只需要拿到数据做UI渲染即可:

interface AdapterItemView<T> {
    fun bindData(data: T, position: Int)
}

CommonRvAdapter

CommonRvAdapterAdapterDataToViewMapping的抽象实现类。它要求所有的ItemView都应该是View的子类:

//data数据集合应该交给CommonRvAdapter维护
abstract class CommonRvAdapter<T>(val data: MutableList<T> = ArrayList()) :
    RecyclerView.Adapter<RecyclerView.ViewHolder>(),
    AdapterDataToViewMapping<T> {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        val item = createItem(viewType)
            ?: throw RuntimeException("AdapterDataToViewMapping.createItem cannot return null")
        return CommonViewHolder(item)
    }

    //item必须继承自View
   protected class CommonViewHolder<T> internal constructor(var item: AdapterItemView<T>) :
        RecyclerView.ViewHolder(if (item is View) item else throw RuntimeException("item view must is view"))

}

CommonRvAdapter强调的是: 把对象映射为View。比如:

class SimpleDescView(context: Context) : AppCompatTextView(context), AdapterItemView<SimpleDescInfo> {

    init {
        layoutParams = ViewGroup.LayoutParams(
            ViewGroup.LayoutParams.MATCH_PARENT,
            ViewGroup.LayoutParams.WRAP_CONTENT
        )
        setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14f)
        setPadding(30, 30, 30, 0)
        setTextColor(Color.DKGRAY)
    }

    override fun bindData(data: SimpleDescInfo, position: Int) {
        text = data.desc
    }

}

SimpleRvAdapter 与 MergeAdapter

他俩都继承自CommonRvAdapter, SimpleRvAdapter提供快速映射对象到View的能力:

val adapter = SimpleRvAdapter<Any>(context).apply {
    registerMapping(String::class.java, SimpleStringView::class.java)
    registerMapping(Repo::class.java, GitRepoView::class.java)
}

即通过反射来动态构造对象对应的View,不过这里View必须要有constructor(context)构造函数

MergeAdapter可以合并多个遵循AdapterDataToViewMapping接口的RecyclerView.Adapter,它可以大大提高RecyclerView.Adapter的复用性:

  private val titleAdapter by lazy {
        SimpleRvAdapter<Any>(this).apply {
            registerMapping(SimpleTitleInfo::class.java, SimpleTitleView::class.java)
        }
    }

    private val descAdapter by lazy {
        SimpleRvAdapter<Any>(this).apply {
            registerMapping(SimpleDescInfo::class.java, SimpleDescView::class.java)
        }
    }

    private val mergeAdapter by lazy {
        MergeAdapter(
            adapterTitle,
            adapterDesc
        )
    }

上面mergeAdapter组合了titleAdapterdescAdapter的映射能力。

规范全局UI状态的刷新

大多数App的页面状态都是相同的, LifeClean定义常见的页面状态, 可以用来规范整个App的页面状态刷新逻辑:

object PageStatus {

    //一些常用的页面状态
    val START_LOAD_MORE = "start_load_more"
    val END_LOAD_MORE = "end_load_more"
    val START_LOAD_PAGE_DATA = "start_load_page_data"
    val END_LOAD_PAGE_DATA = "end_load_page_data"
    val NO_MORE_DATA = "no_more_data"
    val EMPTY_DATA = "empty_data"
    val NET_ERROR = "net_error"
    val TOAST = "show_toast"
    val PRIVACY_DATA = "privacy_data"
    val CONTENT_DELETE = "content_delete"
    val ERROR = "error"
    val UNDEFINE = "undefine"

    ...
}

End

本文介绍了LifeClean的核心思想以及它的主要特性, 遵循LifeClean的思想可以帮助你写出清晰、复用性高的业务代码。

引入方法:

implementation 'com.susion:life-clean:1.0.7'

LifeClean仓库地址 : github.com/SusionSuc/L…