UI 状態の保存と復元は、状態をホイスティングする場所と必要なロジックに応じて、さまざまな API を使用して行うことできます。どのアプリも、これを最適な形で実現するために API を組み合わせて使用しています。
アクティビティやプロセスの再作成が原因で、Android アプリの UI 状態が失われる可能性があります。このように状態が失われるのは、以下の事象が原因です。
- 構成の変更。構成の変更が手動で対応されない限り、アクティビティは破棄され、再作成されます。
- システムの主導によるプロセスの終了。アプリがバックグラウンドにあると、他のプロセスが使用できるようにリソース(メモリなど)が解放されます。
こういった事象の後に状態を存続させることは、好ましいユーザー エクスペリエンスの実現に不可欠です。どの状態を存続させるのが適切かは、アプリの個々のユーザーフローによって違ってきます。ベスト プラクティスとしては、少なくともユーザー入力とナビゲーション関連の状態は存続させることをおすすめします。たとえば、リストのスクロール位置、ユーザーが詳細を確認したいと思うアイテムの ID、選択している途中のユーザー設定、テキスト フィールドへの入力などです。
このページでは、UI 状態の保存に使用できる API の概要を、状態がホイスティングされる場所と、それを必要とするロジックごとにまとめています。
UI ロジック
UI で状態がホイスティングされる場合、コンポーズ可能な関数と、Composition をスコープとする通常の状態ホルダークラスのどちらでも、rememberSaveable
を使用するとアクティビティとプロセスの再作成前後で状態を保持できます。
次のスニペットでは、rememberSaveable
を使用して 1 つのブール値の UI 要素の状態を保存しています。
@Composable fun ChatBubble( message: Message ) { var showDetails by rememberSaveable { mutableStateOf(false) } ClickableText( text = AnnotatedString(message.content), onClick = { showDetails = !showDetails } ) if (showDetails) { Text(message.timestamp) } }
showDetails
は、チャットのふきだしが開いているか閉じているかを示すブール値の変数です。
rememberSaveable
が、保存済みインスタンス状態のメカニズムにより、UI 要素の状態を Bundle
に保存します。
プリミティブ型はバンドルに自動保存できます。データクラスなど、プリミティブではない型に状態を保持する場合は、さまざまな保存メカニズムを使用できます。たとえば、Parcelize
アノテーションの使用、listSaver
や mapSaver
のような Compose API の使用、Compose ランタイムを拡張するカスタム セーバー クラス Saver
クラスの実装などです。これらのメソッドの詳細については、状態を保存する方法をご覧ください。
次のスニペットでは、rememberLazyListState
Compose API で rememberSaveable
を使用して、LazyColumn
または LazyRow
のスクロール状態から構成される LazyListState
を保存しています。ここでは、スクロール状態の保存と復元が可能なカスタム セーバーである LazyListState.Saver
を使用します。アクティビティまたはプロセスの再作成後(デバイスの向きの変更などの構成変更の後)、スクロール状態が維持されます。
@Composable fun rememberLazyListState( initialFirstVisibleItemIndex: Int = 0, initialFirstVisibleItemScrollOffset: Int = 0 ): LazyListState { return rememberSaveable(saver = LazyListState.Saver) { LazyListState( initialFirstVisibleItemIndex, initialFirstVisibleItemScrollOffset ) } }
ベスト プラクティス
rememberSaveable
が Bundle
を使用して UI 状態を保存し、それがアクティビティでの onSaveInstanceState()
の呼び出しなど、そこに書き込みを行う他の API によって共有されます。ただし、この Bundle
のサイズには制限があり、大きなオブジェクトを保存すると実行時に TransactionTooLarge
例外が発生する可能性があります。これは特に、同じ Bundle
がアプリ全体で使用されている単一 Activity
アプリで問題になります。
この種のクラッシュを回避するために、サイズの大きい複雑なオブジェクトやオブジェクト リストをバンドルに保存しないでください。
代わりに、必要最小限の状態(ID やキーなど)を保存し、それらを使用して、より複雑な UI 状態の復元を他のメカニズム(永続ストレージなど)に委譲してください。
どの設計を選択するかは、アプリの具体的なユースケースと、ユーザーが期待する動作によって異なります。
状態の復元を検証する
rememberSaveable
で Compose 要素に保存された状態が、アクティビティまたはプロセスを再作成したときに正しく復元されることを検証できます。StateRestorationTester
など、これを行う専用の API があります。詳しくは、こちらのテストをご覧ください。
ビジネス ロジック
ビジネス ロジックに必要であることから UI 要素の状態を ViewModel
にホイスティングする場合は、ViewModel
の API を使用できます。
Android アプリで ViewModel
を使用する主なメリットの一つは、構成変更をコストなしで処理できることです。構成変更があり、アクティビティが破棄されて再作成されるとき、ViewModel
にホイスティングされた UI 状態はメモリに保持されます。再作成後は、古い ViewModel
インスタンスが新しいアクティビティ インスタンスにアタッチされます。
なお、ViewModel
インスタンスは、システムの主導によるプロセスの終了後まで存続しません。この状況で UI 状態を存続させるには、SavedStateHandle
API を含む ViewModel の保存済み状態のモジュールを使用してください。
ベスト プラクティス
SavedStateHandle
も UI 状態の保存に Bundle
メカニズムを使用するため、これはシンプルな UI 要素の状態を保存する場合にのみ使用する必要があります。
ビジネスルールを適用し、UI 以外のアプリのレイヤにアクセスすることで生成される画面 UI 状態は、複雑さとサイズに関する問題があるため、SavedStateHandle
に保存しないでください。複雑なデータや大規模なデータの保存には、ローカル永続ストレージなど、さまざまなメカニズムを使用できます。プロセスの再作成後、SavedStateHandle
に保存されて復元された一時状態(存在する場合)で画面が再作成され、画面の UI 状態がデータレイヤーから再度生成されます。
SavedStateHandle
API
SavedStateHandle
には、UI 要素の状態を保存するためのさまざまな API があります。最も注目すべきなのは次の API です。
Compose State |
saveable() |
---|---|
StateFlow |
getStateFlow() |
Compose State
SavedStateHandle
の saveable
API を使用して UI 要素の状態を MutableState
として読み書きします。そのため、最小限のコード セットアップでアクティビティとプロセスの再作成後まで維持できます。
saveable
API は、組み込みのプリミティブ型をサポートし、rememberSaveable()
と同様に、stateSaver
パラメータを受け取ってカスタム セーバーを使用します。
次のスニペットでは、message
がユーザー入力タイプを TextField
に保存しています。
class ConversationViewModel( savedStateHandle: SavedStateHandle ) : ViewModel() { var message by savedStateHandle.saveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) } private set fun update(newMessage: TextFieldValue) { message = newMessage } /*...*/ } val viewModel = ConversationViewModel(SavedStateHandle()) @Composable fun UserInput(/*...*/) { TextField( value = viewModel.message, onValueChange = { viewModel.update(it) } ) }
saveable
API の使用方法については、SavedStateHandle
のドキュメントをご覧ください。
StateFlow
getStateFlow()
を使用して UI 要素の状態を保存し、それを SavedStateHandle
からフローとして使用します。StateFlow
は読み取り専用です。この API ではフローを置き換えて新しい値を出力させるためにキーを指定する必要があります。設定したキーを使用すると、StateFlow
を取得して最新の値を収集できます。
次のスニペットで、savedFilterType
はチャットアプリのチャット チャネルのリストに適用されるフィルタタイプを格納する StateFlow
変数です。
private const val CHANNEL_FILTER_SAVED_STATE_KEY = "ChannelFilterKey" class ChannelViewModel( channelsRepository: ChannelsRepository, private val savedStateHandle: SavedStateHandle ) : ViewModel() { private val savedFilterType: StateFlow<ChannelsFilterType> = savedStateHandle.getStateFlow( key = CHANNEL_FILTER_SAVED_STATE_KEY, initialValue = ChannelsFilterType.ALL_CHANNELS ) private val filteredChannels: Flow<List<Channel>> = combine(channelsRepository.getAll(), savedFilterType) { channels, type -> filter(channels, type) }.onStart { emit(emptyList()) } fun setFiltering(requestType: ChannelsFilterType) { savedStateHandle[CHANNEL_FILTER_SAVED_STATE_KEY] = requestType } /*...*/ } enum class ChannelsFilterType { ALL_CHANNELS, RECENT_CHANNELS, ARCHIVED_CHANNELS }
ユーザーが新しいフィルタタイプを選択するたびに、setFiltering
が呼び出されます。これにより、新しい値がキー _CHANNEL_FILTER_SAVED_STATE_KEY_
で SavedStateHandle
に保存されます。savedFilterType
は、キーに対して保存されている最新の値を出力するフローです。filteredChannels
は、チャネル フィルタリングを行うためにフローに対してサブスクライブされています。
getStateFlow()
API について詳しくは、SavedStateHandle
のドキュメントをご覧ください。
まとめ
次の表は、このセクションで説明されている API と、それぞれをどんな場合に使用して UI 状態を保存するのがよいかをまとめたものです。
イベント | UI ロジック | ViewModel でのビジネス ロジック |
---|---|---|
構成の変更 | rememberSaveable |
自動 |
システムの主導によるプロセスの終了 | rememberSaveable |
SavedStateHandle |
使用すべき API は、状態が保持される場所と、それに必要なロジックによって異なります。UI ロジックで使用される状態には、rememberSaveable
を使用します。ビジネス ロジックで使用される状態については、ViewModel
に保持する場合に SavedStateHandle
を使用して保存します。
バンドル API(rememberSaveable
と SavedStateHandle
)は、少量の UI 状態を保存する目的で使用する必要があります。このデータは、他の保存メカニズムを併用して UI を前の状態に戻すために必要な最小限のものです。たとえば、ユーザーが閲覧しているプロファイルの ID をバンドルに保存した場合、プロファイルの詳細などの大量のデータはデータレイヤーから取得できます。
UI 状態のさまざまな保存方法について詳しくは、一般的な UI 状態の保存に関するドキュメントとアーキテクチャ ガイドのデータレイヤーのページをご覧ください。
あなたへのおすすめ
- 注: JavaScript がオフになっている場合はリンクテキストが表示されます
- 状態をホイスティングする場所
- 状態と Jetpack Compose
- リストとグリッド