雑メモ: ViewModel SavedStateのコードリーディング

ViewModelのSavedStateがどのように実現しているのか、内部でどのように動作しているのか気になったので、ソースコードを読んでみました。

この記事のソースコードは全て、下記のライセンスに従います。

/*
 * Copyright 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

まずsavedstate本体のコード

主な登場クラスは以下です。

SavedStateRegistry.SavedStateProvider

/**
 * This interface marks a component that contributes to saved state.
 */
public interface SavedStateProvider {
   /**
    * Called to retrieve a state from a component before being killed
    * so later the state can be received from {@link #consumeRestoredStateForKey(String)}
    *
    * @return S with your saved state.
    */
    @NonNull
    Bundle saveState();
}

onSaveInstanceStateのタイミングでコールされ、outStateに追加で、保存したい値をBundle型で返却します。

SavedStateRegistry

/**
 * An interface for plugging components that consumes and contributes to the saved state.
 *
 * <p>This objects lifetime is bound to the lifecycle of owning component: when activity or
 * fragment is recreated, new instance of the object is created as well.
 */
public final class SavedStateRegistry {
    public Bundle consumeRestoredStateForKey(@NonNull String key)
    public void registerSavedStateProvider(@NonNull String key,
            @NonNull SavedStateProvider provider)
    public void unregisterSavedStateProvider(@NonNull String key)
    public boolean isRestored()
    public void runOnNextRecreation(@NonNull Class<? extends AutoRecreated> clazz)

    void performRestore(@NonNull Lifecycle lifecycle, @Nullable Bundle savedState)
    void performSave(@NonNull Bundle outBundle)
}

先程のSavedStateProviderをregisterSavedStateProviderメソッドを通して集約し、performRestoreperformSaveによって、savedStateから読み込み、outStateに保存します。

SavedStateRegistryOwner

/**
 * A scope that owns {@link SavedStateRegistry}
 */
public interface SavedStateRegistryOwner extends LifecycleOwner {
    /**
     * Returns owned {@link SavedStateRegistry}
     *
     * @return a {@link SavedStateRegistry}
     */
    @NonNull
    SavedStateRegistry getSavedStateRegistry();
}

SavedStateRegistryOwnerは先程のSavedStateRegistryのOwnerになります。これは、ComponentActivityFragmentが実装しています。LifecycleOwner、Lifecycleのような実装です。

SavedStateRegistryController

/**
 * An API for {@link SavedStateRegistryOwner} implementations to control {@link SavedStateRegistry}.
 * <p>
 * {@code SavedStateRegistryOwner} should call {@link #performRestore(Bundle)} to restore state of
 * {@link SavedStateRegistry} and {@link #performSave(Bundle)} to gather SavedState from it.
 */
public final class SavedStateRegistryController {
    public SavedStateRegistry getSavedStateRegistry()
    public void performRestore(@Nullable Bundle savedState)
    public void performSave(@NonNull Bundle outBundle)

    public static SavedStateRegistryController create(@NonNull SavedStateRegistryOwner owner)
}

SavedStateRegistryOwnerのための実装です。ComponentActivityやFragmentではこのクラスを介して、Bundleから値を復元/restoreしたり、保存/saveします。

これがSavedStateで使われている主なクラスになります。

次に、ViewModelのsaveの実行の流れを見てみます。

ViewModelのsaveの実行の流れ

ViewModelのSavedStateのsaveの実行の流れを見ながら、コードリーディングをしていきます。

1. ViewModelを生成する

ViewModelとSavedStateを一緒に扱い時は、SavedStateVMFactoryを使います。 これは、ViewModelインスタンスにSavedStateHandleインスタンスを渡すために必要なFactoryです。

// thisはFragmentActivity
val vm = ViewModelProvider(this, SavedStateVMFactory(this))
    .get(MyViewModel::class.java)

こう書くことで、MyViewModelでSavedStateHandleを受け取ることが出来ます。

SavedStateVMFactoryでは以下の処理を行っています。

2. ComponentActivity#onSaveInstanceState

public class ComponentActivity ... {
    private final SavedStateRegistryController mSavedStateRegistryController =
            SavedStateRegistryController.create(this);

    protected void onSaveInstanceState(@NonNull Bundle outState) {
        ...
        mSavedStateRegistryController.performSave(outState);
    }
}

まずは、SavedStateRegistryControllerに、outStateに保存を頼みます。

3. SavedStateRegistryController#performSave

public final class SavedStateRegistryController {
    private final SavedStateRegistry mRegistry;

    ...

    public void performSave(@NonNull Bundle outBundle) {
        mRegistry.performSave(outBundle);
    }
}

SavedStateRegistryに処理を委譲します。

4. SavedStateRegistry#performSave

public final class SavedStateRegistry {
    private SafeIterableMap<String, SavedStateProvider> mComponents =
            new SafeIterableMap<>();
    private Bundle mRestoredState;

    ...

    void performSave(@NonNull Bundle outBundle) {
        Bundle components = new Bundle();
        if (mRestoredState != null) {
            components.putAll(mRestoredState);
        }
        for (Iterator<Map.Entry<String, SavedStateProvider>> it =
                mComponents.iteratorWithAdditions(); it.hasNext(); ) {
            Map.Entry<String, SavedStateProvider> entry1 = it.next();
            components.putBundle(entry1.getKey(), entry1.getValue().saveState());
        }
        outBundle.putBundle(SAVED_COMPONENTS_KEY, components);
    }
}

outBundle#putBundleを通して、保存を行います。今で、直接ActivityのonSaveInstanceStateをoverrideして書いていた処理がここに移ったイメージです。

5. SavedStateHandleのSavedStateProviderの実装

実際に保存される内容を決めるのはSavedStateHandleのSavedStateProviderの実装/中身になります。

実装は次のようになっています。

final Map<String, Object> mRegular;

private static final String VALUES = "values";
private static final String KEYS = "keys";

private final SavedStateProvider mSavedStateProvider = new SavedStateProvider() {
    @SuppressWarnings("unchecked")
    @NonNull
    @Overrides
    public Bundle saveState() {
        Set<String> keySet = mRegular.keySet();
        ArrayList keys = new ArrayList(keySet.size());
        ArrayList value = new ArrayList(keys.size());
        for (String key : keySet) {
            keys.add(key);
            value.add(mRegular.get(key));
        }

        Bundle res = new Bundle();
        // "parcelable" arraylists - lol
        res.putParcelableArrayList("keys", keys);
        res.putParcelableArrayList("values", value);
        return res;
    }
};

Mapに保存したい値を保持しておいて、それをBundleに書き出すだけです。

ざっくりと保存の流れはこんな感じです:D

まとめ

Written by
あんどろいどでぃべろっぱぁー🍎