Zapisz stan interfejsu w oknie tworzenia

W zależności od tego, gdzie jest przenoszony stan i od wymaganych zasad logicznych, możesz przechowywać i przywracać stan interfejsu za pomocą różnych interfejsów API. Aby to osiągnąć, każda aplikacja korzysta z kombinacji interfejsów API.

Każda aplikacja na Androida może stracić stan interfejsu z powodu aktywności lub odtworzenia procesu. Utrata stanu może być spowodowana tymi przyczynami:

Zachowanie stanu po wystąpieniu tych zdarzeń jest niezbędne dla wygody użytkowników. Wybór stanu do zachowania zależy od procesów podejmowanych przez unikalnych użytkowników aplikacji. Sprawdzoną metodą jest zachowanie przynajmniej stanu danych wejściowych użytkownika i stanu związanych z nawigacją. Może to być na przykład pozycja przewijania listy, identyfikator elementu, o którym użytkownik chce uzyskać więcej informacji, trwający wybór preferencji użytkownika lub dane wprowadzone w polach tekstowych.

Na tej stronie znajdziesz podsumowanie interfejsów API dostępnych do przechowywania stanu interfejsu użytkownika w zależności od tego, do którego stanu zostanie przeniesiony stan i od logiki, która go wymaga.

Logika interfejsu

Jeśli Twój stan jest pobierany w interfejsie (w funkcjach kompozycyjnych lub klasach posiadaczy zwykłych stanów do kompozycji, możesz użyć rememberSaveable do zachowywania stanu w ramach wszystkich działań i odtwarzania procesów).

W tym fragmencie kodu rememberSaveable służy do przechowywania stanu pojedynczego elementu interfejsu z wartością logiczną:

@Composable
fun ChatBubble(
    message: Message
) {
    var showDetails by rememberSaveable { mutableStateOf(false) }

    ClickableText(
        text = AnnotatedString(message.content),
        onClick = { showDetails = !showDetails }
    )

    if (showDetails) {
        Text(message.timestamp)
    }
}

Rysunek 1. Dymek wiadomości czatu rozwija się i zwija po kliknięciu.

showDetails to zmienna logiczna, która przechowuje, gdy dymek czatu jest zwinięty lub rozwinięty.

rememberSaveable przechowuje stan elementu interfejsu w Bundle za pomocą mechanizmu zapisanego stanu instancji.

Może automatycznie zapisywać w pakiecie typy podstawowe. Jeśli stan jest utrzymywany w typie, który nie jest prymitywny, np. klasa danych, możesz użyć innych mechanizmów przechowywania, np. za pomocą adnotacji Parcelize lub interfejsów API tworzenia wiadomości, takich jak listSaver i mapSaver, lub zaimplementowanych niestandardowych klas oszczędzania rozszerzających klasę środowiska wykonawczego Saver. Więcej informacji o tych metodach znajdziesz w dokumentacji sposobów przechowywania informacji o stanie.

W poniższym fragmencie interfejs rememberLazyListState Compose API przechowuje zbiór LazyListState, który składa się ze stanu przewijania LazyColumn lub LazyRow z wykorzystaniem rememberSaveable. Używa ona LazyListState.Saver – niestandardowego wygaszacza, który może przechowywać i przywracać stan przewijania. Po odtworzeniu działania lub procesu (np. po zmianie konfiguracji, np. zmianie orientacji urządzenia) stan przewijania jest zachowywany.

@Composable
fun rememberLazyListState(
    initialFirstVisibleItemIndex: Int = 0,
    initialFirstVisibleItemScrollOffset: Int = 0
): LazyListState {
    return rememberSaveable(saver = LazyListState.Saver) {
        LazyListState(
            initialFirstVisibleItemIndex, initialFirstVisibleItemScrollOffset
        )
    }
}

Sprawdzona metoda

rememberSaveable używa Bundle do przechowywania stanu interfejsu, który jest udostępniany przez inne interfejsy API, które również w nim zapisują, np. wywołania onSaveInstanceState() w Twojej aktywności. Rozmiar tego obiektu Bundle jest jednak ograniczony, a przechowywanie dużych obiektów może spowodować utworzenie wyjątków w czasie działania (TransactionTooLarge). Może to być szczególnie problematyczne w przypadku pojedynczych aplikacji typu Activity, w których w całej aplikacji używa się tego samego parametru Bundle.

Aby uniknąć awarii tego typu, nie przechowuj w pakiecie dużych złożonych obiektów ani list.

Zamiast tego zapisz minimalny wymagany stan, np. identyfikatory lub klucze, i używaj ich do delegowania przywracania bardziej złożonego stanu interfejsu do innych mechanizmów, np. do pamięci trwałej.

Wybrane opcje projektowe zależą od konkretnych przypadków użycia aplikacji i oczekiwania użytkowników.

Sprawdź przywrócenie stanu

Możesz sprawdzić, czy stan zapisany w rememberSaveable w elementach tworzenia wiadomości jest prawidłowo przywracany po utworzeniu działania lub procesu. Istnieją specjalne interfejsy API, które pozwalają to zrobić, np. StateRestorationTester. Więcej informacji znajdziesz w dokumentacji testowania.

Logika biznesowa

