Zapisz stan interfejsu w oknie tworzenia

W zależności od tego, gdzie jest przenoszony stan i jaka jest wymagana logika, możesz używać różnych interfejsów API do przechowywania i przywracania stanu interfejsu. Każda aplikacja używa kombinacji interfejsów API, aby jak najlepiej to osiągnąć.

Każda aplikacja na Androida może utracić swój stan interfejsu z powodu ponownego utworzenia aktywności lub procesu. Utrata stanu może nastąpić z powodu tych zdarzeń:

Zachowanie stanu po tych zdarzeniach jest niezbędne do zapewnienia pozytywnego wrażenia użytkownika. Wybór stanu do utrwalenia zależy od unikalnych ścieżek użytkownika w aplikacji. Zgodnie ze sprawdzoną metodą należy zachować co najmniej dane wejściowe użytkownika i stan związany z nawigacją. Przykłady to pozycja przewijania listy, identyfikator elementu, o którym użytkownik chce dowiedzieć się więcej, trwający wybór preferencji użytkownika lub dane wejściowe w polach tekstowych.

Na tej stronie znajdziesz podsumowanie interfejsów API dostępnych do przechowywania stanu interfejsu w zależności od tego, gdzie jest przenoszony stan i jaka logika jest wymagana.

Logika interfejsu

Jeśli stan jest przenoszony w interfejsie, w funkcjach kompozycyjnych lub w zwykłych klasach przechowujących stan w zakresie kompozycji, możesz użyć rememberSaveable, aby zachować stan podczas ponownego tworzenia aktywności i procesu.

W tym fragmencie kodu funkcja rememberSaveable służy do przechowywania stanu pojedynczego elementu interfejsu typu boolean:

@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. Po kliknięciu dymek wiadomości na czacie rozszerza się i zwija.

showDetails to zmienna typu boolean, która przechowuje informacje o tym, czy dymek czatu jest zwinięty czy rozwinięty.

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

Może automatycznie przechowywać w pakiecie typy podstawowe. Jeśli stan jest przechowywany w typie, który nie jest podstawowy, np. w klasie danych, możesz użyć różnych mechanizmów przechowywania, takich jak adnotacja Parcelize, interfejsy Compose API, np. listSaver i mapSaver, lub zaimplementować niestandardową klasę zapisującą, która rozszerza klasę Saver środowiska wykonawczego Compose. Więcej informacji o tych metodach znajdziesz w dokumentacji Sposoby przechowywania stanu.

