Informacje o modelu ViewModel Część pakietu Android Jetpack.

Klasa ViewModel jest właścicielem stanu logiki biznesowej lub ekranu. Prezentuje stan w interfejsie użytkownika i obejmuje powiązaną logikę biznesową. Jego główną zaletą jest to, że zapisuje stan w pamięci podręcznej i utrzymuje go po zmianach konfiguracji. Oznacza to, że interfejs użytkownika nie musi ponownie pobierać danych podczas przechodzenia między działaniami czy śledzenia zmian w konfiguracji, np. przy obróceniu ekranu.

Więcej informacji na temat posiadaczy uprawnień znajdziesz na stronie właścicieli stanów. Więcej informacji o warstwie interfejsu znajdziesz też w wytycznych dotyczących warstwy interfejsu.

Zalety ViewModel

Alternatywą dla obiektu ViewModel jest zwykła klasa przechowująca dane wyświetlane w interfejsie. Może to powodować problemy podczas nawigowania między działaniami lub miejscami docelowymi nawigacji. W ten sposób zostaną zniszczone dane, których nie zapiszesz za pomocą mechanizmu zapisywania stanu instancji. ViewModel zapewnia wygodny interfejs API zwiększający trwałość danych, który rozwiązuje ten problem.

Zasadniczo główne zalety klasy ViewModel są następujące:

  • Pozwala zachować stan interfejsu użytkownika.
  • Zapewnia dostęp do logiki biznesowej.

Trwałość

ViewModel zapewnia trwałość zarówno przez stan, jaki posiada model ViewModel, jak i przez operacje wywoływane przez niego. Dzięki temu nie musisz ponownie pobierać danych za pomocą typowych zmian konfiguracji, takich jak obrót ekranu.

Zakres

Gdy tworzysz instancję ViewModel, przekazujesz do niego obiekt implementujący interfejs ViewModelStoreOwner. Może to być miejsce docelowe nawigacji, wykres nawigacyjny, aktywność, fragment lub dowolny inny typ implementujący interfejs. Model View jest następnie ograniczony do cyklu życia obiektu ViewModelStoreOwner. Pozostaje w pamięci, dopóki ViewModelStoreOwner nie zniknie na stałe.

Zakres klas jest bezpośrednimi lub pośrednimi podklasami interfejsu ViewModelStoreOwner. Podklasy bezpośrednie to ComponentActivity, Fragment i NavBackStackEntry. Pełną listę podklas pośrednich znajdziesz w dokumentacji ViewModelStoreOwner.

Gdy fragment lub działanie, do którego jest ograniczony model ViewModel, zostaną zniszczone, asynchroniczna praca będzie kontynuowana w modelu ViewModel, którego zakres jest ograniczony. To klucz do wytrwałości.

Więcej informacji znajdziesz w poniższej sekcji poświęconej cyklowi życia modelu ViewModel.

SavedStateHandle

SavedStateHandle pozwala na trwałe przechowywanie danych nie tylko w wyniku zmian konfiguracji, ale i przez odtworzenie każdego procesu. Oznacza to, że pozwala zachować stan UI niezmieniony, nawet gdy użytkownik zamknie aplikację i otworzy ją później.

Dostęp do logiki biznesowej

Mimo że w warstwie danych tkwi większość logiki biznesowej, warstwa interfejsu może też zawierać logikę biznesową. Może się tak zdarzyć, gdy łączysz dane z wielu repozytoriów w celu utworzenia stanu interfejsu ekranu lub gdy określony typ danych nie wymaga warstwy danych.

ViewModel to odpowiednie miejsce do obsługi logiki biznesowej w warstwie interfejsu użytkownika. Model ViewModel odpowiada również za obsługę zdarzeń i przekazywanie ich do innych warstw w hierarchii, gdy trzeba zastosować logikę biznesową do modyfikacji danych aplikacji.

Jetpack Compose

W przypadku korzystania z Jetpack Compose podstawowym sposobem udostępniania stanu interfejsu użytkownika w funkcjach kompozycyjnych jest model ViewModel. W aplikacji hybrydowej działania i fragmenty po prostu hostują funkcje kompozycyjne. Jest to odejście od wcześniejszego podejścia, ponieważ tworzenie elementów interfejsu wielokrotnego użytku z działaniami i fragmentami nie było tak proste i intuicyjne, co sprawiło, że były one znacznie bardziej aktywne jako kontrolery UI.

Najważniejszą rzeczą, o której należy pamiętać podczas korzystania z modelu ViewModel z funkcją Compose, jest to, że nie można określić zakresu obiektu ViewModel na obiekt kompozycyjny. To dlatego, że funkcja kompozycyjna to nie ViewModelStoreOwner. 2 instancje tego samego elementu kompozycyjnego w elemencie „Composition” lub 2 różne obiekty kompozycyjne uzyskujące dostęp do tego samego typu obiektu ViewModel w ramach jednego obiektu ViewModelStoreOwner otrzymają to samo wystąpienie klasy ViewModel, co często nie jest oczekiwanym zachowaniem.

Aby wykorzystać zalety modelu ViewModel w funkcji Compose, przechowuj każdy ekran we fragmencie lub aktywności albo użyj nawigacji związanej z tworzeniem i używaj modeli ViewModels w funkcjach kompozycyjnych jak najbliżej miejsca docelowego Nawigacji. Wynika to z tego, że możesz określić zakres modelu ViewModel na miejsca docelowe nawigacji, wykresy nawigacyjne, aktywności i fragmenty.

Więcej informacji znajdziesz w przewodniku po przenoszeniu stanów w Jetpack Compose.

Wdrażanie modelu widoku

Poniżej znajduje się przykładowa implementacja modelu ViewModel na ekranie, który umożliwia użytkownikowi rzucanie kostkami.

Kotlin

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,
            )
        }
    }
}

Java

public class DiceUiState {
    private final Integer firstDieValue;
    private final Integer secondDieValue;
    private final int numberOfRolls;

    // ...
}

public class DiceRollViewModel extends ViewModel {

    private final MutableLiveData<DiceUiState> uiState =
        new MutableLiveData(new DiceUiState(null, null, 0));
    public LiveData<DiceUiState> getUiState() {
        return uiState;
    }

    public void rollDice() {
        Random random = new Random();
        uiState.setValue(
            new DiceUiState(
                random.nextInt(7) + 1,
                random.nextInt(7) + 1,
                uiState.getValue().getNumberOfRolls() + 1
            )
        );
    }
}

Dostęp do modelu ViewModel możesz uzyskać w aktywności w następujący sposób:

Kotlin

import androidx.activity.viewModels

class DiceRollActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        // Create a ViewModel the first time the system calls an activity's onCreate() method.
        // Re-created activities receive the same DiceRollViewModel instance created by the first activity.

        // Use the 'by viewModels()' Kotlin property delegate
        // from the activity-ktx artifact
        val viewModel: DiceRollViewModel by viewModels()
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect {
                    // Update UI elements
                }
            }
        }
    }
}

Java

public class MyActivity extends AppCompatActivity {
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Create a ViewModel the first time the system calls an activity's onCreate() method.
        // Re-created activities receive the same MyViewModel instance created by the first activity.
        DiceRollViewModel model = new ViewModelProvider(this).get(DiceRollViewModel.class);
        model.getUiState().observe(this, uiState -> {
            // update UI
        });
    }
}

Jetpack Compose

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 funkcją ViewModel

ViewModel zapewnia obsługę współprogramów Kotlin. Może utrzymywać pracę asynchroniczną w taki sam sposób jak utrzymywany stan interfejsu użytkownika.

Więcej informacji znajdziesz w artykule o używaniu współprogramów Kotlin z komponentami architektury Androida.

Cykl życia obiektu ViewModel

Cykl życia obiektu ViewModel jest bezpośrednio powiązany z jego zakresem. Element ViewModel pozostaje w pamięci do momentu, gdy zniknie element ViewModelStoreOwner, do którego jest ograniczony. Może się to zdarzyć w tych kontekstach:

  • W przypadku aktywności – po jej zakończeniu.
  • W przypadku fragmentu, gdy się odłącza.
  • w przypadku wpisu dotyczącego nawigacji, gdy jest on usuwany ze stosu wstecznego.

To sprawia, że obiekty ViewModels stanowią świetne rozwiązanie do przechowywania danych, które nie są w stanie przetrwać zmian konfiguracji.

