Upgrade to Pro — share decks privately, control downloads, hide ads and more …

IllegalStateException: Can not perform this act...

IllegalStateException: Can not perform this action after onSaveInstanceState

There is one thing we all agree about Fragments -- it is easy to fall into problems with them. They were created for a noble reason, but using them involves effort because they can behave in unexpected ways.

When our team got a chance to refactor a legacy codebase, we decided to drop the traditional Android navigation pattern, in favour of a ViewGroup-driven UI stack with a single Activity. This talk is an exploration of our journey so far.

Saket Narayan

April 21, 2018
Tweet

More Decks by Saket Narayan

Other Decks in Technology

Transcript

  1. Introduced in Android 3.0 “An important goal for Android 3.0

    is to make it easier for developers to write applications that can scale across a variety of screen sizes, beyond the facilities already available in the platform” - 2011
  2. java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState at android.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:1109)

    at android.app.FragmentManagerImpl.popBackStackImmediate(FragmentManager.java:399) at android.app.Activity.onBackPressed(Activity.java:2066) at android.app.Activity.onKeyDown(Activity.java:1962) at android.view.KeyEvent.dispatch(KeyEvent.java:2482) at android.app.Activity.dispatchKeyEvent(Activity.java:2274) at com.android.internal.policy.impl.PhoneWindow$DecorView.dispatchKeyEvent(PhoneWindow.java:1668) at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1112) at android.app.Activity.dispatchKeyEvent(Activity.java:2269) at com.android.internal.policy.impl.PhoneWindow$DecorView.dispatchKeyEvent(PhoneWindow.java:1668) at android.view.ViewRoot.deliverKeyEventPostIme(ViewRoot.java:2851) at android.view.ViewRoot.handleFinishedEvent(ViewRoot.java:2824) at android.view.ViewRoot.handleMessage(ViewRoot.java:2011) at android.os.Handler.dispatchMessage(Handler.java:99) at android.os.Looper.loop(Looper.java:132) at android.app.ActivityThread.main(ActivityThread.java:4025) at java.lang.reflect.Method.invokeNative(Native Method) at java.lang.reflect.Method.invoke(Method.java:491) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:841) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:599) at dalvik.system.NativeStart.main(Native Method)
  3. Too many gotchas @Override void onActivityResult(int requestCode, int resultCode, Intent

    data) { if (resultCode == RESULT_OK) { SuccessDialog.show(); } }
  4. java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState at android.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:1109)

    at android.app.FragmentManagerImpl.popBackStackImmediate(FragmentManager.java:399) at android.app.Activity.onBackPressed(Activity.java:2066) at android.app.Activity.onKeyDown(Activity.java:1962) at android.view.KeyEvent.dispatch(KeyEvent.java:2482) at android.app.Activity.dispatchKeyEvent(Activity.java:2274) at com.android.internal.policy.impl.PhoneWindow$DecorView.dispatchKeyEvent(PhoneWindow.java:1668) at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1112) at android.app.Activity.dispatchKeyEvent(Activity.java:2269) at com.android.internal.policy.impl.PhoneWindow$DecorView.dispatchKeyEvent(PhoneWindow.java:1668) at android.view.ViewRoot.deliverKeyEventPostIme(ViewRoot.java:2851) at android.view.ViewRoot.handleFinishedEvent(ViewRoot.java:2824) at android.view.ViewRoot.handleMessage(ViewRoot.java:2011) at android.os.Handler.dispatchMessage(Handler.java:99) at android.os.Looper.loop(Looper.java:132) at android.app.ActivityThread.main(ActivityThread.java:4025) at java.lang.reflect.Method.invokeNative(Native Method) at java.lang.reflect.Method.invoke(Method.java:491) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:841) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:599) at dalvik.system.NativeStart.main(Native Method)
  5. Too many gotchas @Override void onActivityResult(int requestCode, int resultCode, Intent

    data) { if (resultCode == RESULT_OK) { pendingResumeRunnable = () -> { SuccessDialog.show(); }; } } @Override void onResume() { pendingResumeRunnable.run(); }
  6. Too many gotchas @Override void onActivityResult(int requestCode, int resultCode, Intent

    data) { if (resultCode == RESULT_OK) { lifecycle().onResume() .take(1) .subscribe(o -> { SuccessDialog.show(); }); } }
  7. Too many gotchas class Activity { @BindView(R.id.text) TextView textView; @Override

    void onCreate(Bundle s) { super.onCreate(s); setContentView(...); ButterKnife.bind(this); showFragment(...); } void setText(String text) { textView.setText(text); } } class Fragment { @Override void onAttach(Activity a) { a.setText("Blrdroid"); } }
  8. Too many gotchas class Activity { @BindView(R.id.text) TextView textView; @Override

    void onCreate(Bundle s) { super.onCreate(s); setContentView(...); ButterKnife.bind(this); showFragment(...); } void setText(String text) { textView.setText(text); } } class Fragment { @Override void onAttach(Activity a) { a.setText(“Blrdroid"); } }
  9. Too many gotchas class Activity { @BindView(R.id.text) TextView textView; @Override

    void onCreate(Bundle s) { super.onCreate(s); setContentView(...); ButterKnife.bind(this); showFragment(...); } void setText(String text) { textView.setText(text); } } class Fragment { @Override void onAttach(Activity a) { a.setText("Blrdroid"); } }
  10. Too many gotchas class Activity { @BindView(R.id.text) TextView textView; @Override

    void onCreate(Bundle s) { super.onCreate(s); setContentView(...); ButterKnife.bind(this); showFragment(...); } void setText(String text) { textView.setText(text); } } class Fragment { @Override void onAttach(Activity a) { a.setText(“Blrdroid"); } }
  11. Too many gotchas class Activity { @BindView(R.id.text) TextView textView; @Override

    void onCreate(Bundle s) { super.onCreate(s); setContentView(...); ButterKnife.bind(this); showFragment(...); } void setText(String text) { textView.setText(text); } } class Fragment { @Override void onAttach(Activity a) { a.setText(“Blrdroid"); } } NullPointerException!
  12. Too many gotchas Fragments are not resumed when Activity#onResume() is

    called. Use Activity#onResumeFragments() instead.
  13. Fragments are like Bhagwan An extra layer of uncertainty in

    life isn’t worth it. - Saket Narayan
  14. Basic necessities • Lifecycle • Backstack • State persistence •

    Start a screen for result • Screen change animations • Nested screens • Dialog lifecycle and callbacks
  15. Options • Conductor (Blueline) • Flow (Square) • Scoop (Lyft)

    • Magellan (Wealthfront) • RIBs (Uber)
  16. Why not Conductor Designed as a drop-in replacement for fragments.

    public class HomeFragment extends Fragment { @Override View onCreateView(LayoutInflater inflater, ViewGroup container) { View view = inflater.inflate(R.layout.home, container,...); return view; } }
  17. Why not Conductor Designed as a drop-in replacement for fragments.

    public class HomeController extends Controller { @Override View onCreateView(LayoutInflater inflater, ViewGroup container) { View view = inflater.inflate(R.layout.home, container,...); return view; } }
  18. Why not Conductor Designed as a drop-in replacement for fragments.

    void startActivityForResult(...) void requestPermissions(...) void setHasOptionsMenu(...) void onCreateOptionsMenu(...)
  19. Why not Conductor It is not simple. It feels as

    exhaustive as Fragments. void onContextAvailable() void onContextUnavailable() void onSaveViewState(...) void onSaveInstanceState(...)
  20. Why Flow Is a library for navigating between scopes (and

    not screens) Billing Dashboard User preferences
  21. Why Flow Pro: Extremely simple. Flow is just a back-stack

    for scopes. Con: No batteries included. Flow does not even inflate Views.
  22. Options • Conductor (Blueline) • Flow (Square) • Scoop (Lyft)

    • Magellan (Wealthfront) • RIBs (Uber)
  23. Setup class TheActivity { @Inject ScreenRouter screenRouter; @Override void onAttachBaseContext(Context

    c) { super.onAttachBaseContext(screenRouter.install(c)); } @Override void onBackPressed() { BackPressCallback callback = screenRouter.pop(); if (!callback.popped()) { super.onBackPressed(); } } }
  24. Screen routing class DashboardScreen extends FrameLayout { public static final

    ScreenKey KEY = DashboardScreen.Key.create(); public DashboardScreen(Context context, AttributeSet attrs) { super(context, attrs); } }
  25. Screen routing class DashboardScreen extends FrameLayout { public static final

    ScreenKey KEY = DashboardScreen.Key.create(); public DashboardScreen(Context context, AttributeSet attrs) { super(context, attrs); } }
  26. Screen routing class LogInScreen extends FrameLayout { @Inject ScreenRouter screenRouter;

    void openDashboard() { screenRouter.push(DashboardScreen.KEY); } }
  27. Screen routing class LogInScreen extends FrameLayout { @Inject ScreenRouter screenRouter;

    void openDashboard() { screenRouter.push(DashboardScreen.KEY); } }
  28. Screen routing class LogInScreen extends FrameLayout { @Inject ScreenRouter screenRouter;

    void openDashboard() { screenRouter.push(DashboardScreen.KEY_BUILDER .theme(Theme.DARK) .key1(“value1”) .key2("value2") .build()); } }
  29. Screen routing class DashboardScreen extends FrameLayout { @Inject ScreenRouter screenRouter;

    void onAttachedToWindow() { super.onAttachedToWindow(); DashboardScreen.Key key = screenRouter.key(this); applyTheme(key.theme()); } }
  30. Screen routing class DashboardScreen extends FrameLayout { @Inject ScreenRouter screenRouter;

    void onAttachedToWindow() { super.onAttachedToWindow(); DashboardScreen.Key key = screenRouter.key(this); applyTheme(key.theme()); } }
  31. Inflating screens @Override void changeKey(State outgoing, State incoming, Direction direction,

    ...) { if (frame.getChildCount() > 0) { View outgoingView = frame.getChildAt(0); if (outgoing != null) { outgoing.save(outgoingView); } frame.removeAllViews(); } View incomingView = inflate(layoutResIdForKey(incoming), ...); frame.addView(inView); incoming.restore(inView); }
  32. Inflating screens @Override void changeKey(State outgoing, State incoming, Direction direction,

    ...) { if (frame.getChildCount() > 0) { View outgoingView = frame.getChildAt(0); if (outgoing != null) { outgoing.save(outgoingView); } frame.removeAllViews(); } View incomingView = inflate(layoutResIdForKey(incoming), ...); frame.addView(inView); incoming.restore(inView); }
  33. Inflating screens @Override void changeKey(State outgoing, State incoming, Direction direction,

    ...) { if (frame.getChildCount() > 0) { View outgoingView = frame.getChildAt(0); if (outgoing != null) { outgoing.save(outgoingView); } frame.removeAllViews(); } View incomingView = inflate(layoutResIdForKey(incoming), ...); frame.addView(inView); incoming.restore(inView); }
  34. Inflating screens @Override void changeKey(State outgoing, State incoming, Direction direction,

    ...) { if (frame.getChildCount() > 0) { View outgoingView = frame.getChildAt(0); if (outgoing != null) { outgoing.save(outgoingView); } frame.removeAllViews(); } View incomingView = inflate(layoutResIdForKey(incoming), ...); frame.addView(inView); incoming.restore(inView); }
  35. Inflating screens @Override void changeKey(State outgoing, State incoming, Direction direction,

    ...) { if (frame.getChildCount() > 0) { View outgoingView = frame.getChildAt(0); if (outgoing != null) { outgoing.save(outgoingView); } frame.removeAllViews(); } View incomingView = inflate(layoutResIdForKey(incoming), ...); frame.addView(inView); incoming.restore(inView); }
  36. Inflating screens @Override void changeKey(State outgoing, State incoming, Direction direction,

    ...) { if (frame.getChildCount() > 0) { View outgoingView = frame.getChildAt(0); if (outgoing != null) { outgoing.save(outgoingView); } frame.removeAllViews(); } View incomingView = inflate(layoutResIdForKey(incoming), ...); frame.addView(inView); incoming.restore(inView); }
  37. Inflating screens @Override void changeKey(State outgoing, State incoming, Direction direction,

    ...) { if (frame.getChildCount() > 0) { View outgoingView = frame.getChildAt(0); if (outgoing != null) { outgoing.save(outgoingView); } frame.removeAllViews(); } View incomingView = inflate(layoutResIdForKey(incoming), ...); frame.addView(inView); incoming.restore(inView); }
  38. Screen routing FYI: changeKey() gets called on every app resume.

    Make sure you only change Views when needed. https://github.com/square/flow/issues/173
  39. Sending screen results class DashboardScreen extends FrameLayout { void onAttachedToWindow()

    { gifButton.setOnClickListener(o -> screenRouter.push(GifPickerScreen.KEY); ); screenRouter.streamResults() .ofType(GifPickerResult.class) .takeUntil(lifecycle.onDetach()) .subscribe(result -> { gifImageView.setImage(result.gif()); }); } }
  40. Sending screen results class DashboardScreen extends FrameLayout { void onAttachedToWindow()

    { gifButton.setOnClickListener(o -> screenRouter.push(GifPickerScreen.KEY); ); screenRouter.streamResults() .ofType(GifPickerResult.class) .takeUntil(lifecycle.onDetach()) .subscribe(result -> { gifImageView.setImage(result.gif()); }); } }
  41. Sending screen results class GifPickerScreen extends FrameLayout { void onGifSelected(Gif

    gif) { GifPickerResult result = GifPickerResult.create(gif); screenRouter.sendResultAndPop(result); } }
  42. Sending screen results class GifPickerScreen extends FrameLayout { void onGifSelected(Gif

    gif) { GifPickerResult result = GifPickerResult.create(gif); screenRouter.sendResultAndPop(result); } }
  43. Sending screen results class DashboardScreen extends FrameLayout { void onAttachedToWindow()

    { gifButton.setOnClickListener(o -> screenRouter.push(GifPickerScreen.KEY); ); screenRouter.streamResults() .ofType(GifPickerResult.class) .takeUntil(lifecycle.onDetach()) .subscribe(result -> { gifImageView.setImage(result.gif()); }); } }
  44. Sending screen results class DashboardScreen extends FrameLayout { void onAttachedToWindow()

    { gifButton.setOnClickListener(o -> screenRouter.push(GifPickerScreen.KEY); ); screenRouter.streamResults() .ofType(GifPickerResult.class) .takeUntil(lifecycle.onDetach()) .subscribe(result -> { gifImageView.setImage(result.gif()); }); } } Observable<Result>
  45. Sending screen results class DashboardScreen extends FrameLayout { void onAttachedToWindow()

    { gifButton.setOnClickListener(o -> startActivityForResult(GifPickerActivity.intent(), REQ_CODE_GIF); ); screenRouter.streamResults() .ofType(ActivityResult.class) .filter(result -> result.requestCode() == REQ_CODE_GIF) .flatMap(result -> GifPickerActivity.extractGif(result.intentData()) .takeUntil(lifecycle.onDetach()) .subscribe(gif -> { gifImageView.setImage(gif); }); } }
  46. Sending screen results class DashboardScreen extends FrameLayout { void onAttachedToWindow()

    { gifButton.setOnClickListener(o -> startActivityForResult(GifPickerActivity.intent(), REQ_CODE_GIF); ); screenRouter.streamResults() .ofType(ActivityResult.class) .filter(result -> result.requestCode() == REQ_CODE_GIF) .flatMap(result -> GifPickerActivity.extractGif(result.intentData()) .takeUntil(lifecycle.onDetach()) .subscribe(gif -> { gifImageView.setImage(gif); }); } }
  47. Sending screen results class DashboardScreen extends FrameLayout { void onAttachedToWindow()

    { gifButton.setOnClickListener(o -> startActivityForResult(GifPickerActivity.intent(), REQ_CODE_GIF); ); screenRouter.streamResults() .ofType(ActivityResult.class) .filter(result -> result.requestCode() == REQ_CODE_GIF) .flatMap(result -> GifPickerActivity.extractGif(result.intentData()) .takeUntil(lifecycle.onDetach()) .subscribe(gif -> { gifImageView.setImage(gif); }); } }
  48. Sending screen results class TheActivity extends Activity { @Override void

    onActivityResult(int requestCode, int resultCode, Intent data) { screenRouter.sendResult(ActivityResult.create(requestCode, resultCode, data)); } }
  49. Sending screen results class DashboardScreen extends FrameLayout { void onAttachedToWindow()

    { gifButton.setOnClickListener(o -> startActivityForResult(GifPickerActivity.intent(), REQ_CODE_GIF); ); screenRouter.streamResults() .ofType(ActivityResult.class) .filter(result -> result.requestCode() == REQ_CODE_GIF) .flatMap(result -> GifPickerActivity.extractGif(result.intentData()) .takeUntil(lifecycle.onDetach()) .subscribe(gif -> { gifImageView.setImage(gif); }); } }
  50. Life is synchronous again DashboardScreen dashboardScreen = inflate(...); dashboardScreen.title.setText(...); class

    DashboardScreen { TextView title; @Override void onFinishInflate() { title = findViewById(...); } }
  51. Life is synchronous again DashboardScreen dashboardScreen = inflate(...); dashboardScreen.title.setText(...); class

    DashboardScreen { TextView title; @Override void onFinishInflate() { title = findViewById(...); } }
  52. Life is synchronous again DashboardScreen dashboardScreen = inflate(...); dashboardScreen.title.setText(...); class

    DashboardScreen { TextView title; @Override void onFinishInflate() { title = findViewById(...); } }
  53. Life is synchronous again DashboardScreen dashboardScreen = inflate(...); dashboardScreen.title.setText(...); class

    DashboardScreen { TextView title; @Override void onFinishInflate() { title = findViewById(...); } }
  54. Consistent Lifecycle class Activity { @Override void onCreate(Bundle b) {

    super.onCreate(); setContentView(...); } @Override void onRestoreInstanceState(Bundle b) { // Views get restored here. super.onRestoreInstanceState(b); } }
  55. Intercepting View inflation class DaggerLayoutFactory implements LayoutInflater.Factory2 { private DaggerComponent

    component; public View onCreateView(View parent, String name, ...) { View view = inflate(parent, name); if (view instanceOf ViewWithDaggerDependencies) { view.injectDependencies(component); } } }
  56. Intercepting View inflation class DaggerLayoutFactory implements LayoutInflater.Factory2 { private DaggerComponent

    component; public View onCreateView(View parent, String name, ...) { View view = inflate(parent, name); if (view instanceOf ViewWithDaggerDependencies) { view.injectDependencies(component); } } }
  57. Intercepting View inflation class DaggerLayoutFactory implements LayoutInflater.Factory2 { private DaggerComponent

    component; public View onCreateView(View parent, String name, ...) { View view = inflate(parent, name); if (view instanceOf ViewWithDaggerDependencies) { view.injectDependencies(component); } } }
  58. Google Maps class MapsScreen extends FrameLayout { @Inject TheActivity activity;

    void onAttachedToWindow() { activity.getFragmentManager() .beginTransaction() .add(getId(), MapFragment.newInstance()) .commitNowAllowingStateLoss(); } void onDetachedFromWindow() { activity.getFragmentManager() .beginTransaction() .remove(mapFragment) .commitNowAllowingStateLoss(); } }
  59. Google Maps class MapsScreen extends FrameLayout { @Inject TheActivity activity;

    void onAttachedToWindow() { activity.getFragmentManager() .beginTransaction() .add(getId(), MapFragment.newInstance()) .commitNowAllowingStateLoss(); } void onDetachedFromWindow() { activity.getFragmentManager() .beginTransaction() .remove(mapFragment) .commitNowAllowingStateLoss(); } } #YOLO
  60. If Views don’t work out @Override void changeKey(State outgoing, State

    incoming, Direction direction, ...) { if (frame.getChildCount() > 0) { View outgoingView = frame.getChildAt(0); if (outgoing != null) { outgoing.save(outgoingView); } frame.removeAllViews(); } View incomingView = inflate(layoutResIdForKey(incoming), ...); frame.addView(inView); incoming.restore(inView); }
  61. If Views don’t work out @Override void changeKey(State outgoing, State

    incoming, Direction direction, ...) { if (frame.getChildCount() > 0) { View outgoingView = frame.getChildAt(0); if (outgoing != null) { outgoing.save(outgoingView); } frame.removeAllViews(); } View incomingView = inflate(layoutResIdForKey(incoming), ...); frame.addView(inView); incoming.restore(inView); }
  62. If Views don’t work out @Override void changeKey(State outgoing, State

    incoming, Direction direction, ...) { Fragment incomingFrag = ScreenFragment.create(incoming.key()); fragmentManager .beginTransaction() .replace(frame.getId(), incomingFrag) .commitNow(); }