The project that I'm working uses a view-presenter abstraction. Here is a simplified version of all the main classes.
The abstract activity (wire Presenter instance, with View)
public abstract class MvpActivity<Presenter extends MvpPresenter>
extends ActionBarActivity {
protected Presenter mPresenter;
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mPresenter = getPresenterInstance();
}
@Override protected void onResume() {
super.onResume();
mPresenter.onResume(this);
}
@Override protected void onPause() {
mPresenter.onPause();
super.onPause();
}
}
The view interface
public interface MyView {
void redirect();
}
The view implementation
public class MyActivity
extends MvpActivity<MyPresenter>
implements MyView {
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.my_view);
Button myButton = (Button)findViewById(R.id.my_button);
myButton.setOnClickListener(v -> mPresenter.onButtonPressed());
}
@Override protected MyPresenter getPresenterInstance() {
return new MyPresenter();
}
@Override void redirect(){
startActivity(new Intent(this, MyOtherActivity.class));
}
The abstract presenter
public abstract class MvpPresenter<ViewType> {
private ViewType mView;
public void onResume(ViewType view) {
mView = view;
}
public void onPause() {
mView = null;
}
protected ViewType getView() {
if (mView == null) {
throw new IllegalStateException("Presenter view is null");
}
return mView;
}
}
And the presenter implementation
public class MyPresenter extends MvpPresenter<MyView> {
@Override public void onResume(MyView myView){
super.onResume(myView);
Log.("MyPresenter", "Presenter resumed");
}
@Override public void onPause(){
super.onPause()
Log.("MyPresenter", "Presenter paused");
}
public void onButtonPressed(){
getView().redirect();
}
}
The issue comes up as an "IllegalStateException: Presenter view is null"
triggered by getView().redirect();
when called from the MyPresenter.onButtonPressed()
method.
This doesn't make any sense to me, as the view should always be not null if the listener is fired. The view is only set to null if the MvpPresenter.onPause()
is executed which is only being called from MvpActivity.onPause()
. I wouldn't expect to receive any click events after this happens, so what am I missing here?
Sadly, I can not reproduce this issue by manually testing the application. The reports are coming in from Crashlytics.
Note: retrolambda is in use for the button click listener
Some ways of fixing this issues:
- https://developer.android.com/reference/android/view/View.html#cancelPendingInputEvents()
- https://github.com/JakeWharton/butterknife/blob/master/butterknife/src/main/java/butterknife/internal/DebouncingOnClickListener.java
Short answer: don't do that.
Unfortunately, you're relying on an order of events that is undefined. Activity lifecycle events and Window events are two different things, even though they're often closely related. You'll get onPause() when the activity is paused for any reason. But the View touch events aren't unhooked until the View's window loses focus.
It's very common for an activity to pause right when its window loses focus--for instance, when the screen is locked or when another activity is launched. But as you've seen, you can get pauses without a focus change and focus changes without a pause. Even when the two events occur together, there's a narrow window of time when onPause() has been called but the window input handlers are still active.
As with any undefined behavior, the actual results you see will vary by OS version and hardware type.
If you need to make sure that you don't receive View messages after onPause, you should unhook your handlers in onPause.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With