什么时候去解绑 Presenter
在 Android 应用中实现 MVP 模式时,什么时候进行 Presenter 和 View 的 bind/unbind ,我觉得可以参考下这篇文章提到的一些思路。
原文:Where to Unbind the Presenter
为了找到 Android 应用中 MVP 模式的最佳实现,我尝试写了很多种方案。其中有一个问题是被拿出来反复思考的:什么时候允许 Presenter 去改变 UI ? 这篇文章主要是展示在各种解决方案下所产生的不同结果,为大家提供一个参考,而不是提供一个绝对的答案。
方案
1. onCreate() & onDestroy()
通常开发者一般都采用在 View 的 onCreate()
中调用 Presenter 的 bind 方法,在 onDestroy()
中调用 Presenter 的 unbind 方法。这基本上是最容易想到的方案,这样的话 Presenter 从一开始就可以对 View 进行操作,而且像一些使用 Navigation Drawer (侧边抽屉导航)的场景,确实也需要在 View 生命周期的早期进行一些设置操作。
下面一起来撸个示例代码来看看:
// Presenter.java
class Presenter implements SingletonService.Listener {
private SingletonService service;
public void bind(View view) {
service.registerListener(this);
}
public void unbind() {
service.unregisterListener();
}
...
}
注意看上面的简要代码逻辑,Presenter 实现了 SingletonService.Listener 方法,所以可以监听到应用的 SingletonService 的改变。前面有提到,我们是在 View 的 onCreate() 调用 Presenter 的 bind() 方法,此时 Presenter 在 bind 方法中进行 SingletonService 监听器的注册( 调用 registerListener)。然后在 View 的 onDestroy() 中调用 Presenter 的 unbind() 方法,此时 Presenter 在 unbind 方法中进行 SingletonService 监听器的反注册(调用 unregisterListener )。
但从另一方面而言,当 Presenter 正在与一个配置可能随时改变(像是屏幕旋转)的 View 进行交互时,事情就可能变得复杂了。这时你可能会发现:新被创建的 Activity 其 onCreate() 方法在上一次被销毁的调用 onDestroy() 方法之前就被先调用了。
2. onStart() & onStop()
如果你觉得 Presenter 不应该在 View 不可见时去更改视图内容,这时可以考虑在 onStart() 进行 bind,并在 onStop() 中进行 unbind。这对于需要在后台线程执行一些任务后再回来改变 View 内容的场景,是一个更好的方案。但是问题也随之而来,由于你此时也不知道被绑定 View 时从堆栈中返回来的还是新创建的,所以你还需要使用 Presenter 对 View 进行额外的设置(比如处理数据填充)。
3. onResume() & onPause()
有些情况下,为了确保 Presenter 在 View 上进行更改是安全的,那么尽可能地了解用户当前正在与哪个对象进行交互就十分有必要了。所以此时最好的方案是在 View 的 onResume() 中绑定,并在 onPause() 中解除绑定。当然,还有一些情况是需要额外考虑的,因为有些场景需要在 View 处于暂停状态下显示变化(比如在分屏模式下,其中一个应用程序没有获得用户焦点,但是用户期望它能正常工作)。
异步
在执行异步任务时,你需要考虑到用户可能在异步任务完成之前把应用退到后台。在这种情况下,最好是能提前解绑 Presenter ,以避免潜在的内存泄漏和崩溃。在与 Fragment 的事务 (Transaction) 结合使用时,此问题会更为明显。
// Activity.java
public void showArticleView(String article) {
getSupportFragmentManager()
.beginTransaction()
.replace(container, ArticleDetailsFragment.newInstance(article))
.commit();
}
// Presenter.java
public void onArticleClicked(String id) {
service.getArticle(id)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(article -> view.showArticleView(article));
}
如果你的项目中存在像上面这样的代码, 你可能会得到很多
IllegalStateException: Can not perform this action after onSaveInstanceState
的异常日志。当然这可以通过使用
commitAllowingStateLoss()
代替原来的 commit()
来避免,但是在大多数情况下,你并不是真的想失去这个 UI 状态。如果你想深入了解,我推荐这篇文章。
这里我再拓展一下,其实我们在使用 DialogFragment 时,遇到像上面说的那种情况,如果使用 dismiss() 隐藏 DialogFragment, 也是会有
java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
的情况。
因为 onSaveInstanceState 方法是在 Activity 即将被销毁前调用,来保存 Activity 数据的,如果在保存完状态后再给它添加 Fragment 就会出错。解决办法就是把 commit() 方法替换成 commitAllowingStateLoss()。而 DialogFragment 的
dismissAllowingStateLoss 内部实现就是 commitAllowingStateLoss。
注意,上面说到的不仅仅只是 Fragment 的问题,如果你在 Activity 的 onSaveInstanceState()
被调用后去修改 View
对象,那么在活动进程结束之后,这些改变的 UI 状态还是会丢失。
你应该知道 onSaveInstanceState()
只会在 Android 系统觉得应用可能需要恢复 UI 状态的时候才会被调用。所以当你只是正常退出屏幕时(通过后退按钮),该回调并不会被触发,但你仍然需要解除与 Presenter 的绑定。
解决办法
There’s no silver bullet.
没有银弹:复杂的软件工程问题无法靠简单的答案来解决。
说到底,特定场景还是需要根据实际情况特定分析,没有万金油。一般来说,当你的应用程序只做同步处理时(用户交互时有直接的 UI 变化结果),你最好是选择在 onCreate() 和 onDestroy() 进行 bind/unbind。除此之外,我发现最好的办法是在调用 bind 和 unbind 方法时先检查 Presenter 的绑定状态。
//
public abstract class BaseActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
presenter.bind(this);
}
@Override
protected void onRestart() {
super.onRestart();
if (!presenter.isViewBound()) {
presenter.bind(this);
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
presenter.unbind();
super.onSaveInstanceState(outState);
}
@Override
protected void onDestroy() {
if (presenter.isViewBound()) {
presenter.unbind();
}
super.onDestroy();
}
}
public abstract class BaseFragment extends Fragment {
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
presenter.bind(this);
}
@Override
public void onStart() {
super.onStart();
if (!presenter.isViewBound()) {
presenter.bind(this);
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
presenter.unbind();
super.onSaveInstanceState(outState);
}
@Override
public void onDestroyView() {
if (presenter.isViewBound()) {
presenter.unbind();
}
super.onDestroyView();
}
}