Help us understand the problem. What is going on with this article?

ViewModelのデータを保存/復元するにはSavedStateHandleを使う

こうだ

saved_state_handle.gif

Developer optionsDon't keep activities を有効にしています

Androidとメモリー上のデータ

Androidはすぐメモリー上のデータが飛びます。
画面を回転したら飛ぶ、システムがアクティビティを破棄したら飛ぶ、システムがプロセスをキルったら飛ぶ。

これらはAndroidのライフサイクルのせいで、上記のタイミングでActivityやFragmentのインスタンスが都度作り直しになるためです。
そのために開発者はActivityやFragmentの onSaveInstanceState関数で飛ばしたくないデータを保存し、 onCreate 関数などで savedInstanceState からデータを復元する処理を書かなくてはいけませんでした。

ViewModelの登場

AndroidでViewModelクラスが登場して以降、開発者は様々な恩恵を受けられるようになりました。

その一つにメモリー上のデータが飛びにくくなったことがあります。
ViewModelは画面を回転させてもデータは飛びません! 自動で復元されます!
やったね!……?

ViewModelの問題

しかしViewModelといえど、システムがアクティビティを破棄した時、システムがプロセスをキルした時はやはり飛びます。No, やったね!。やってない。

結局、ViewModle内の状態も savedInstanceState に都度保存・から都度復元しなければならないのでしょうか?

SavedStateHandle

いいえ、そんなことはありません。ViewModelには SavedStateHandle という便利なものが用意されています。

SavedStateHandle はViewModel内のプロパティの保存/復元を行うためのヘルパークラスです。
SavedStateHandle の内部でプロパティの実体を保持しており、ライフサイクルを勝手に観測していい感じに保存・復元をしてくれます。

ここで例を見てみましょう。
保存/復元を行いたいのは EditText とデータバインディングされた val userTextInput: MutableLiveData<String> プロパティであるとします。
保存/復元を考えない場合、 userTextInputval userTextInput = MutableLiveData<String>("") のように宣言されていたことでしょう。
これをSavedStateHandleを使って保存/復元するには以下のように書きます。

MainFragment.kt
class MainFragment : Fragment() {
    ...

    // SavedStateHandleを使っても使わなくてもViewModelの初期化方法は同じ。
    private val viewModel: MainViewModel by viewModels()

    ...
}
MainViewModel.kt
class MainViewModel(
    // ViewModelの第1引数にSavedStateHandleを指定します
    handle: SavedStateHandle
) : ViewModel() {
    // SavedStateHandleからキー"USER_TEXT_INPUT"にひも付くMutableLiveDataを探して引っ張ってくる。
    // 存在しない場合は新しく作る(そしてSavedStateHandle内でキー"USER_TEXT_INPUT"に対してひも付け、抱えておく)。
    // 初期値は空文字。
    val userTextInput: MutableLiveData<String> = handle.getLiveData<String>("USER_TEXT_INPUT", "")

普通にMutableLiveDataのコンストラクタから宣言していたのを handle の関数から取得するようにしただけです。
これだけでシステムがアクティビティを破棄する時、システムがプロセスをキルする時はその時のuserTextInputの値を勝手に保存してくれますし、復帰する時は勝手に復元してくれます。

val userTextInput = MutableLiveData<String>("") (Simple LiveData)方式と val userTextInput: MutableLiveData<String> = handle.getLiveData<String>("USER_TEXT_INPUT", "")
(SavedStateHandle#getLiveData)方式の振る舞いを比較したのが冒頭の動画になります。
EditText に文字を入力したあとホームボタンを押してランチャー画面を表示させ、再度アプリアイコンをタップしてアプリを起動させたところ、Simple LiveDataの方は入力したデータが飛んでしまっていますが、SavedStateHandle#getLiveDataの方はきちんとデータが残っています。これは保存/復元が行われたということです。今度こそやったね!

なお、handle.getLiveData<T>(...)T になれるのは、

  • 基本型
    • Boolean
    • Short
    • Int
    • Long
    • Float
    • Double
    • String
    • Byte
    • Char
    • CharSequence
  • Parcelable
  • Array<基本型 または Parcelable>
  • Serializable
  • Binder
  • Bundle
  • Size
  • SizeF
  • ArrayList<Tになれるどれか>
  • SparseArray<Tになれるどれか>

のみです。
T がエンティティクラスの場合はParcelableを継承し実装するのがよいでしょう。

ちなみに

SavedStateHandleget という関数も持っています。
これは MutableLiveData<T> ではなく生の値(T)を返す関数です。
使う機会は多くないかもしれませんが覚えておくといいでしょう。

おわりに

Androidのライフサイクルに泣きたくなるのをこらえて頑張りましょう!

追記

続きのような立ち位置の記事を書きました。合わせてどうぞ。

Repositoryから取得したLiveDataをViewModelでキャッシュしておくには?

que9
何でも屋になりたい。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした