Omówienie ViewModel   części Androida Jetpack.

Wypróbuj Kotlin Multiplatform
Kotlin Multiplatform umożliwia udostępnianie logiki biznesowej innym platformom. Dowiedz się, jak skonfigurować i używać ViewModel w KMP

Klasa ViewModel to kontener stanu logiki biznesowej lub stanu na poziomie ekranu holder. Udostępnia stan interfejsowi i hermetyzuje powiązaną logikę biznesową. Jej główną zaletą jest to, że buforuje stan i zachowuje go podczas zmian konfiguracji. Oznacza to, że interfejs nie musi ponownie pobierać danych podczas przechodzenia między aktywnościami ani po zmianach konfiguracji, np. po obróceniu ekranu.

Więcej informacji o kontenerach stanu znajdziesz w przewodniku po kontenerach stanu. Podobnie więcej informacji o warstwie interfejsu znajdziesz w przewodniku po warstwie interfejsu.

Korzyści ViewModel

Alternatywą dla ViewModel jest zwykła klasa, która zawiera dane wyświetlane w interfejsie. Może to stanowić problem podczas przechodzenia między aktywnościami lub miejscami docelowymi nawigacji. Jeśli nie zapiszesz danych za pomocą mechanizmu zapisanego stanu instancji, zostaną one zniszczone. ViewModel udostępnia wygodny interfejs API do utrwalania danych, który rozwiązuje ten problem.

W przypadku kontenerów stanu Compose oferuje funkcje retain, które umożliwiają zwykłym klasom przetrwanie zmian konfiguracji bez pełnej infrastruktury ViewModel. Chociaż oba mechanizmy pomagają w zachowaniu stanu, generalnie bezpieczniej jest udostępnić ViewModel instancji zachowanej niż odwrotnie, ponieważ ich cykle życia i zachowania związane z czyszczeniem różnią się.

Główne korzyści klasy ViewModel są zasadniczo 2:

  • Umożliwia zachowanie stanu interfejsu.
  • Zapewnia dostęp do logiki biznesowej.

Trwałość

ViewModel umożliwia trwałość zarówno dzięki stanowi, który zawiera, jak i operacjom, które wywołuje. Dzięki buforowaniu nie musisz ponownie pobierać danych po typowych zmianach konfiguracji, takich jak obrócenie ekranu.

Zakres

Podczas tworzenia instancji ViewModel przekazujesz jej obiekt, który implementuje interfejs ViewModelStoreOwner. Może to być miejsce docelowe nawigacji, wykres nawigacji, aktywność lub dowolny inny typ, który implementuje ten interfejs. Możesz też bezpośrednio określić zakres ViewModel w komponencie za pomocą interfejsu API rememberViewModelStoreOwner. ViewModel jest wtedy ograniczony do cyklu życia ViewModelStoreOwner. Pozostaje w pamięci, dopóki jego ViewModelStoreOwner nie zniknie na stałe (np. gdy właściciel komponentu opuści kompozycję).

Zakres klas jest bezpośrednio lub pośrednio podklasą interfejsu ViewModelStoreOwner. Bezpośrednie podklasy to ComponentActivity i NavBackStackEntry. Pełną listę pośrednich podklas znajdziesz w dokumentacji ViewModelStoreOwner reference. Aby ograniczyć zakres ViewModels do poszczególnych elementów w LazyList lub Pager, użyj rememberViewModelStoreProvider(), aby przenieść zarządzanie właścicielem do elementu nadrzędnego.

Gdy aktywność hosta ulega zmianie konfiguracji, praca asynchroniczna jest kontynuowana w ViewModel, niezależnie od tego, czy jest ona ograniczona do aktywności, czy do konkretnego komponentu. Jest to klucz do trwałości.

Więcej informacji znajdziesz w sekcji Cykl życia ViewModel, interfejsach API zakresu ViewModel, i przewodniku po przenoszeniu stanu w Jetpack Compose.

SavedStateHandle

SavedStateHandle umożliwia zachowanie danych nie tylko podczas zmian konfiguracji, ale też po śmierci procesu. Oznacza to, że możesz zachować stan interfejsu nawet wtedy, gdy użytkownik zamknie aplikację i otworzy ją później.

