Go With the Flow – Handling Configuration Changes with Flow


Last week we covered how to use flow with a master-detail view. I got some great feedback from Ray Ryan and Logan Johnson from the Square team. This week I’m going to be covering how to handle rotation.

Crash on Rotation

Screen Shot 2016-03-05 at 2.22.14 PM

If you had tried rotating last weeks example you would have noticed a bug. Specifically when you rotate from the master-detail in landscape to portrait that it would crash. We’re going to figure out how to solve that. First let’s take a look at our stack trace:


E/AndroidRuntime: FATAL EXCEPTION: main
     Process: com.jimbaca.masterdetails, PID: 28989
     java.lang.RuntimeException: Unable to start activity
ComponentInfo{com.jimbaca.masterdetails/com.jimbaca.masterdetails.MainActivity}: 
java.lang.NullPointerException: Attempt to invoke virtual method 
'int android.view.ViewGroup.getChildCount()' on a null object reference
     at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2416)
     at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2476)
     at android.app.ActivityThread.handleRelaunchActivity(ActivityThread.java:4077)
     at android.app.ActivityThread.-wrap15(ActivityThread.java)
     at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1350)

The getChildCount is from this line:

   if (target.getChildCount() > 0) {

Screen Shot 2016-03-05 at 2.30.25 PM

Upon further investigation we see that target isn’t set and it’s crashing because the set contents view hasn’t been called after the rotation and we are trying to handle the top most item of the back stack. What we need to do after rotation is to make sure that our default key has already been called. There many ways of doing this the initial thought would be that we check if any of our “panes” exist for placing content into. If we do a findViewById and can’t find any of the panes, then that must mean that we need to run our default key first. Here is what my code looks like for that:

public void dispatch(@NonNull Traversal traversal, @NonNull TraversalCallback callback) {

   Object dest = traversal.destination.top();

   @LayoutRes final int layout;
   ViewGroup target = null;
   boolean shouldRun = needsInitialization();

   if (dest instanceof DefaultKey) {
+       initialize();
+       callback.onTraversalCompleted();

       return;
+   } else if(shouldRun){
+       initialize();
+   }


   if (dest instanceof ContactsUiKey) {
       layout = R.layout.list_contacts_screen;
       target = getLeftPane();
   } else if (dest instanceof EditNameScreen) {
       layout = R.layout.edit_name_screen;
       target = getRightPane();
   } else {
       throw new AssertionError("Unrecognized screen " + dest);
   }

   if (target.getChildCount() > 0) {
       target.removeAllViews();
   }

   View incomingView = LayoutInflater.from(traversal.createContext(dest, activity)) //
           .inflate(layout, target, false);

   target.addView(incomingView);
   traversal.getState(traversal.destination.top()).restore(incomingView);

   callback.onTraversalCompleted();

}

+private void initialize() {
+   showLayout(activity, R.layout.activity_main);
+
+   Flow.get(activity).set(new ContactsUiKey());
+}

private void showLayout(Context context, @LayoutRes int layout) {
   LayoutInflater inflater = LayoutInflater.from(context);
   activity.setContentView(inflater.inflate(layout, null));
}

+private boolean needsInitialization() {
+   return getLeftPane() == null;
+}

major bummer

Great now if we test this same code on a tablet we see that it still works. We try rotating on the tablet list view, no more crash. This is great. But what happens when we try rotating on one of the detail views? Major bummer, it rotates without a crash but the view we are left with is the list view not the detail view. Looks like our initialization handling is causing our backstack to be changed when we don’t want it to be, specifically in the case when the key isn’t the default key. So let’s change the code a bit more to tackle that problem.

@Override
public void dispatch(@NonNull Traversal traversal, @NonNull TraversalCallback callback) {

   Object dest = traversal.destination.top();

   @LayoutRes final int layout;
   ViewGroup target = null;
   boolean shouldRun = needsInitialization();

   if (dest instanceof DefaultKey) {
       initialize();
       callback.onTraversalCompleted();

       return;
   } else if(shouldRun){
       initialize();
   }


   if (dest instanceof ContactsUiKey) {
       layout = R.layout.list_contacts_screen;
       target = getLeftPane();
   } else if (dest instanceof EditNameScreen) {
       layout = R.layout.edit_name_screen;
       target = getRightPane();
   } else {
       throw new AssertionError("Unrecognized screen " + dest);
   }

   if (target.getChildCount() > 0) {
       target.removeAllViews();
   }

   View incomingView = LayoutInflater.from(traversal.createContext(dest, activity)) //
           .inflate(layout, target, false);

   target.addView(incomingView);
   traversal.getState(traversal.destination.top()).restore(incomingView);

   callback.onTraversalCompleted();

}

private void initialize() {
   showLayout(activity, R.layout.activity_main);

   Flow.get(activity).set(new ContactsUiKey());
}

private void showLayout(Context context, @LayoutRes int layout) {
   LayoutInflater inflater = LayoutInflater.from(context);
   activity.setContentView(inflater.inflate(layout, null));
}

private boolean needsInitialization() {
   return getLeftPane() == null;
}

Summary

Flow is great as a custom back stack, but we need to fill in the logic for handling not just the back stack but configuration changes.

Problems

This doesn’t work for tablets, but I’ll cover that in another post.

Leave a Reply

Your email address will not be published. Required fields are marked *