Friday, August 21, 2015

Some twists of using Google Maps in your Android app - Part 2: Adding a Floating Action Button and fighting BadParcelableException

Part 1: Smart follow

If you want to add a custom control over your custom MapFragment, there's actually more than one way to do that. Either you can add your control in the layout of your enclosing Activity, or you can override the onCreateView method in your Fragment and add the view there.

Because the previous will add an element that is visible on top of all your Fragments displayed in your Activity (which sometimes can be useful, too), now I'm gonna go with the latter:
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
    View view = super.onCreateView(inflater, parent, savedInstanceState);
    TouchableWrapper touchableWrapper = new TouchableWrapper(getActivity(), this);
    touchableWrapper.addView(view);

    View controls = inflater.inflate(R.layout.map_overlay_controls, null);
    touchableWrapper.addView(controls);

    return touchableWrapper;
}
Here I'm inflating a TouchableWrapper (see Part 1 to see why) and adding a custom layout on top of that, but you could just skip those lines and call view.addView(controls) directly. In this case I'm adding a Floating Action Button from https://github.com/clans/FloatingActionButton among other things.

Now the problem might not appear immediately - unless of course your mommy have taught you to always use the Don't keep activities flag in the Developer options when you were a little toddler, as she should have.

The next time your app comes back after having been put to rest by the OS and tries to restore its state, there's a good chance that it will crash and burn with a BadParcelableException:
java.lang.RuntimeException: Unable to start activity ComponentInfo{...}: android.os.BadParcelableException: ClassNotFoundException when unmarshalling: com.github.clans.fab.FloatingActionButton$ProgressSavedState
            at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2314)
            at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2388)
            at android.app.ActivityThread.access$800(ActivityThread.java:148)
            at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1292)
            at android.os.Handler.dispatchMessage(Handler.java:102)
            at android.os.Looper.loop(Looper.java:135)
            at android.app.ActivityThread.main(ActivityThread.java:5312)
            at java.lang.reflect.Method.invoke(Native Method)
            at java.lang.reflect.Method.invoke(Method.java:372)
            at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:901)
            at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:696)
     Caused by: android.os.BadParcelableException: ClassNotFoundException when unmarshalling: com.github.clans.fab.FloatingActionButton$ProgressSavedState
So what is the problem here and how can you solve it?

The root of the problem is that when your MapFragment saved its state, that also included saving the state of its views - and we just inflated a custom class and injected it in the layout.

When unpacking, it just doesn't know how to handle FloatingActionButton$ProgressSavedState without an appropriate ClassLoader.

So what if we just let MapFragment save its own state and handle the rest outside of it?

First, override how onSaveInstance is implemented:
public static final String KEY_MAP_FRAGMENT_STATE = "com.example.app.mapFragmentState";

public void onSaveInstanceState(Bundle outState) {
    Bundle mapFragmentState = new Bundle();
    super.onSaveInstanceState(mapFragmentState);
    outState.putBundle(KEY_MAP_FRAGMENT_STATE, mapFragmentState);
}
So here, instead of letting the parent put everything in outState, we create a new, empty Bundle object, and pass that to super. Clean and isolated. Then we put the Bundle in outState under a new key.

Now we only have to do this in reverse, override how unpacking is done. First, define how to handle the Bundle:
private Bundle getMapFragmentState(Bundle savedInstanceState) {
    return savedInstanceState != null ? savedInstanceState.getBundle(KEY_MAP_FRAGMENT_STATE) : null;
}
And now pass its result in onCreate an onCreateView to the super method instead of the savedInstanceState directly:
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(getMapFragmentState(savedInstanceState));
}

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
    View view = super.onCreateView(inflater, parent, getMapFragmentState(savedInstanceState));
    TouchableWrapper touchableWrapper = new TouchableWrapper(getActivity(), this);
    touchableWrapper.addView(view);

    View controls = inflater.inflate(R.layout.map_overlay_controls, null);
    touchableWrapper.addView(controls);

    return touchableWrapper;
}

No comments:

Post a Comment