Więcej informacji o zapisywaniu stanu interfejsu, zobacz Zapisywanie stanu interfejsu w Compose.

Dostęp do logiki biznesowej

Chociaż zdecydowana większość logiki biznesowej znajduje się w warstwie danych, warstwa interfejsu może również zawierać logikę biznesową. Może się tak zdarzyć, gdy łączysz dane z wielu repozytoriów, aby utworzyć stan interfejsu ekranu, lub gdy określony typ danych nie wymaga warstwy danych.

ViewModel to odpowiednie miejsce do obsługi logiki biznesowej w warstwie interfejsu. ViewModel odpowiada też za obsługę zdarzeń i przekazywanie ich do innych warstw hierarchii, gdy trzeba zastosować logikę biznesową, aby zmodyfikować dane aplikacji.

Implementowanie ViewModel

Poniżej znajdziesz przykład implementacji ViewModel dla ekranu, który umożliwia użytkownikowi rzucanie kostką.

data class DiceUiState(
    val firstDieValue: Int? = null,
    val secondDieValue: Int? = null,
    val numberOfRolls: Int = 0,
)

class DiceRollViewModel : ViewModel() {

    // Expose screen UI state
    private val _uiState = MutableStateFlow(DiceUiState())
    val uiState: StateFlow<DiceUiState> = _uiState.asStateFlow()

    // Handle business logic
    fun rollDice() {
        _uiState.update { currentState ->
            currentState.copy(
                firstDieValue = Random.nextInt(from = 1, until = 7),
                secondDieValue = Random.nextInt(from = 1, until = 7),
                numberOfRolls = currentState.numberOfRolls + 1,
            )
        }
    }
}

Następnie możesz uzyskać dostęp do ViewModel z komponentu na poziomie ekranu w ten sposób:

import androidx.lifecycle.viewmodel.compose.viewModel

// Use the 'viewModel()' function from the lifecycle-viewmodel-compose artifact
@Composable
fun DiceRollScreen(
    viewModel: DiceRollViewModel = viewModel()
) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    // Update UI elements
}

Używanie współprogramów z ViewModel

ViewModel obsługuje współprogramy Kotlin. Może zachowywać pracę asynchroniczną w taki sam sposób jak stan interfejsu.

Więcej informacji znajdziesz w artykule Używanie współprogramów Kotlin z komponentami architektury Androida.

Cykl życia ViewModel

Cykl życia ViewModel jest bezpośrednio powiązany z jego zakresem. A ViewModel pozostaje w pamięci, dopóki nie zniknie ViewModelStoreOwner, do którego jest ograniczony disappears. Może się to zdarzyć w tych kontekstach:

  • W przypadku aktywności – gdy się zakończy.
  • W przypadku wpisu nawigacji – gdy zostanie usunięty ze stosu wstecznego.
  • W przypadku komponentu – gdy opuści kompozycję. Za pomocą rememberViewModelStoreOwner możesz ograniczyć zakres ViewModel bezpośrednio do dowolnej części interfejsu (np. Pager lub LazyList).

Dzięki temu ViewModels są doskonałym rozwiązaniem do przechowywania danych, które przetrwają zmiany konfiguracji.

Rysunek 1 przedstawia różne stany cyklu życia aktywności, gdy ulega ona obróceniu, a następnie zostaje zakończona. Ilustracja pokazuje też czas życia ViewModel obok powiązanego cyklu życia działania. Ten konkretny diagram przedstawia stany aktywności.

Ilustracja przedstawiająca cykl życia obiektu ViewModel w miarę zmiany stanu działania.
Rysunek 1. Stany cyklu życia aktywności i ViewModel.

Zwykle prosisz o ViewModel przy pierwszym wywołaniu przez system metody onCreate() obiektu aktywności. System może wywołać onCreate() kilka razy w trakcie istnienia aktywności, np. gdy ekran urządzenia zostanie obrócony. ViewModel istnieje od momentu pierwszego żądania ViewModel do momentu zakończenia i zniszczenia aktywności.

Czyszczenie zależności ViewModel