W tym fragmencie kodu rememberLazyListState Compose API przechowuje LazyListState, który składa się ze stanu przewijania elementu LazyColumn lub LazyRow, za pomocą funkcji rememberSaveable. Używa on funkcji LazyListState.Saver, czyli niestandardowej funkcji zapisującej, która może przechowywać i przywracać stan przewijania. Po ponownym utworzeniu aktywności lub procesu (np. po zmianie konfiguracji, takiej jak zmiana 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 współdzielony przez inne interfejsy API, które również zapisują w nim dane, np. wywołania onSaveInstanceState() w aktywności. Rozmiar tego Bundle jest jednak ograniczony, a przechowywanie dużych obiektów może prowadzić do TransactionTooLarge wyjątków w czasie działania. Może to być szczególnie problematyczne w aplikacjach z jedną Activity, w których ten sam Bundle jest używany w całej aplikacji.

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

Zamiast tego przechowuj 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 trwałego przechowywania.

Te decyzje projektowe zależą od konkretnych przypadków użycia w aplikacji i od tego, jak użytkownicy oczekują jej działania.

Sprawdzanie przywracania stanu

Możesz sprawdzić, czy stan przechowywany za pomocą rememberSaveable w elementach Compose jest prawidłowo przywracany po ponownym utworzeniu aktywności lub procesu. Służą do tego konkretne interfejsy API, np. StateRestorationTester. Więcej informacji znajdziesz w dokumentacji Testowanie.

Logika biznesowa

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

Jedną z głównych zalet używania ViewModel w aplikacji na Androida jest to, że bezpłatnie obsługuje zmiany konfiguracji. Gdy nastąpi zmiana konfiguracji, a aktywność zostanie zniszczona i ponownie utworzona, stan interfejsu przeniesiony do ViewModel jest przechowywany w pamięci. Po ponownym utworzeniu stara instancja ViewModel jest dołączana do nowej instancji aktywności.

Instancja ViewModel nie przetrwa jednak zakończenia procesu zainicjowanego przez system. Aby stan interfejsu przetrwał, użyj modułu Saved State for ViewModel, który zawiera interfejs SavedStateHandle API.

Sprawdzona metoda

SavedStateHandle używa też mechanizmu Bundle do przechowywania stanu interfejsu, dlatego należy go używać tylko do przechowywania prostego stanu elementu interfejsu.

Stan interfejsu ekranu, który jest tworzony przez stosowanie reguł biznesowych i dostęp do warstw aplikacji innych niż interfejs, 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. lokalnego trwałego przechowywania. Po ponownym utworzeniu procesu ekran jest ponownie tworzony z przywróconym stanem przejściowym, który był przechowywany w SavedStateHandle (jeśli taki stan istniał), a stan interfejsu ekranu jest ponownie tworzony na podstawie warstwy danych.

Interfejsy API SavedStateHandle

SavedStateHandle ma różne interfejsy API do przechowywania stanu elementu interfejsu, w szczególności:

Compose State saveable()
StateFlow getStateFlow()

Compose State

Użyj interfejsu saveable API SavedStateHandle, aby odczytywać i zapisywać stan elementu interfejsu jako MutableState, dzięki czemu przetrwa on ponowne utworzenie aktywności i procesu przy minimalnej konfiguracji kodu.

Interfejs saveable API obsługuje typy podstawowe od razu po wyjęciu z pudełka i otrzymuje parametr stateSaver, aby używać niestandardowych funkcji zapisujących, tak jak rememberSaveable().

W tym fragmencie kodu message przechowuje dane wejściowe użytkownika wpisane w 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 używaniu interfejsu saveable API znajdziesz w dokumentacji SavedStateHandle.

StateFlow

Użyj getStateFlow(), aby przechowywać stan elementu interfejsu i używać go jako przepływu z SavedStateHandle. The StateFlow is read-only, and the API requires you to specify a key so you can replace the flow to emit a new value. Za pomocą skonfigurowanego klucza możesz pobrać StateFlow i zebrać najnowszą wartość.

W tym fragmencie kodu savedFilterType to zmienna StateFlow, która przechowuje typ filtra zastosowany do listy kanałów czatu w aplikacji do 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ływana jest funkcja setFiltering. Spowoduje to zapisanie nowej wartości w SavedStateHandle przechowywanej pod kluczem _CHANNEL_FILTER_SAVED_STATE_KEY_. savedFilterType to przepływ emitujący najnowszą wartość przechowywaną pod kluczem. filteredChannels jest subskrybowany w przepływie, aby filtrować kanały.

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

Podsumowanie

W tabeli poniżej znajdziesz podsumowanie interfejsów API omówionych w tej sekcji oraz informacje o tym, kiedy należy używać każdego z nich do zapisywania stanu interfejsu:

Wydarzenie Logika interfejsu Logika biznesowa w ViewModel
Zmiany konfiguracji rememberSaveable Automatycznie
Śmierć procesu zainicjowana przez system rememberSaveable SavedStateHandle

Interfejs API, którego należy użyć, zależy od tego, gdzie jest przechowywany stan i jaka jest wymagana logika. W przypadku stanu używanego w logice interfejsu użyj funkcji rememberSaveable. W przypadku stanu używanego w logice biznesowej, jeśli przechowujesz go w ViewModel, zapisz go za pomocą SavedStateHandle.

Do przechowywania niewielkich ilości stanu interfejsu należy używać interfejsów API pakietu (rememberSaveable i SavedStateHandle). Te dane są minimalną ilością informacji niezbędnych do przywrócenia interfejsu do poprzedniego stanu wraz z innymi mechanizmami przechowywania. Jeśli na przykład w pakiecie przechowujesz identyfikator profilu, który użytkownik przeglądał, możesz pobrać z warstwy danych duże ilości danych, np. szczegóły profilu.

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