Rysunek 1 przedstawia różne stany cyklu życia działania podczas jego rotacji, a następnie zakończenia. Ilustracja przedstawia też czas trwania ViewModel obok powiązanego cyklu życia aktywności. Ten diagram ilustruje stany działania. W cyklu życia fragmentu mają zastosowanie te same stany podstawowe.

Ilustruje cykl życia modelu ViewModel w miarę zmian stanu aktywności.

Żądanie ViewModel jest zwykle wysyłane przy pierwszym wywołaniu przez system metody onCreate() obiektu aktywności. System może wywołać metodę onCreate() kilka razy w trakcie występowania działania, na przykład przy obróceniu ekranu urządzenia. Obiekt ViewModel istnieje od momentu, gdy pierwszy raz zażądasz elementu ViewModel, do zakończenia działania i jego zniszczenia.

Czyszczenie zależności ViewModel

ViewModel wywołuje metodę onCleared, gdy ViewModelStoreOwner zniszczy ją w trakcie swojego cyklu życia. Pozwala to wyczyścić wszelkie zadania i zależności zgodne z cyklem życia obiektu ViewModel.

Przykład poniżej pokazuje alternatywę dla viewModelScope. viewModelScope to wbudowana właściwość CoroutineScope, która automatycznie śledzi cykl życia obiektu ViewModel. ViewModel używa go do aktywowania operacji związanych z działalnością firmy. Jeśli do łatwiejszego testowania chcesz użyć zakresu niestandardowego zamiast viewModelScope, obiekt ViewModel może otrzymać zależność CoroutineScope jako zależność w swoim konstruktorze. Gdy ViewModelStoreOwner wyczyści model ViewModel na koniec swojego cyklu życia, obiekt ViewModel anuluje też obiekt CoroutineScope.

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

    // Other ViewModel logic ...

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

Od cyklu życia w wersji 2.5 i nowszych możesz przekazać do konstruktora ViewModel co najmniej 1 obiekt Closeable, który zamyka się automatycznie 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 najważniejszych sprawdzonych metod, które należy stosować podczas implementacji obiektu ViewModel:

  • Ze względu na swój zakres możesz używać obiektów ViewModel jako szczegółów implementacji posiadacza stanu na poziomie ekranu. Nie używaj ich jako stanów elementów interfejsu wielokrotnego użytku, takich jak grupy elementów lub formularze. W przeciwnym razie otrzymasz tę samą instancję ViewModel w różnych przypadkach użycia tego samego komponentu interfejsu użytkownika w ramach tego samego obiektu ViewModelStoreOwner.
  • Obiekty ViewModele nie powinny mieć informacji o szczegółach implementacji interfejsu użytkownika. Zachowaj jak najbardziej ogólne nazwy metod udostępnianych przez interfejs ViewModel API oraz pól stanu interfejsu. Dzięki temu ViewModel może obsłużyć każdy interfejs: telefon komórkowy, składany, tablet, a nawet Chromebooka.
  • Modele View mogą żyć dłużej niż obiekt ViewModelStoreOwner, dlatego nie powinny zawierać żadnych odniesień do interfejsów API związanych z cyklem życia, takich jak Context czy Resources, aby zapobiec wyciekom pamięci.
  • Nie przekazuj obiektów ViewModel do innych klas, funkcji ani innych komponentów interfejsu użytkownika. Ponieważ to platforma, którymi zarządza, staraj się, by były jak najbliżej siebie. W pobliżu funkcji, fragmentu lub funkcji kompozycyjnej na poziomie ekranu. Uniemożliwia to komponentom niższego poziomu dostęp do większej ilości danych i logiki, niż potrzebują.

Dodatkowe informacje

W miarę jak Twoje dane stają się coraz bardziej złożone, możesz utworzyć osobną klasę do ich wczytywania. Obiekt ViewModel służy do herbaty danych dla kontrolera interfejsu użytkownika, tak aby dane mogły przetrwać zmiany w konfiguracji. Informacje o wczytywaniu i utrwalaniu danych oraz zarządzaniu nimi w przypadku różnych zmian konfiguracji znajdziesz w artykule o zapisywaniu stanów interfejsu.

W przewodniku po architekturze aplikacji na Androida zalecamy utworzenie klasy repozytorium do obsługi tych funkcji.

Dodatkowe materiały

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

Dokumentacja

Próbki