ViewModel wywołuje metodę onCleared, gdy ViewModelStoreOwner niszczy ją w trakcie cyklu życia. Umożliwia to zwalnianie miejsca na wszelkie prace lub zależności, które są zgodne z cyklem życia ViewModel.

Poniższy przykład pokazuje alternatywę dla viewModelScope. viewModelScope to wbudowany CoroutineScope, który automatycznie śledzi cykl życia ViewModel. ViewModel używa go do wywoływania operacji związanych z działalnością. Jeśli chcesz używać niestandardowego zakresu zamiast viewModelScope dla łatwiejszego testowania, ViewModel może otrzymać CoroutineScope jako zależność w swoim konstruktorze. Gdy ViewModelStoreOwner wyczyści ViewModel na końcu jego cyklu życia, ViewModel anuluje też CoroutineScope.

class MyViewModel(
    private val coroutineScope: CoroutineScope =
        CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
) : ViewModel() {

    // Other ViewModel logic ...

    override fun onCleared() {
        coroutineScope.cancel()
    }
}

Od wersji 2.5 cyklu życia możesz przekazać do konstruktora ViewModel co najmniej 1 obiekt Closeable, który zostanie automatycznie zamknięty po wyczyszczeniu instancji ViewModel.

class CloseableCoroutineScope(
    context: CoroutineContext = SupervisorJob() + Dispatchers.Main.immediate
) : Closeable, CoroutineScope {
    override val coroutineContext: CoroutineContext = context
    override fun close() {
        coroutineContext.cancel()
   }
}

class MyViewModel(
    private val coroutineScope: CoroutineScope = CloseableCoroutineScope()
) : ViewModel(coroutineScope) {
    // Other ViewModel logic ...
}

Sprawdzone metody

Oto kilka kluczowych sprawdzonych metod, których należy przestrzegać podczas implementowania ViewModel:

  • Ze względu na ich zakres używaj ViewModels jako szczegółów implementacji zmiennej stanu na poziomie ekranu. Nie używaj ich jako kontenerów stanu komponentów interfejsu wielokrotnego użytku, takich jak grupy chipów czy formularze. W przeciwnym razie w różnych zastosowaniach tego samego komponentu interfejsu w ramach tego samego ViewModelStoreOwner uzyskasz tę samą instancję ViewModel, chyba że użyjesz jawnego klucza modelu widoku dla każdego chipa.
  • ViewModels nie powinny znać szczegółów implementacji interfejsu. Nazwy metod udostępnianych przez interfejs API ViewModel i pól stanu interfejsu powinny być jak najbardziej ogólne. Dzięki temu ViewModel może obsługiwać każdy typ interfejsu: telefon komórkowy, urządzenie składane, tablet, a nawet Chromebooka.
  • Ponieważ ViewModels mogą potencjalnie żyć dłużej niż ViewModelStoreOwner, nie powinny zawierać żadnych odwołań do interfejsów API związanych z cyklem życia, takich jak Context czy Resources, aby zapobiec wyciekom pamięci.
  • Nie przekazuj ViewModels do innych klas, funkcji ani komponentów interfejsu. Ponieważ platforma nimi zarządza, powinny one znajdować się jak najbliżej niej – blisko Activity, funkcji typu „composable” na poziomie ekranu lub miejsca docelowego Navigation. Zapobiega to uzyskiwaniu przez komponenty niższego poziomu dostępu do większej ilości danych i logiki niż potrzebują.

Dodatkowe informacje

W miarę jak dane stają się bardziej złożone, możesz utworzyć osobną klasę tylko do wczytywania danych. Celem ViewModel jest hermetyzacja danych dla kontrolera interfejsu, aby dane przetrwały zmiany konfiguracji. Informacje o tym, jak wczytywać, zachowywać i zarządzać danymi podczas zmian konfiguracji, znajdziesz w artykule Zapisywanie stanów interfejsu.

Przewodnik po architekturze aplikacji na Androida sugeruje utworzenie klasy repozytorium do obsługi tych funkcji.

Dodatkowe materiały

Więcej informacji o klasie ViewModel znajdziesz w tych materiałach.

Dokumentacja

Wyświetlanie treści

Przykłady