MVP这种架构在Android界已经基本成为标配,MVP本身也有很多写法和变种,当然,没有最好的架构,只有最合适的架构,具体架构要怎么写,还是要看实际项目的需要。
我们在这里简单梳理一下MVP的一些演化版本,希望为具体的项目实现提供一点参考。
MVP本身的概念,就是把Model、View和Presenter相互解耦,大概可以这样理解:
MVP的基本概念
各自分工如下:
- View负责外部界面交互,不直接处理业务逻辑
- Presenter负责内部业务逻辑,不直接处理业务数据
- Model负责核心业务数据,与数据库和网络进行数据交互
原始MVP
如果仅从分工的角度实现MVP,只需要发生两次引用:
- 向View里引用Presenter,(因为View都是Fragment或Activity,有特定的构造函数,所以一般采用set方式引用),以处理具体的内部业务逻辑,代码形如:
public class TasksFragment extends Fragment{
...
private Presenter mPresenter;
public void setPresenter(Presenter presenter) {
mPresenter = presenter;
}
...
}
- 向Presenter里引用Model和View,其中View需要通过接口封装一下再引用(一般在构造函数中引用),引用Model为业务逻辑提供核心的业务数据,引用View操作与界面相关的业务逻辑,代码形如:
public class Presenter{
...
private Repository mRepository;//model的实现这里不再展开
private TasksFragment mView;
public Presenter(Repository tasksRepository, TasksFragment tasksView) {//presenter里引用model
mRepository = tasksRepository;
mTView = tasksView;
mTasksView.setPresenter(this);//view里引用presenter
}
...
}
这就是一个最原始的MVP的实现,这个版本有一个严重的问题,就是可维护性太差!
这版MVP虽然实现了各司其职,但其实质只不过是把代码拆到了不同的文件里,在实现中,M、V和P都引用了实体类的实例,形成了非常紧密的耦合,它其实只是实现了这样的效果:
实际效果
很显然,难以复用,难以扩展,未来的维护简直是个灾难。
为了解耦合,很自然地,要使用接口去解耦合。
演化1-Google Architecture
Google在github上开源的architecture是个教科书般的MVP框架,它是这样做的:
- V和P的接口化和注入
View和Presenter不再引用实体类,而是引用抽象接口,View里引用的Presenter的接口,Presenter里引用的也是View的接口,这样的View和Presenter的代码形如:
public class TasksFragment extends Fragment implements IView{//实现接口以便注入到Presenter
...
private IPresenter mPresenter;
public void setPresenter(IPresenter presenter) {//view已有特定的构造函数,以set方式注入为宜
mPresenter = presenter;
}
...
}
和
public class Presenter implements IPresenter{//实现接口以便注入到view
...
private Repository mRepository;//model的实现这里不再展开
private IView mView;
public Presenter(Repository tasksRepository, IView tasksView) {//presenter里注入model和view
mRepository = tasksRepository;
mTView = tasksView;
mTasksView.setPresenter(this);//view里注入presenter
}
...
}
如果愿意的话,model也可以采用接口注入的形式(google architecture并没有做model的接口注入,是为了确保引用的实例是一个全局唯一的数据层单例,这样容易避免污染数据),这样就能实现一个完好解耦的MVP:
可维护性良好MVP
- 集中管理V和P的接口
其实就是把V和P的接口放在同一个接口文件下了,代码形如:
public interface ITasksContract {
interface IView{...}
interface IPresenter{...}
}
这样做有两个好处:
1.从一组业务来讲,业务逻辑和界面逻辑在同一个文件中定义,极具连贯性,极大地方便了阅读、理解和维护(这也会引导你先从接口开始写代码)
2.从多组业务来讲(App一般有多组业务),便于管理好多个V和P的接口,这些接口天然按照业务分别写在不同文件里,扩展和引用更加清晰,不易出错
google architecture还做了一项改进,为V和P的接口定义了更基础的接口,在基础接口中统一定义了View注入Presenter的行为和Presenter开启业务工作的行为,代码形如:
public interface BaseView<T> {//用泛型定义Presenter
void setPresenter(T presenter);//用set注入Presenter
}
和
public interface BasePresenter {
void start();//开启业务工作
}
你自己实现的V和P的接口,只要继承基础接口,就能保证MVP基础行为的一致性,这样你的V和P就可以更加专注于业务
(除了教科书般的MVP,google architecture还提供了教科书般的数据Model层实现,不过这里就不做展开了)
演化2-泄露的问题
上面的这种做法,有一个潜在的问题,就是内存泄露
我们知道,Presenter为了实现业务逻辑,一手持有数据Model,一手持有View,这里面有一个隐含的bug:
数据Model在处理数据时,无论是处理本地数据还是网络数据,都是耗时操作,是不能在主线程运行的;而View,是必须在主线程运行的。这就容易产生一个问题,当View关闭退出时,Presenter可能还在异步线程里工作,而且Presenter还持有着View的实例——标准的内存泄露场景
要避免持有型的内存泄露,一个很有效的办法就是把强引用的持有变成弱引用,就是说,在Presenter里,要用WeakReference的方式去持有View,实现代码形如:
protected WeakReference<T> mViewRef; // view 的弱引用
public void attachView(T view){//持有View
mViewRef = new WeakReference<T>(view);
}
public void detachView(){
if (mViewRef != null){
mViewRef.clear();
mViewRef = null;
}
}
public T getView() {//获取view的实例
return mViewRef.get();
}
这段代码其实是通用代码,根据聚焦业务的原则,应该抽象为基础行为,而接口是不能实现任何方法的,所以,这段代码只能通过抽象类实现通用化,整个类的代码形如:
public abstract class MVPBasePresenter<T> {
protected WeakReference<T> mViewRef; // view 的弱引用
public void attachView(T view){//持有View
mViewRef = new WeakReference<T>(view);
}
public void detachView(){
if (mViewRef != null){
mViewRef.clear();
mViewRef = null;
}
}
public T getView() {//获取view的实例
return mViewRef.get();
}
}
其中,attachView和detachView要在View的相应的生命周期中调用,这样的话,我们又需要为View实现相关的抽象类,Fragment和Activity都需要
//需要两个泛型类型,一个用来继承Presenter的抽象类,而这个Presenter抽象类又需要一个View的泛型
public abstract class MVPBaseFragment<V,T extends MVPBasePresenter<V>> extends Fragment {
protected T mPresenter;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mPresenter = createPresenter();
mPresenter.attachView((V)this);
}
@Override
public void onDestroy() {
super.onDestroy();
if(mPresenter!=null)
mPresenter.detachView();
}
protected abstract T createPresenter();
}
这样一个业务Fragment在实例化时,代码形如:
//ICategoryContract.View是IView接口
//我们还定义了ICategoryContract.Presenter作为IPresenter接口
//CategoryPresenter继承了MVPBasePresenter抽象了和IPresenter接口
public class MainFragment
extends MVPBaseFragment<ICategoryContract.View,CategoryPresenter>
implements ICategoryContract.View {...}
我们实际上把Presenter抽象类和IPresenter业务接口做了分离,把View抽象类和IView业务接口做了分离,基础行为和业务逻辑互不干扰。
Activity的代码内容类似,这里不再重复。
到了实际项目中,V和P分别继承对应的抽象类,因为抽象类里已经实现了弱引用和相关的管理,所以我们可以专注于业务逻辑的实现。
不过,这样做带来两个问题:
- 如果在View的构造函数中自动处理Presenter的实例化,实际上会束缚了我们自己的写作方式,比如我们的Presenter需要注入Model,就不能用构造方式注入;更严重的是,如果我们在Presenter初始化时需要设置某些UI控件,因为抽象类的oncreate需要先于业务类的oncreate去执行(业务类里需要先执行super.oncreate),会遇到UI控件不能及时初始化的问题。
- Android的View其实是在不断扩张的,以Activity为例,常见的就包括Activity、AppCompatActivity、FragmentActivity、RxAppCompatActivity等,如果使用这种抽象类的模式,每遇到一种Activity,就得去做一个对应的抽象类,可扩展性很差。
参照Google的做法,我们应该再多做一点接口的文章
演化3-View的剥离
我们回头再看一遍Presenter抽象类
//Presenter抽象类
public abstract class MVPBasePresenter<T>{...}
其实在Presenter抽象类里,用来处理View的泛型是与业务无关的,我们此前是做了一个View的抽象类来配合Presenter做弱引用处理,其实细想起来,这个View的角色没必要使用抽象类,我们用一个IView基础接口就可以满足需要:
//基础接口,不需要定义任何方法
public interface IMVPBaseView {}
我们的业务接口里,IView业务接口继承这个基础接口:
public interface CategoryContract {
...
interface View implements IMVPBaseView{
...
}
}
我们的Fragement可以恢复Google教科书那样的简洁:
public class TasksFragment extends Fragment implements IView{//实现的接口中包含基础行为和业务逻辑
...
}
最终,我们的MVP结构是这样的:
Model:接口注入(更灵活)或引用一个全局单例(更干净)
View:IMVPBaseView(基础行为)-> IContract.View(业务逻辑)-> XXFragment(V的具体实现)
Presenter:(MVPBasePresenter(基础行为) + IContract.Presenter)-> XXPresenter(P的具体实现)
当然,在这种方式下,Presenter的创建、初始化、销毁等行为,也还给了最终的业务Fragment(或Activity)。
演化4-Dagger
MVP里面其实有大量的依赖关系和注入行为,代码会显得比较复杂,而Dagger是一个专门处理依赖注入的框架,可以用配置的方式实现复杂的依赖关系,所以我们完全可以用Dagger来实现MVP
在Dagger(Dagger2)里,核心要素就是Module、Inject和Component,它们分别起这样的作用:
- Module:提供依赖,其实就是把我们此前用set或构造参数注入的依赖实例,改用module配置出来,由Dagger负责传给要注入的类,比如把IView和数据Model注入到Presenter里,代码形如:
//为presenter提供IView参数实例
@Module
public class TasksPresenterModule {
private final TasksContract.View mView;
public TasksPresenterModule(TasksContract.View view) {
mView = view;
}
@Provides//提供参数的函数方法
TasksContract.View provideTasksContractView() {
return mView;
}
}
和
//为presenter提供数据Model参数实例(google官方示例里又嵌套了几层component)
@Singleton//要求dagger实现单例
@Component(modules = {TasksRepositoryModule.class, ApplicationModule.class})
public interface TasksRepositoryComponent {
TasksRepository getTasksRepository();
}
- Inject:指定依赖,就是说明某个属性对象是需要用Module注入进来的,比如在Presenter里说明某个modle对象和某个view对象是需要dagger注入进来的,代码形如:
//presenter类的参数改用Dagger注入
class TasksPresenter implements TasksContract.Presenter {
private final TasksRepository mTasksRepository;
private final TasksContract.View mTasksView;
/**
* Dagger strictly enforces that arguments not marked with {@code @Nullable} are not injected
* with {@code @Nullable} values.
*/
@Inject //参数是需要注入的
TasksPresenter(TasksRepository tasksRepository, TasksContract.View tasksView) {
mTasksRepository = tasksRepository;
mTasksView = tasksView;
}
...
}
同样,在Activity里也要用dagger注入persenter,代码形如:
public class TasksActivity extends AppCompatActivity {
@Inject TasksPresenter mTasksPresenter;//内部对象是需要注入的
...
}
- Component:组装器,做两件事:1-把做好的Module对象作为参数提供给要注入的类,比如把Modle对象和IView对象实例化,作为Presenter的参数,完成Presenter的实例化;2-把完成注入和实例化的类,注入到当前类里,比如把完成实例化的Presenter注入到Activity里,代码形如:
public class TasksActivity extends AppCompatActivity {
@Inject TasksPresenter mTasksPresenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
...
// Create the presenter
DaggerTasksComponent.builder()
//component会做出presenter需要的两个参数
.tasksRepositoryComponent(((ToDoApplication) getApplication()).getTasksRepositoryComponent())
.tasksPresenterModule(new TasksPresenterModule(tasksFragment))
.build()//构造出Presenter的实例
.inject(this);//把Presenter注入到当前Activity中
...
}
...
}
Dagger只是用注解来配置依赖关系,编译时还是用工厂类和传参等形式实现的依赖注入,例如,针对上述代码,Dagger的apt插件会在编译时把它转成形如这样的代码:
...
//生成的Component类里,Module工厂类实现Module的实例化
this.provideTasksViewProvider = MainModule_ProvideTasksViewFactory.create(builder.mainModule);
...
//生成的Component类里,Presenter工厂类实现Presenter的实例化
mainPresenterProvider = MainPresenter_Factory.create(provideTasksRepositoryProvider,provideTasksViewProvider);
...
//生成的Activity的Injector类里,用构造参数实现依赖注入
this.mainPresenterProvider = mainPresenterProvider;
这样用Dagger实现的MVP,最开始会有点别扭,因为类之间的注入关系好像不像直接代码实现那样熟悉,但习惯之后,你会发现这么几个好处:
1.基于JSR330的稳定和标准的依赖注入方法
2.依赖关系是配置化的,代码可读性更强,也容易聚焦业务
3.可以通过注解实现全局单例
演化5-Kotlin的引入
作为基础通用框架,我们必须有一个Kotlin的版本,当然,不同的演化版本,会有不同的写法,如果参照演化3的版本,对应的Kotlin版本形如:
//基础IMVPView接口
interface IMvpView {
}
//基础MvpPresenter抽象类
abstract class MvpPresenter<T:IMvpView> {
protected var mViewRef:WeakReIference<T>?=null
fun attachView(view:T){
mViewRef= WeakReference(view)
}
fun detachView(){
if(mViewRef!= null){
mViewRef!!.clear()
mViewRef=null
}
}
val view:T? get() = mViewRef!!.get()
}
//业务逻辑接口
interface ICatContract {
interface View<Presenter>{
fun refreshUI()
}
interface Presenter{
fun doInitPage()
}
}
//业务Presenter
class CatPresenter : MvpPresenter<CatActivity>(),ICatContract.Presenter {
override fun doInitPage() {
}
}
//业务Activity
class CatActivity : AppCompatActivity(),MvpView,ICatContract.View<CatPresenter> {
val TAG: String = "CatActivity"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_test)
}
override fun refreshUI() {
}
}
总结
MVP作为一个基础型的结构,核心作用在于辅助我们实行良好的可读性和可维护性,我们可以为一个Presenter提供多种View的实现(例如,一个业务可以同时有全屏Activity和对话框Activity两种形式,分别提供给不同的业务环节,背后却使用同一个Presenter),也可以为一个Presenter提供不同的数据Model(例如,在两个根据后台数据动态绘制界面的Activity实例中,业务逻辑一致,可以使用同一种Presenter,但数据内容不同,就可以使用两个分别注入了不同Model的Presenter实例)
MVP里有Passive View(Presenter通过View的接口操作View,并作为主要的业务驱动方)和Supervisor Controller(Presenter负责大量复杂的View逻辑)两种衍生,
MVP还是一个开放性的结构,你可以根据自己的需要,去规避某些缺陷,或取得某些优势,如何去演化一个适合自己需求的MVP框架,一方面满足需求,一方面保持灵活,完全看自己的发挥了
关于MVVM
MVP的结构比较通透明了,不过其中的View总是要写一些业务逻辑相关的代码,比如操纵Presenter,处理生命周期,实例化Model对象等,如果需要更进一步,把View的角色限定为纯粹的UI,不做任何业务逻辑,不涉及任何数据,就需要用到MVVM模式了。
在MVVM模式里,不再有Presenter,用ViewModel来处理业务逻辑,ViewModel不处理UI,而View只负责UI,与ViewModel建立数据绑定关系,通过databinding自动实现UI和Model之间的数据操作。
技术细节推荐阅读如何构建Android MVVM应用程序
引用
Github Google android-architecture
Android App的设计架构:MVC,MVP,MVVM与架构经验谈
网友评论