Jeśli stan elementu interfejsu jest przenoszony do ViewModel, ponieważ jest to wymagane przez logikę biznesową, możesz używać interfejsów API ViewModel.

Jedną z głównych zalet korzystania z ViewModel w aplikacji na Androida jest to, że umożliwia on bezpłatne wprowadzanie zmian w konfiguracji. W przypadku zmiany konfiguracji, a następnie zniszczeniu działania i jego odtworzeniu, stan interfejsu przeniesiony do elementu ViewModel jest zachowywany w pamięci. Po odtworzeniu stara instancja ViewModel jest dołączona do nowej instancji aktywności.

Instancja ViewModel nie przetrwa jednak śmierci procesu inicjowanego przez system. Aby stan interfejsu użytkownika przetrwał ten okres, użyj modułu Zapisanego stanu dla obiektu ViewModel, który zawiera interfejs API SavedStateHandle.

Sprawdzona metoda

SavedStateHandle wykorzystuje również mechanizm Bundle do przechowywania stanu interfejsu, więc należy go używać tylko do przechowywania prostego stanu elementu interfejsu.

Stan interfejsu ekranu, który powstaje przez stosowanie reguł biznesowych i uzyskiwanie dostępu do warstw aplikacji innych niż UI, nie powinien być przechowywany w SavedStateHandle ze względu na jego potencjalną złożoność i rozmiar. Do przechowywania złożonych lub dużych danych możesz używać różnych mechanizmów, np. lokalnej pamięci trwałej. Po odtworzeniu procesu ekran jest odtworzony z przywróconym stanem przejściowym, który został zapisany w funkcji SavedStateHandle (jeśli występuje), a stan interfejsu ekranu jest ponownie generowany z warstwy danych.

SavedStateHandle interfejsów API

SavedStateHandle ma różne interfejsy API do zapisywania stanu elementów interfejsu, a w szczególności:

Utwórz State saveable()
StateFlow getStateFlow()

Utwórz State

Używaj interfejsu saveable API w SavedStateHandle, aby odczytywać i zapisywać stan elementu interfejsu o stanie MutableState, aby zachować aktywność i przetwarzać odtwarzanie przy minimalnej konfiguracji kodu.

Interfejs saveable API obsługuje od razu typy podstawowe i otrzymuje parametr stateSaver służący do korzystania z niestandardowych wygaszaczy (tak jak rememberSaveable()).

W tym fragmencie message zapisuje dane wejściowe użytkownika w elemencie 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) }
    )
}

Więcej informacji o korzystaniu z interfejsu API saveable znajdziesz w dokumentacji SavedStateHandle.

StateFlow

Używaj getStateFlow(), aby przechowywać stan elementu interfejsu i wykorzystywać go jako przepływ z SavedStateHandle. StateFlow jest tylko do odczytu, a interfejs API wymaga określenia klucza, aby można było zastąpić przepływ w celu wyemitowania nowej wartości. Za pomocą skonfigurowanego klucza możesz pobrać StateFlow i pobrać najnowszą wartość.

W tym fragmencie kodu savedFilterType to zmienna StateFlow, która przechowuje typ filtra zastosowany do listy kanałów czatu w aplikacji do obsługi czatu:

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
}

Za każdym razem, gdy użytkownik wybierze nowy typ filtra, wywoływane jest setFiltering. Spowoduje to zapisanie nowej wartości w elemencie SavedStateHandle zapisanej z kluczem _CHANNEL_FILTER_SAVED_STATE_KEY_. savedFilterType to przepływ, który przesyła najnowszą wartość zapisaną w kluczu. filteredChannels subskrybuje przepływ, aby móc filtrować kanały.

Więcej informacji o interfejsie API getStateFlow() znajdziesz w dokumentacji SavedStateHandle.

Podsumowanie

W tabeli poniżej znajdziesz podsumowanie omówionych w tej sekcji interfejsów API i informacje o tym, kiedy przy użyciu każdego z nich zapisać stan interfejsu:

Wydarzenie Logika interfejsu Logika biznesowa w obiekcie ViewModel
Zmiany konfiguracji rememberSaveable Automatyczny
Przerwanie procesu inicjowanego przez system rememberSaveable SavedStateHandle

Interfejs API, którego należy użyć, zależy od tego, gdzie jest utrzymywany stan i jakie wymaga logiki. Jako stan używany w logice UI użyj rememberSaveable. W przypadku stanu używanego w logice biznesowej, jeśli przechowujesz go w polu ViewModel, zapisz go za pomocą atrybutu SavedStateHandle.

Do przechowywania niewielkich ilości stanu interfejsu użytkownika należy używać interfejsów API pakietu (rememberSaveable i SavedStateHandle). Dane te to minimum niezbędne do przywrócenia interfejsu użytkownika do poprzedniego stanu wraz z innymi mechanizmami przechowywania. Jeśli na przykład przechowujesz w pakiecie identyfikator profilu, który oglądał użytkownik, możesz pobierać z warstwy danych bardzo dużo danych, takich jak szczegóły profilu.

Więcej informacji o różnych sposobach zapisywania stanu interfejsu znajdziesz w ogólnej dokumentacji dotyczącej zapisywania stanu interfejsu i na stronie warstwy danych w przewodniku po architekturze.