Search code examples
androidandroid-fragmentsandroid-lifecycleandroid-savedstateviewmodel-savedstate

Android ViewModel with SavedState rewrites arguments for new instances


I have an app which is having Master/Detail kind of architecture. When I select some item, the details are displayed in details fragment.

For the detail fragment to know what to load id is sent over via Arguments. Using SavedStateHandle in VM I can directly read these arguments from the handle without re-routing it in Fragment itself. That works great for the first detail fragment. The problem comes with next selections.

Every time I load details, the id from first selection is populated although every time a new fragment is created along with new ViewModel.

I was looking into the code of lifecycle-viewmodel-savedstate library (v.2.3.1) and found this method in SavedStateHandle called:

static SavedStateHandle createHandle(@Nullable Bundle restoredState,
        @Nullable Bundle defaultState) {
    if (restoredState == null && defaultState == null) {
        return new SavedStateHandle();
    }

    Map<String, Object> state = new HashMap<>();
    if (defaultState != null) {
        for (String key : defaultState.keySet()) {
            state.put(key, defaultState.get(key));
        }
    }

    if (restoredState == null) {
        return new SavedStateHandle(state);
    }

    ArrayList keys = restoredState.getParcelableArrayList(KEYS);
    ArrayList values = restoredState.getParcelableArrayList(VALUES);
    if (keys == null || values == null || keys.size() != values.size()) {
        throw new IllegalStateException("Invalid bundle passed as restored state");
    }
    for (int i = 0; i < keys.size(); i++) {
        state.put((String) keys.get(i), values.get(i));
    }
    return new SavedStateHandle(state);
}

Here I can see that in defaultState the correct id is set for every new view model. But as you can see defaultState is processed first and restoredState is processed after that. restoredState contains same key with old id which at the end replace the correct one from defaultState.

I can understand that is probably wanted behavior for real restoring but in my case I'm not restoring the fragment. Yes, the class is same but I'm just replacing detail fragment with another detail fragment with new/different data.

Am I doing something wrong? Can I give the framework a hint that this is not restoration and I'm not interested in saved values from old fragment?


Solution

  • What I didn't mentioned was that I'm using ViewPager2 for Master/Detail view. I didn't consider it having some influence on state staving but it has.

    ViewPager2 library is saving and restoring state on the background which is then used by SavedStateHandle. To map fragments with states it uses ids from FragmentStateAdapter. I didn't override getItemId method so ids were assigned by position.

    This of course map same state to new fragments on the exact position. Implementing getItemId which assign different ids to different instances of fragment fixed the problem.