Android Presenter与View的解耦探讨

作者: susion哒哒 | 来源:发表于2019-01-21 17:11 被阅读5次

    这篇文章的主要内容前面其实我已经写过一遍了,但是我认为那篇描述的不是很清楚,因此又整理了一下。

    本文主要讨论如何将Android中的Presenter以一种简洁的方式做到与View的解耦,并且不容易脱轨(变的混乱)。本文假设页面数据完全由Presenter管理。

    我们先来看一下常规的PresenterView的写法(下文对于PresenterView的叙述简称为VP),并探讨一下这种写法存在什么问题:

    常规的写法

    对于Android中的VP,我们为了做到互相解耦,我们通常要给Presenter定义一个接口,给View定义一个接口, 假设我们要写一个搜索逻辑,可能会写出如下代码:

    1. 定义接口
         class SearchProtocol{
            interface Presenter{
                fun search()  //搜索
            }    
    
            interface View {
                fun showSearchResult() //显示搜索结果
            }
        }
    
    1. 接口实现
        class SearchPresenter : SearchProtocol.Presenter{ }
    
        class SearchView : SearchProtocol.View{
    
            val presenter:SearchProtocol.Presenter = LoginPresenter()
    
            fun doSearch(){
                presenter.search()
            }
    
            overried showSearchResult(){}
        }
    

    我认为这样写是存在一些问题的:

    问题一 : 接口过多

    PV还没开始写,两个接口先定义下来了。(虽然做到了PV一定意义上的解耦)

    问题二 : View依赖于固定的Presenter接口

    比如大家经常使用的一种构建UI的方式 : 一个RecyclerView构建所有UI,页面不同的部分使用不同的RecyclerViewItem来表现。

    假如下图这个搜索结果页就是使用RecyclerView构建的:

    RecyclerView构建UI.png

    如果用户点击筛选按钮(其实本质还是搜索),那么就需要调用persenter.search()。但是筛选这个item实际上是使用RecyclerView的一个ItemView构建的,因此我可能就需要把presenter(SearchPresenter)的实例传到这个ItemView,ItemView在筛选时调用presenter.search()

    这样做可能有一些不好的地方:

    1. View依赖了一个固定的Presenter接口,VP存在耦合,不利于复用。如果在其他的界面我想复用这个ItemView,那么传另一个界面的Presenter很明显是不合适的。

    2. 不利于View的单元测试。其实RecyclerView中的ItemView也是一个View,如果在实例化这个View的时候还需要传一个指定的Presenter(SearchPresenter),那么单元测试这个View时为了提供它的环境就有点麻烦了,因为还要关心Presenter实例。

    3. 对于数据状态的获取Presenter也需要提供给View一个方法。

    那怎么写可以解决上面的问题呢?我认为下面是一种可行的方案:

    更纯净的VP写法

    对于VP, 我认为他们之间的交流可以分为两种:

    1. View接收用户事件,触发Presenter执行一些逻辑,比如数据加载。
    2. View需要获取当前的数据状态,来决定UI的展现或者UI层的一些逻辑,比如事件打点。

    描述上面两种交流方式,可以把Presenter抽象为下面这个接口:

        open class Action() 
    
        open class State()
    
        abstract class BasePresenter()  { 
    
            abstract fun dispatch(action:Action)
    
            abstract fun <T : State> queryStatus(statusClass: KClass<T>): T?
        }
    

    Action : View触发的操作,可以通过一个Action来通知Presenter

    State : 描述View可以从Presenter中获得的数据的状态。

    BasePresenter : View只依赖这个最抽象的接口。通过ActionState来与Presenter交互。

    下面详细来解释一下ActionState的思想:

    使用Action统一Presenter的处理逻辑

    在往下阅读之前可以先看一下这篇文章 : https://segmentfault.com/a/1190000008736866
    这篇文章介绍了redux的设计思想,而下文所要介绍的Presenter的实现就是借鉴了Redux的设计思想。

    对于常规的写法,Presenter的处理逻辑是通过调用固定的方法实现的,这就导致依赖于一个固定的Presenter接口, 参考Redux的设计,可以这样设计Presenter:

        class Action
    
        class BasePresenter{
            abstract fun dispatch(action: Action)
        }
    

    即所有的Presenter都实现这一个接口,外界对于Presenter逻辑的触发都通过dispatch()方法实现,对于上面搜索那个例子可以这样实现:

        class SearchAction(val keyword:String) : Action
    
        class SearchPresenter(searchView:SearchViewProtocol):BasePresenter{
            overried fun dispatch(action:Action){
                when(action){ //只处理感兴趣的action
                    is SearchAction -> doSearch()
                }
            }
    
            fun doSearch(){
              //...
              searchView.showSearchResult()
            }
        }
    
        class SearchView:SearchViewProtocol{
    
            val presenter:BasePresenter = SearchPresenter(this)
    
             fun doSearch(){
                presenter.dispatch(SearchAction("narato"))
            }
            ......
        }
    

    这样写后对比于常规的写法有什么好处呢?

    1. 减少了Presneter接口的定义,由于现在Presenter对外层的抽象是dispatch方法,因此新的VP不需要特别定义与View配套的Presenter接口。
    2. View不依赖于固定的Presenter接口,统一使用BasePresenter,View可以很好的复用和进行单元测试。
    3. View发出的ActionPresenter可以选择处理,也可以不处理。

    View使用State来获取当前的状态

    在Redux中,View dispatch Action后对于数据的变化,可以通过订阅(观察)数据来刷新UI。不过对于这次我介绍的VPView的数据是由Presenter所提供的,那么就不能使用Redux这种方法了(View不会直接接触数据)。

    举一个例子,比如有一个自定义按钮,它是否可以点击执行一些事情,依赖于当前界面某些数据的状态。这个状态并不属于当前View

    那常规我们可能会这样做:

        //View中的按钮被点击
        class MyBtton(presenter:SearchPresenter){
            fun onClick(){
                if(presenter.canExecute()){
    
                }
            }
        }
    

    如果这样写那就又会出现上面的问题:

    1. 依赖具体的presenter,复用困难
    2. 单元测试麻烦
    3. 为获取状态,又多了一个方法

    我们可以借用dispatch的设计,引入State:

        class SeachState
    
        class SeachBasePresenter{
            fun <T : SeachState> queryState(statteClass: KClass<T>): T?
        }
    

    即我们可以这样实现这个需求:

        class MyBtton(presenter:SeachBasePresenter){
            fun onClick(){
                if(presenter.queryState(MyButtonState::class)?.canExecute == true){
    
                }
            }
        }
    
        class MyButtonState(val canExecute:Boolean = false) : SearchState
    
        class SeachButtonPresenter{
            override fun <T : SearchState> queryStatus(statusClass: KClass<T>): T? {
                return when (statusClass) {
                    MyButtonState::class -> {
                        MyButtonState(true) as T
                    }
                    else -> null
                }
            }
        }
    

    这样的做法不仅解决了上面的问题。并且SearchState是一个对象,我们可以封装许多数据的状态,减少State的定义。

    上面只是我应用在目前业务中的一种PV写法,当然对于不同的业务,可能这套写法会出现问题,欢迎讨论。

    欢迎关注我的Android进阶计划看更多干货

    欢迎关注我的微信公众号:susion随心

    微信公众号.jpeg

    相关文章

      网友评论

        本文标题:Android Presenter与View的解耦探讨

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