这篇文章的主要内容前面其实我已经写过一遍了,但是我认为那篇描述的不是很清楚,因此又整理了一下。
本文主要讨论如何将Android中的
Presenter
以一种简洁的方式做到与View
的解耦,并且不容易脱轨(变的混乱)。本文假设页面数据
完全由Presenter
管理。
我们先来看一下常规的Presenter
与View
的写法(下文对于Presenter
与View
的叙述简称为VP
),并探讨一下这种写法存在什么问题:
常规的写法
对于Android中的VP
,我们为了做到互相解耦,我们通常要给Presenter
定义一个接口,给View
定义一个接口, 假设我们要写一个搜索逻辑,可能会写出如下代码:
定义接口
class SearchProtocol{ interface Presenter{ fun search() //搜索 } interface View { fun showSearchResult() //显示搜索结果 } }
接口实现
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,页面不同的部分使用不同的RecyclerView
的Item
来表现。
假如下图这个搜索结果页就是使用RecyclerView
构建的:
RecyclerView构建UI.png
如果用户点击筛选按钮(其实本质还是搜索),那么就需要调用persenter.search()
。但是筛选这个item实际上是使用RecyclerView
的一个ItemView
构建的,因此我可能就需要把presenter(SearchPresenter)
的实例传到这个ItemView,ItemView在筛选时调用presenter.search()
这样做可能有一些不好的地方:
View
依赖了一个固定的Presenter
接口,VP
存在耦合,不利于复用。如果在其他的界面我想复用这个ItemView,那么传另一个界面的Presenter
很明显是不合适的。不利于
View
的单元测试。其实RecyclerView
中的ItemView
也是一个View
,如果在实例化这个View
的时候还需要传一个指定的Presenter(SearchPresenter)
,那么单元测试这个View
时为了提供它的环境就有点麻烦了,因为还要关心Presenter
实例。对于数据状态的获取
Presenter
也需要提供给View
一个方法。
那怎么写可以解决上面的问题呢?我认为下面是一种可行的方案:
更纯净的VP写法
对于VP
, 我认为他们之间的交流可以分为两种:
View
接收用户事件,触发Presenter
执行一些逻辑,比如数据加载。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
只依赖这个最抽象的接口。通过Action
和State
来与Presenter
交互。
下面详细来解释一下Action
和State
的思想:
使用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")) } ...... }
这样写后对比于常规的写法有什么好处呢?
减少了
Presneter
接口的定义,由于现在Presenter
对外层的抽象是dispatch
方法,因此新的VP不需要特别定义与View
配套的Presenter
接口。View
不依赖于固定的Presenter
接口,统一使用BasePresenter
,View可以很好的复用和进行单元测试。View
发出的Action
,Presenter
可以选择处理,也可以不处理。
View使用State
来获取当前的状态
在Redux中,View dispatch Action
后对于数据的变化,可以通过订阅(观察)数据来刷新UI。不过对于这次我介绍的VP
,View
的数据是由Presenter
所提供的,那么就不能使用Redux这种方法了(View不会直接接触数据)。
举一个例子,比如有一个自定义按钮,它是否可以点击执行一些事情,依赖于当前界面某些数据的状态。这个状态并不属于当前View
那常规我们可能会这样做:
//View中的按钮被点击 class MyBtton(presenter:SearchPresenter){ fun onClick(){ if(presenter.canExecute()){ } } }
如果这样写那就又会出现上面的问题:
依赖具体的presenter,复用困难
单元测试麻烦
为获取状态,又多了一个方法
我们可以借用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 } } }
作者:susion哒哒
链接:https://www.jianshu.com/p/806ccf96892c