前提
lifecycle-viewmodel-compose
の2.5.0から saveable APIが追加されました。
まだ試験運用版ではありますが、これとStateを併用することで簡単に状態をSavedStateHandleに保存することができます。
詳しい使い方については下記をご覧ください。
疑問
このsaveableの実相を見てみましょう
@SavedStateHandleSaveableApi
@JvmName("saveableMutableState")
fun <T : Any, M : MutableState<T>> SavedStateHandle.saveable(
stateSaver: Saver<T, out Any> = autoSaver(),
init: () -> M,
): PropertyDelegateProvider<Any?, ReadWriteProperty<Any?, T>> =
PropertyDelegateProvider<Any?, ReadWriteProperty<Any?, T>> { _, property ->
val mutableState = saveable(
key = property.name,
stateSaver = stateSaver,
init = init
)
// Create a property that delegates to the mutableState
object : ReadWriteProperty<Any?, T> {
override fun getValue(thisRef: Any?, property: KProperty<*>): T =
mutableState.getValue(thisRef, property)
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) =
mutableState.setValue(thisRef, property, value)
}
}
// ※ lifecycle-viewmodel-compose:2.6.2時点
SavedStateHandleに値を保存しているのに、saveable使用時にキーを指定する必要がないのは、自動で property.name
をキーとしているからです。デバッグで確認したところ、property.name
とは変数名のことでした。そのままの意味ですが。
var text: String by savedStateHandle.saveable { mutableStateOf("textA") }
// このとき、SavedStateHandleには「text」がキーとして保存される
つまり1つのSavedStateHandle内で変数名が被ってしまったら、保存に失敗するということです。
saveableのコメントでも下記のように記載がありました。(和訳してます)
/**
SavedStateHandleとSaverの相互運用により、カスタムSaverでrememberSaveableを介して保存されるステートホルダーもSavedStateHandleで保存できるようになります。
キーは、このデリゲートが作成するために使用されるプロパティの名前として自動的に取得されます。
デリゲートされたMutableStateは、自動キーでSavedStateHandleから値を保存または復元する唯一の方法でなければなりません。
別の SavedStateHandle メソッドで同じキーを再度使用することはサポートされていません。
このオーバーロードを使用すると、rememberSaveableと同様に、変更可能な状態に委譲できます:
**/
1つのSavedStateHandle内で変数名が被ってしまう、そのような状況はありえるのでしょうか?
例えば1画面上にタブを表示していて、それぞれのタブの画面でViewModelを使用している場合、それらの中で同一のSavedStateHandleが使われてしまい、変数名が被って爆死...という状況まで想像した私は、さっそく実験してみることにしました。
実験
@Composable
fun RememberA(viewModel: RememberSaveableTestViewModel = viewModel()) {
Column {
Text(text = viewModel.text)
RememberB()
}
}
@Composable
private fun RememberB(viewModel: RememberSaveableTest2ViewModel = viewModel()) {
Text(text = viewModel.text)
}
@HiltViewModel
class RememberSaveableTestViewModel @Inject constructor(
savedStateHandle: SavedStateHandle
): ViewModel() {
@OptIn(SavedStateHandleSaveableApi::class)
var text: String by savedStateHandle.saveable { mutableStateOf("textA") }
private set
}
// 上記の「textA」を「textB」にしたバージョンのRememberSaveableTest2ViewModel.ktを作成
これでビルドしたところ、画面上には「textA」「textB」が縦に並んで表示されました。
ここで開発者オプションの「アクティビティを保持しない」設定をONにしてみます。ドキドキ...。
その結果、クラッシュすることもなく「textA」「textB」が表示され続けました。
なぜなのか?
結果をまとめると、同一画面上の複数のViewModelで同じ変数名をsaveableしても問題ないということです。
問題ない理由の可能性としては
- 実は変数名をキーにしていない
- 変数名が被ったとしても何とかしてくれている
- そもそもSavedStateHandleのインスタンスが別
- 今回の実験の方法が間違っている
- 実は問題が起きている
などが考えられます。
そこでデバッグした結果、「SavedStateHandleのインスタンスが別」のため問題が発生していないことが分かりました。
そもそもなぜ同一画面上ではSavedStateHandleが一緒かもしれない、と思ったかというと NavBackStackEntry が同一画面上では一緒になる(ここ不正確です。誤りがあったらすみません)ため、そのイメージに引っ張られていました。
1つのViewModelクラス上では同じ変数名にできないため、下記のように別の方法でキーを指定するなどしなければ問題なさそうです。
var text: String by savedStateHandle.saveable { mutableStateOf("textA") }
private set
val text2: String? = savedStateHandle["text"]
最後に
自身のSavedStateHandleへの理解が浅いあまり、余計な心配をしてしまいました。
saveableは試験運用版のため変更の可能性はあるのですが、直近は安心して使いたいと思います。