Zapisz stan interfejsu w oknie tworzenia

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

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

Zachowanie stanu po tych zdarzeniach jest niezbędne do zapewnienia użytkownikom pozytywnych wrażeń. Wybór stanu, który ma zostać zachowany, zależy od unikalnych wzorców przeglądania aplikacji przez użytkowników. Zgodnie ze sprawdzonymi metodami należy zachować co najmniej dane wejściowe użytkownika i stan związany z nawigacją. Przykłady obejmują pozycję przewijania listy, identyfikator elementu, o którym użytkownik chce uzyskać więcej informacji, wybór preferencji użytkownika w toku lub dane wejściowe 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, gdzie stan jest przenoszony i jakiej logiki wymaga.

Logika interfejsu

Jeśli stan jest podniesiony w interfejsie, w funkcjach typu „composable” lub w zwykłych klasach zmiennych stanu o zakresie ograniczonym do kompozycji, możesz użyć rememberSaveable, aby zachować stan podczas odtwarzania aktywności i procesu.

We fragmencie poniżej rememberSaveable służy do przechowywania pojedynczego stanu 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. Dymek wiadomości na czacie rozwija się i zwija po kliknięciu.

showDetails to zmienna logiczna, która określa, 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ć typy podstawowe w pakiecie. Jeśli stan jest przechowywany w typie, który nie jest prymitywny, np. w klasie danych, możesz użyć różnych mechanizmów przechowywania, takich jak adnotacja Parcelize, interfejsy API Compose, takie jak listSaver i mapSaver, lub niestandardowa klasa zapisu rozszerzająca klasę Saver środowiska wykonawczego Compose. Więcej informacji o tych metodach znajdziesz w dokumentacji dotyczącej sposobów przechowywania stanu.

We fragmencie poniżej interfejs Compose API rememberLazyListState przechowuje LazyListState, który składa się ze stanu przewijania LazyColumn lub LazyRow, za pomocą rememberSaveable. Używa on LazyListState.Saver, czyli niestandardowego obiektu zapisującego, który 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 udostępniany innym interfejsom API, które również do niego zapisują, np. wywołania onSaveInstanceState() w aktywności. Rozmiar Bundle jest jednak ograniczony, a przechowywanie dużych obiektów może prowadzić do wyjątków TransactionTooLarge w czasie działania. Może to być szczególnie problematyczne w aplikacjach z jednym elementem Activity, w których ten sam element Bundle jest używany w całej aplikacji.

Aby uniknąć tego typu awarii, nie przechowuj 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 przekazywania przywracania bardziej złożonego stanu interfejsu innym mechanizmom, takim jak pamięć trwała.

Te wybory projektowe zależą od konkretnych przypadków użycia aplikacji i tego, jak użytkownicy oczekują, że będzie ona działać.

Weryfikowanie przywracania stanu

Możesz sprawdzić, czy stan zapisany za pomocą rememberSaveable w elementach Compose jest prawidłowo przywracany po ponownym utworzeniu aktywności lub procesu. Służą do tego określone interfejsy API, takie jak StateRestorationTester. Więcej informacji znajdziesz w dokumentacji dotyczącej testowania.

Logika biznesowa

Jeśli stan elementu interfejsu jest przeniesiony do ViewModel, ponieważ jest wymagany przez logikę biznesową, możesz użyć 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ępuje zmiana konfiguracji, a aktywność jest niszczona i tworzona ponownie, stan interfejsu użytkownika przeniesiony do ViewModel jest przechowywany w pamięci. Po ponownym utworzeniu stara instancja ViewModel jest dołączana do nowej instancji aktywności.

Jednak instancja ViewModel nie przetrwa śmierci procesu zainicjowanej przez system. Aby stan interfejsu nie został utracony, użyj modułu Saved State dla ViewModel, który zawiera interfejs API SavedStateHandle.

Sprawdzona metoda

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

Stan interfejsu ekranu, który jest generowany przez stosowanie reguł biznesowych i uzyskiwanie dostępu do warstw aplikacji innych niż interfejs, nie powinien być przechowywany w SavedStateHandle ze względu na potencjalną złożoność i rozmiar. Do przechowywania złożonych lub dużych danych możesz używać różnych mechanizmów, takich jak lokalna pamięć trwała. Po ponownym utworzeniu procesu ekran jest odtwarzany z przywróconym stanem przejściowym, który został zapisany w SavedStateHandle (jeśli istnieje), a stan interfejsu ekranu jest ponownie generowany z warstwy danych.

Interfejsy API SavedStateHandle

SavedStateHandle ma różne interfejsy API do przechowywania stanu elementów interfejsu, w tym:

Utwórz State saveable()
StateFlow getStateFlow()

Utwórz: State

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

Interfejs API saveable obsługuje typy podstawowe i otrzymuje parametr stateSaver, aby używać niestandardowych funkcji zapisu, podobnie jak rememberSaveable().

We fragmencie poniżej message przechowuje dane wpisane przez użytkownika 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 korzystaniu z interfejsu API saveable znajdziesz w dokumentacji SavedStateHandle.

StateFlow

Użyj getStateFlow(), aby przechowywać stan elementu interfejsu i wykorzystywać go jako przepływ z SavedStateHandle. Właściwość StateFlow jest tylko do odczytu, a interfejs API wymaga podania klucza, aby można było zastąpić przepływ i wygenerować nową wartość. Za pomocą skonfigurowanego klucza możesz pobrać StateFlow i uzyskać najnowszą wartość.

We fragmencie poniżej 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ływana jest funkcja setFiltering. Spowoduje to zapisanie nowej wartości w SavedStateHandle przechowywanej z kluczem _CHANNEL_FILTER_SAVED_STATE_KEY_. savedFilterType to przepływ, który emituje najnowszą wartość zapisaną w kluczu. filteredChannels subskrybuje przepływ, aby filtrować kanały.

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

Podsumowanie

W tabeli poniżej znajdziesz podsumowanie interfejsów API opisanych w tej sekcji oraz informacje o tym, kiedy używać poszczególnych interfejsów do zapisywania stanu interfejsu:

Zdarzenie 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 jakiej logiki wymaga. W przypadku stanu używanego w logice interfejsu użyj rememberSaveable. Jeśli stan używany w logice biznesowej jest przechowywany w ViewModel, zapisz go za pomocą SavedStateHandle.

Do przechowywania niewielkich ilości danych o stanie interfejsu należy używać interfejsów API pakietów (rememberSaveable i SavedStateHandle). Te dane to minimum niezbędne do przywrócenia interfejsu do poprzedniego stanu, wraz z innymi mechanizmami przechowywania. Jeśli na przykład przechowujesz w pakiecie 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 dotyczącej zapisywania stanu interfejsu oraz na stronie warstwy danych w przewodniku po architekturze.