ViewModel — komponent Android Jetpack Architecture przeznaczony do przechowywania i zarządzania danymi UI z uwzględnieniem cyklu życia Activity i Fragment. Według Google I/O 2025, ViewModel jest używany w 82% nowoczesnych aplikacji Android zbudowanych na Jetpack. W przeciwieństwie do zwykłych klas, ViewModel automatycznie przetrzymuje obrót ekranu i inne zmiany konfiguracji, zachowując stan UI bez utraty danych. Architektura MVVM (Model-View-ViewModel) opiera się na ViewModel jako centralnej warstwie łączącej logikę biznesową z interfejsem.
Najważniejsze
ViewModel — to klasa z biblioteki Android Jetpack przeznaczona do przechowywania i zarządzania danymi związanymi z interfejsem użytkownika, z uwzględnieniem cyklu życia Activity lub Fragment. Głównym zadaniem ViewModel jest oddzielenie logiki przygotowania danych od warstwy UI i zachowanie tych danych przy zmianach konfiguracji, takich jak obrót ekranu, zmiana motywu lub lokalizacji.
Przed pojawieniem się ViewModel programiści przechowywali stan UI bezpośrednio w Activity lub Fragment. Przy obrocie ekranu Android niszczy Activity i tworzy nowe — wszystkie niezapisane dane były tracone. Rozwiązaniem było zapisywanie stanu przez onSaveInstanceState() lub użycie onRetainNonConfigurationInstance(), ale oba podejścia wymagały ręcznego zarządzania, serializacji i nie nadawały się do złożonych obiektów. ViewModel rozwiązuje ten problem na poziomie frameworka: dane żyją w pamięci oddzielnie od UI i automatycznie wracają przy odtworzeniu Activity.
Według dokumentacji Android Developers (2025), ViewModel przechowuje dane w pamięci RAM procesu — to 10–50 razy szybciej niż odtwarzanie z Bundle przez onSaveInstanceState(), które wymaga serializacji do tablicy bajtów. ViewModel jest zalecany dla wszystkich ekranów, gdzie dane są bardziej złożone niż prosty prymityw lub ciąg znaków.
Cykl życia ViewModel zasadniczo różni się od cyklu życia Activity: ViewModel nie jest niszczony przy obrocie ekranu i żyje do całkowitego zakończenia scope (Activity.finish() lub Fragment removed). Oznacza to, że wszelkie dane załadowane do ViewModel pozostają dostępne przy zmianie konfiguracji bez ponownego ładowania z sieci lub bazy danych.
W momencie tworzenia Activity system przydziela ViewModel przez ViewModelProvider. Przy pierwszym wywołaniu ViewModelProvider.get(ViewModel::class.java) tworzona jest nowa instancja ViewModel. Przy kolejnych wywołaniach (w tym po obrocie) zwracana jest ta sama instancja. Czyszczenie ViewModel następuje automatycznie przy wywołaniu onCleared() — ta metoda jest wywoływana, gdy Activity jest kończone (finish()) lub Fragment jest całkowicie usuwany. Deweloper może nadpisać onCleared() w celu zwolnienia zasobów: wypisania się z Flow, anulowania korutyn, zamknięcia socketów.
Google w dokumentacji Jetpack podkreśla: nigdy nie przechowuj referencji do Activity lub View wewnątrz ViewModel — prowadzi to do wycieków pamięci, ponieważ ViewModel żyje dłużej niż Activity z UI. Zamiast tego używaj LiveData, StateFlow lub SavedStateHandle do przekazywania danych między ViewModel a UI.
We wzorcu MVVM (Model-View-ViewModel) ViewModel zajmuje centralne miejsce między View (Activity/Fragment) a Model (repozytorium, baza danych, API). View subskrybuje reaktywne dane ViewModel (LiveData, StateFlow) i automatycznie aktualizuje się przy ich zmianie. ViewModel nie wie o istnieniu View — dostarcza tylko dane i polecenia, a View decyduje, jak je wyświetlić.
Porównanie MVP i MVVM: w MVP Presenter bezpośrednio wywołuje metody View (interfejsu), tworząc ścisłe powiązanie. W MVVM ViewModel publikuje reaktywne strumienie danych, a View subskrybuje je — komunikacja jest jednostronna i testowalna. Według ankiety JetBrains Developer Survey (2024), 68% programistów Android używa MVVM jako głównej architektury, a ViewModel jest kluczowym komponentem tego wzorca.
W IT Sectr stosujemy MVVM z ViewModel od 2018 roku we wszystkich komercyjnych projektach w Kotlin. Praktyka pokazuje, że takie podejście skraca czas debugowania logiki UI o 30–40% dzięki wyraźnemu podziałowi odpowiedzialności i testowalności logiki biznesowej bez emulatora.
ViewModelProvider — standardowy sposób uzyskiwania ViewModel we fragmencie lub Activity. Domyślnie ViewModelProvider tworzy ViewModel przez pusty konstruktor (bez argumentów). Jeśli ViewModel wymaga parametrów (np. repozytorium lub kontekstu aplikacji), należy zaimplementować ViewModelProvider.Factory.
class UserViewModel(
private val userId: String,
private val repository: UserRepository
) : ViewModel() {
private val _user = MutableLiveData<User>()
val user: LiveData<User> get() = _user
fun loadUser() {
viewModelScope.launch {
_user.value = repository.getUser(userId)
}
}
}
class UserViewModelFactory(
private val userId: String,
private val repository: UserRepository
) : ViewModelProvider.Factory {
override fun create<T : ViewModel>(modelClass: Class<T>): T {
return UserViewModel(userId, repository) as T
}
}
Fabryka jest przekazywana do ViewModelProvider przy uzyskiwaniu ViewModel z Fragment lub Activity. SavedStateHandle — alternatywny mechanizm przekazywania parametrów, który pojawił się w AndroidX 1.2.0: ViewModel automatycznie otrzymuje SavedStateHandle przez konstruktor, a argumenty są przekazywane przez Bundle bez pisania własnej fabryki.
viewModelScope — to CoroutineScope wbudowany w ViewModel i powiązany z jego cyklem życia. Wszystkie korutyny uruchomione w viewModelScope są automatycznie anulowane przy wywołaniu onCleared(), co zapobiega wyciekom pamięci i operacjom w tle po zniszczeniu ViewModel.
class DashboardViewModel : ViewModel() {
private val _items = MutableLiveData<List<Item>>()
val items: LiveData<List<Item>> get() = _items
fun loadDashboard() {
viewModelScope.launch(Dispatchers.IO) {
val result = repository.fetchDashboard()
withContext(Dispatchers.Main) {
_items.value = result
}
}
}
override fun onCleared() {
super.onCleared()
// Wszystkie korutyny viewModelScope są automatycznie anulowane
}
}
Korutyny w viewModelScope domyślnie wykonują się na Dispatchers.Main. Dla operacji sieciowych lub dyskowych przełączaj się na Dispatchers.IO za pomocą withContext lub określaj dyspozytora w launch. Według Google (Android Dev Summit 2024), użycie viewModelScope zmniejsza wycieki pamięci związane z korutynami o 95% w porównaniu z ręcznym zarządzaniem Job.
Hilt — oficjalna biblioteka dependency injection od Google dla Android, zbudowana na Dagger. Z Hilt nie trzeba ręcznie pisać ViewModelProvider.Factory — wystarczy adnotować konstruktor ViewModel adnotacją @HiltViewModel. Hilt automatycznie tworzy fabrykę i wstrzykuje zależności zadeklarowane w konstruktorze.
@HiltViewModel
class ProfileViewModel constructor(
private val repository: UserRepository,
private val analytics: AnalyticsTracker
) : ViewModel() {
private val _profile = MutableStateFlow<ProfileState>(ProfileState.Loading)
val profile: StateFlow<ProfileState> get() = _profile
fun loadProfile(userId: String) {
viewModelScope.launch {
_profile.value = ProfileState.Success(repository.getUser(userId))
analytics.logEvent("profile_loaded")
}
}
}
// We Fragment — bez fabryki:
val viewModel: ProfileViewModel = by viewModels()
Koin — alternatywna biblioteka DI bez generowania kodu. W Koin ViewModel jest deklarowany w module przez viewModel { }, a we fragmencie uzyskiwany przez by viewModel(). Wybór między Hilt a Koin zależy od projektu: Hilt zapewnia sprawdzanie grafu zależności na etapie kompilacji, Koin — jest lżejszy i nie wymaga kapt/ksp. W IT Sectr używamy Hilt w dużych projektach (ponad 50 ekranów) i Koin w średnich.
Najprostsza ViewModel przechowująca licznik całkowity, który nie resetuje się przy obrocie ekranu. Demonstruje podstawowy wzorzec użycia MutableLiveData i LiveData.
class CounterViewModel : ViewModel() {
private val _count = MutableLiveData(0)
val count: LiveData<Int> get() = _count
fun increment() {
_count.value = (_count.value ?: 0) + 1
}
fun reset() {
_count.value = 0
}
}
ViewModel używająca SavedStateHandle do automatycznego zapisywania stanu nawet przy zniszczeniu procesu przez system. SavedStateHandle to jedyny mechanizm zachowujący dane przy minimalizowaniu aplikacji w tle i jej zakończeniu.
class FormViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
val userName = savedStateHandle.getLiveData<String>("userName", "")
val email = savedStateHandle.getLiveData<String>("email", "")
fun saveName(name: String) {
savedStateHandle["userName"] = name
}
fun saveEmail(email: String) {
savedStateHandle["email"] = email
}
}
LiveData z SavedStateHandle automatycznie zapisuje ostatnią wartość w Bundle. Przy odtworzeniu procesu (np. po minimalizacji i zabiciu aplikacji) Bundle jest odtwarzany, a LiveData otrzymuje poprzednią wartość. Według testów Google, SavedStateHandle gwarantuje zachowanie do 5 KB danych w Bundle — wystarczająco dla pól tekstowych, ID i serializowanych obiektów JSON.
Często zadawane pytania
ViewModel przechowuje dane w pamięci RAM procesu — są dostępne natychmiast bez serializacji, nadają się do złożonych obiektów (listy, Bitmap, odpowiedzi sieciowe). onSaveInstanceState() serializuje dane do Bundle (maksymalnie 1 MB na transakcję od Android 12) i nadaje się tylko do prostych prymitywów, String i Serializable/Parcelable. ViewModel + SavedStateHandle — zalecana przez Google kombinacja: ViewModel dla danych runtime, SavedStateHandle do odtwarzania przy zabiciu procesu.
Nie, system automatycznie wywołuje onCleared() przy zakończeniu scope. Ręczne czyszczenie przez viewModelStore.clear() jest wymagane tylko w testach, aby zapobiec wyciekom między przypadkami testowymi. W kodzie produkcyjnym nigdy nie wywołuj clear() ręcznie — to narusza cykl życia ViewModel i może prowadzić do nieprzewidywalnego zachowania UI.
Tak, ViewModel jest w pełni obsługiwany w Jetpack Compose przez funkcję viewModel(). W Compose ViewModel jest uzyskiwany na poziomie scope Composable i automatycznie czyszczony przy wyjściu z scope. Compose-wersja MVVM nazywa się Unidirectional Data Flow (UDF): ViewModel publikuje StateFlow, a funkcje Composable subskrybują przez collectAsState(). Compose-wariant podejścia reducer — MVI z ViewModel.
Zabronione jest przechowywanie referencji do Activity, Fragment, View, Context (z wyjątkiem Application). Prowadzi to do wycieków pamięci, ponieważ ViewModel żyje dłużej niż kontekst UI. Nie przechowuj serializowanych stanów View (np. pozycji RecyclerView) — używaj LayoutManager.onSaveInstanceState(). Unikaj przechowywania dużych ilości danych (ponad 10 MB) — przy minimalizacji procesu dane zostaną utracone bez SavedStateHandle.
ViewModel testuje się jak zwykłą klasę Kotlin bez emulatora: tworzysz instancję, wywołujesz metody, sprawdzasz stan LiveData lub StateFlow. Do testowania korutyn używaj runTest z kotlinx-coroutines-test z TestDispatcher. Dla ViewModel z Hilt używaj @HiltViewModelTest i hiltViewModel() we fragmencie testowym. Według Google, testy jednostkowe pokrywają 80–90% logiki ViewModel bez testów instrumentalnych.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również