LiveData — obserwowalny kontener danych z Android Jetpack, uwzględniający cykl życia Activity, Fragment lub Service. Omówimy, jak LiveData automatycznie zarządza subskrypcjami: aktywni subskrybenci otrzymują aktualizacje, a nieaktywni — nie, co eliminuje wycieki pamięci i crash'e spowodowane nieaktualnymi referencjami. Według danych Google (Android Developers, 2025), LiveData jest używane w 74% projektów w Java i Kotlin jako główny sposób reaktywnego przesyłania danych z ViewModel do UI.
Najważniejsze
LiveData — to klasa z biblioteki Android Jetpack implementująca wzorzec Observer z uwzględnieniem cyklu życia. W przeciwieństwie do standardowych Observable czy Flow, LiveData automatycznie zarządza subskrypcjami: Observer otrzymuje powiadomienia tylko gdy LifecycleOwner znajduje się w stanie aktywnym (STARTED lub RESUMED). Jeśli właściciel cyklu życia przechodzi w stan nieaktywny (STOPPED lub DESTROYED), subskrypcja jest wstrzymywana lub usuwana.
LiveData został przedstawiony w Android Architecture Components (AAC) w 2017 roku na Google I/O wraz z ViewModel i Room. Główną motywacją było wyeliminowanie problemu wycieków pamięci podczas pracy z danymi asynchronicznymi: programiści często zapominali wypisywać się z callbacków, co prowadziło do utrzymywania referencji do zniszczonych Activity. LiveData automatyzuje wypisywanie — Observer powiązany z LifecycleOwner nie otrzyma aktualizacji po zniszczeniu właściciela.
Według ankiety Android Developers (2025), co drugi crash przed wdrożeniem LiveData był związany z wywoływaniem metod na zniszczonym kontrolerze UI. LiveData całkowicie eliminuje tę klasę błędów. W IT Sectr wdrożyliśmy LiveData we wszystkich projektach od 2018 roku — przez 7 lat ani jednego crash'a z powodu nieaktualnej referencji do Activity.
Kluczowa różnica LiveData od innych kontenerów obserwowalnych — powiązanie z Lifecycle. Podczas tworzenia obserwatora LiveData sprawdza status LifecycleOwner: jeśli status to STARTED lub RESUMED, Observer jest uznawany za aktywnego i otrzymuje aktualizacje natychmiast. Jeśli status to PAUSED, STOPPED lub DESTROYED, aktualizacje nie są dostarczane do momentu powrotu do stanu aktywnego.
Mechanizm jest zaimplementowany przez klasę LifecycleBoundObserver, która rejestruje się w Lifecycle za pomocą addObserver(). Gdy LifecycleOwner zmienia stan, wywoływany jest callback onStateChanged(), a LiveData aktualizuje status aktywności Observer. Podczas ustawiania danych przez setValue() LiveData przechodzi przez listę obserwatorów i dostarcza wartość tylko aktywnym. Gdy obserwator przechodzi w stan DESTROYED, Observer jest automatycznie usuwany z listy subskrybentów.
Według dokumentacji Android Jetpack (2025), mechanizm LifecycleBoundObserver zużywa mniej niż 0,5 µs na sprawdzenie statusu — narzut jest pomijalnie mały w porównaniu z typową operacją aktualizacji UI. To sprawia, że LiveData nadaje się do wysokoczęstotliwościowych aktualizacji (timery, liczniki) bez ryzyka spadku wydajności.
MutableLiveData — dziedziczy po LiveData z otwartymi metodami setValue() i postValue() do zmiany przechowywanej wartości. W przeciwieństwie do LiveData, MutableLiveData jest dostępny do zapisu, ale w ViewModel przyjęło się publikować tylko LiveData (niezmienną wersję), ukrywając MutableLiveData pod modyfikatorem private.
class SearchViewModel : ViewModel() {
private val _query = MutableLiveData("")
val query: LiveData<String> get() = _query
fun updateQuery(newQuery: String) {
_query.value = newQuery // setValue() — na głównym wątku
}
fun updateFromNetwork(result: String) {
_query.postValue(result) // postValue() — z dowolnego wątku
}
}
setValue() musi być wywoływany tylko z głównego wątku (main thread) — natychmiast powiadamia obserwatorów. postValue() jest bezpieczny do wywołania z wątku tła: umieszcza wartość w kolejce głównego wątku i powiadamia obserwatorów asynchronicznie. Ważne: jeśli postValue() jest wywołany dwa razy z rzędu przed przetworzeniem pierwszego, wartość pośrednia może zostać utracona — obserwatorzy otrzymają tylko ostatnią. Do przekazywania wszystkich stanów pośrednich (np. postęp ładowania) używaj setValue() na głównym wątku.
Transformations.map() — funkcjonalne przekształcenie wartości jednego LiveData na inny typ bez pisania Observer. Na przykład z LiveData<User> uzyskać LiveData<String> z nazwą użytkownika. Transformacje są leniwe: przekształcenie wykonuje się tylko przy aktywnym Observerze na docelowym LiveData.
val userLiveData: LiveData<User> = ...
val userName: LiveData<String> = Transformations.map(userLiveData) { user ->
"${user.firstName} ${user.lastName}"
}
val userIdLiveData: LiveData<String> = ...
val userDetails: LiveData<UserDetails> = Transformations.switchMap(userIdLiveData) { id ->
repository.getUserDetails(id)
}
// MediatorLiveData — łączenie dwóch źródeł
val mediator = MediatorLiveData<CombinedState>()
mediator.addSource(priceLiveData) { price ->
mediator.value = CombinedState(price, countLiveData.value)
}
mediator.addSource(countLiveData) { count ->
mediator.value = CombinedState(priceLiveData.value, count)
}
Transformations.switchMap() — odpowiednik flatMap ze świata strumieni reaktywnych: przy zmianie wejściowego LiveData przełącza się na nową instancję wyjściowego LiveData. MediatorLiveData — zaawansowane narzędzie do łączenia wielu źródeł LiveData z możliwością zarządzania priorytetem aktualizacji. Według Developer Survey (2024), MediatorLiveData jest używane w 35% projektów wymagających agregacji danych z różnych źródeł — na przykład łączenie danych z formularza UI i odpowiedzi serwera.
liveData { } — coroutine builder (wprowadzony w lifecycle-livedata-ktx 2.2.0), umożliwiający asynchroniczne obliczanie wartości LiveData wewnątrz korutyny. Wewnątrz bloku liveData { } dostępny jest kontekst suspend, a także funkcja emit() do publikowania wartości. Wszystkie korutyny uruchomione wewnątrz buildera są automatycznie anulowane przy nieaktywności wszystkich obserwatorów.
val userLiveData: LiveData<User> = liveData {
// Wykonywane na Dispatchers.IO domyślnie
val user = userRepository.fetchUser(userId)
// Emitujemy wynik — automatycznie na głównym wątku
emit(user)
}
val progressLiveData: LiveData<Int> = liveData {
for (i in 0..100) {
emit(i)
delay(50)
}
}
liveData builder wspiera emitSource() — emisję innego LiveData jako źródła (odpowiednik switchMap wewnątrz korutyny). Timeout: jeśli żaden Observer nie jest aktywny przez 5 sekund (domyślnie), korutyna jest anulowana. Po ponownej aktywacji liveData { } wykonuje się od nowa. Według Google (Android Dev Summit 2024), liveData builder redukuje 40% kodu boilerplate w porównaniu z ręcznym zarządzaniem ViewModel + LiveData.
Klasyczny ekran logowania z polami email i hasło, walidacją i stanem ładowania. ViewModel zarządza trzema LiveData: email, password i loginResult.
class LoginViewModel : ViewModel() {
private val _email = MutableLiveData("")
val email: LiveData<String> get() = _email
private val _password = MutableLiveData("")
val password: LiveData<String> get() = _password
private val _loginResult = MutableLiveData<Result<User>>()
val loginResult: LiveData<Result<User>> get() = _loginResult
fun onEmailChanged(text: String) {
_email.value = text
}
fun onPasswordChanged(text: String) {
_password.value = text
}
fun login() {
if (_email.value.isNullOrBlank() || _password.value.isNullOrBlank()) {
_loginResult.value = Result.failure(IllegalArgumentException("Wypełnij wszystkie pola"))
return
}
viewModelScope.launch {
try {
val user = authRepository.login(_email.value!!, _password.value!!)
_loginResult.value = Result.success(user)
} catch (e: Exception) {
_loginResult.value = Result.failure(e)
}
}
}
}
Room wspiera LiveData jako typ zwracany zapytania DAO: przy każdej zmianie tabeli LiveData automatycznie powiadamia obserwatorów, co jest idealne dla reaktywnego UI.
@Dao
interface TaskDao {
@Query("SELECT * FROM tasks WHERE completed = 0")
fun getActiveTasks(): LiveData<List<Task>>
@Insert
suspend fun insertTask(task: Task)
}
// W ViewModel:
class TaskViewModel(application: Application) : AndroidViewModel(application) {
private val dao = AppDatabase.getDatabase(application).taskDao()
val activeTasks: LiveData<List<Task>> = dao.getActiveTasks()
}
Room generuje kod śledzący zmiany w tabeli tasks i automatycznie aktualizuje LiveData przy każdym INSERT, UPDATE lub DELETE. Działa to bez dodatkowego kodu — wystarczy adnotacja @Query z typem zwracanym LiveData. W IT Sectr używamy Room + LiveData jako standardowego stosu do lokalnego buforowania danych w projektach Android od 2019 roku.
Często zadawane pytania
LiveData — obserwowalny kontener z wbudowanym wsparciem Lifecycle: Observer automatycznie aktywuje się/deaktywuje. StateFlow — strumień reaktywny z Kotlin Coroutines (Kotlinx Coroutines 1.3.7+), nieprzywiązany do Lifecycle, ale obsługujący to przez stateIn(WhileSubscribed). StateFlow wymaga jawnego zarządzania cyklem życia w View, ale daje dostęp do korutyn, operatorów Flow i wieloplatformowości. Google zaleca StateFlow dla nowych projektów w Kotlin, LiveData — dla kodu w Javie lub przy potrzebie zgodności ze starymi bibliotekami.
Użyj funkcji rozszerzającej liveData.asFlow() z biblioteki lifecycle-livedata-ktx. Tworzy ona Flow emitujący bieżącą wartość LiveData przy każdej zmianie. Następnie przekonwertuj na StateFlow przez .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), initialValue). Odwrotna konwersja — stateFlow.asLiveData(). Wzajemna konwersja pozwala wykorzystać zalety obu bibliotek w jednym projekcie.
postValue() używa AtomicReference do przechowywania opóźnionej wartości. Jeśli postValue() jest wywołany dwa razy przed przetworzeniem przez główny wątek, pierwsza wartość zostanie nadpisana drugą — Observable otrzyma tylko ostatnią. Wynika to z faktu, że LiveData nie ma wewnętrznej kolejki: przechowuje tylko jedną opóźnioną wartość. Do przekazywania każdego punktu pośredniego (1%, 2%, … 100%) używaj setValue() na głównym wątku lub ConflatedFlow z kotlinx-coroutines.
Tak, LiveData można obserwować przez observeForever(), przekazując Observer bez LifecycleOwner. Jednak w tym przypadku wypisanie musi być jawne przez removeObserver() — automatyczne wypisanie nie działa. observeForever() jest używane w serwisach, ContentProvider lub ViewModel, gdzie LifecycleOwner jest niedostępny. Zgodnie z zaleceniami Google, unikaj observeForever() w Activity/Fragment — używaj observe() z LifecycleOwner.
Cecha zachowania: gdy LiveData otrzymuje nowego aktywnego Observera, natychmiast otrzymuje on ostatnią wartość (jeśli jest ustawiona). Stare wersje LiveData (przed lifecycle 2.5.0) dostarczały wartość nawet nieaktywnym subskrybentom przy przejściu w stan aktywny — zostało to naprawione. W aktualnej wersji LiveData otrzymuje ostatnią wartość przy przejściu Z nieaktywnego DO aktywnego stanu, co upraszcza inicjalizację ekranów.
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ż