Galapagos Tech Blog

株式会社ガラパゴスのメンバーによる技術ブログです。

icepick に別れを告げる

こんにちは。エンジニアの松下です。 最近視力が落ちたな〜と思い、そろそろメガネを新調したいな〜と思っています。

そういえば、 DroidKaigi 2024 が発表されました。去年に引き続き弊社も参加したい!

弊社にも結構長く保守している案件がありまして、その新 OS バージョン対応を行っている際にコンパイルが通らなくなっていることに気が付きました。
原因を追っていると、どうも icepick のコード自動生成部分が悪さをしているようでした。

github.com

icepick は @State というアノテーションをつけておけばいい感じに View や Activity の状態を保持できるスグレモノなのですが、自分の記憶だと kotlin でうまく動かなくて、なんやかんやで SavedStateHandle とかあるし最近は使われなくなった印象です。

developer.android.com

icepick 久しぶりに見たな〜と懐かしみつつ、今回のアプリは ViewModel とか使ってないのでどうしようかなと悩んでいたところ、 SavedStateRegistry.SavedStateProvider を使えばいい感じになるのではと思い、以下のように実装してみました。

developer.android.com

public class SavedStateProvider<T extends Serializable> implements SavedStateRegistry.SavedStateProvider {
    private final String provider;
    private final String tag;

    private final WeakReference<SavedStateRegistryOwner> registryOwner;

    @Nullable
    private T state;

    public SavedStateProvider(SavedStateRegistryOwner registryOwner, String tag) {
        this(registryOwner, tag, null);
    }

    public SavedStateProvider(SavedStateRegistryOwner registryOwner, String tag, @Nullable T defaultValue) {
        this.registryOwner = new WeakReference<>(registryOwner);
        this.tag = tag;
        this.provider = "SavedStateProvider: " + tag;
        this.state = defaultValue;

        registryOwner.getLifecycle().addObserver((LifecycleEventObserver) (source, event) -> {
            switch (event) {
                case ON_CREATE -> {
                    SavedStateRegistry registry = registryOwner.getSavedStateRegistry();
                    registry.registerSavedStateProvider(provider, this);
                    restore(registryOwner);
                    break;
                }
                case ON_DESTROY -> {
                    this.registryOwner.clear();
                    break;
                }
                default -> {
                    break;
                }
            }
        });
    }

    @Nullable
    public T getState() {
        return state;
    }

    public void setState(@Nullable T state) {
        this.state = state;
    }

    /**
     * どうしても同期的にリストアされたいときに気合でリストアを試みる
     *
     * @return state
     */
    public T restore() {
        return this.restore(registryOwner.get());
    }

    @NonNull
    @Override
    public Bundle saveState() {
        Bundle bundle = new Bundle();
        bundle.putSerializable(tag, state);
        Timber.d("saveSate: (tag: %s, state: %s)", tag, state);
        return bundle;
    }

    /**
     * @noinspection unchecked
     */
    private T restore(SavedStateRegistryOwner registryOwner) {
        SavedStateRegistry registry = registryOwner.getSavedStateRegistry();
        Bundle state = registry.consumeRestoredStateForKey(provider);

        if (state != null) {
            Timber.d("restore: (tag: %s, state: %s)", tag, state.getSerializable(tag));
            this.state = (T) state.getSerializable(tag);
        }

        return this.state;
    }
}

利用例

public class HogeActivity extends AppCompatActivity {
    private final SavedStateProvider<Boolean> fuga = new SavedStateProvider<>(this, "fuga", false);

    void piyo() {
        if (!fuga.getState()) {
            fuga.setState(true);
        }
    }
}

これで自動でリストアや保存が行われ、画面回転やアクティビティ破棄に対応できるようになりました。
ただし、 LifecycleEventObserver で自動でやる場合は利用側の onCreate ではリストアが間に合わないので、要件によっては同期的にコントロールするほうが良いかもしれません。

しかし、いい時代になったものだ…。そして超久しぶりに Java ちゃんと書いた。

いつもの

弊社ではいっしょに働くアプリエンジニアを募集しています!
ご興味のある方はぜひご応募いただけますと嬉しいです。

Androidエンジニア募集中!: www.wantedly.com
リードエンジニア(AndroidiOS問わず)も募集中!:www.wantedly.com