LiveData
У build.gradle файл проекту додайте репозитарій google()
allprojects {
repositories {
jcenter()
google()
}
...
}
У build.gradle файлі модуля додайте dependencies:
dependencies {
implementation "android.arch.lifecycle:extensions:1.0.0"
annotationProcessor "android.arch.lifecycle:compiler:1.0.0"
...
}
LiveData - сховище даних, що працює за принципом патерну Observer (спостерігач). Це сховище вміє робити дві речі:
- У нього можна помістити будь-який об'єкт
- На нього можна підписатися й отримувати об'єкти, які в нього поміщають.
Тобто з одного боку, хтось поміщає об'єкт у сховище, а з іншого боку, хтось підписується і отримує цей об'єкт.
Як аналогію можна навести, наприклад, канали в Telegram. Автор пише пост і відправляє його в канал, а всі підписники отримують цей пост.
Здавалося б, нічого особливого в такому сховищі немає, але є один дуже важливий нюанс. LiveData вміє визначати активний підписник чи ні, і надсилати дані буде тільки активним передплатникам. Передбачається, що підписники LiveData будуть Activity і фрагменти. А їхній стан активності визначатиметься за допомогою їхнього Lifecycle об'єкта, який ми уже розглядали.
Отримання даних із LiveData
Нехай у нас є якийсь синглтон клас DataController, з якого можна отримати LiveData<String>.
LiveData<String> liveData = DataController.getInstance().getData();
DataController періодично щось там усередині себе робить і оновлює дані в LiveData. Як він це робить, ми подивимося трохи пізніше. Спочатку подивимося, як Activity може підписатися на LiveData і отримувати дані, які поміщає в нього DataController.
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
LiveData<String> liveData = DataController.getInstance().getData();
liveData.observe(this, new Observer<String>() {
@Override
public void onChanged(@Nullable String value) {
textView.setText(value)
}
});
}
Отримуємо LiveData з DataController, і методом observe підписуємося. У метод observe нам необхідно передати два параметри:
Перший - це LifecycleOwner. Нагадаю, що LifecycleOwner - це інтерфейс із методом getLifecycle. Activity і фрагменти в Support Library, починаючи з версії 26.1.0 реалізують цей інтерфейс, тому ми передаємо this.
LiveData отримає з Activity його Lifecycle і за ним визначатиме стан Activity. Активним вважається стан STARTED або RESUMED. Тобто якщо Activity видно на екрані, то LiveData вважає його активним і буде відправляти дані в його колбек.
Другий параметр - це безпосередньо підписник, тобто колбек, у який LiveData відправлятиме дані. У ньому тільки один метод onChanged. У нашому прикладі туди буде приходити String.
Тепер, коли DataController помістить будь-який String-об'єкт у LiveData, ми одразу отримаємо цей об'єкт в Activity, якщо Activity перебуває в стані STARTED або RESUMED.
Нюанси поведінки
Розпишу відразу кілька важливих моментів у поведінці LiveData.
- Якщо
Activityбуло неактивним під час оновлення даних уLiveData, то при поверненні в активний стан, йогоobserverотримає останнє актуальне значення даних. - У момент підписки,
observerотримає останнє актуальне значення зLiveData. - Якщо
Activityбуде закрито, тобто перейде в статусDESTROYED, тоLiveDataавтоматично відпише від себе йогоobserver. - Якщо
Activityу станіDESTROYEDспробує підписатися, то підписку не буде виконано. - Якщо
Activityвже підписувало свійobserver, і спробує зробити це ще раз, то просто нічого не станеться. - Ви завжди можете отримати останнє значення
LiveDataза допомогою його методуgetValue. - Як бачите, підписувати
ActivityнаLiveData- це зручно. Поворот екрана і повне закриттяActivity- все це коректно і зручно обробляється автоматично без будь-яких зусиль з нашого боку.
Надсилання даних у LiveData
Ми розібралися, як отримувати дані з LiveData, і яким чином при цьому враховується стан Activity. Тепер давайте подивимося з іншого боку - як передавати дані в LiveData.
У класі DataController змінна LiveData матиме такий вигляд:
private MutableLiveData<String> liveData = new MutableLiveData<>();
LiveData<String> getData() {
return liveData;
}
Назовні ми передаємо LiveData, який дозволить зовнішнім об'єктам тільки отримувати дані. Але всередині DataController ми використовуємо об'єкт MutableLiveData, який дозволяє поміщати в нього дані.
Щоб помістити значення в MutableLiveData, використовується метод setValue:
liveData.setValue("new value");
Цей метод оновить значення LiveData, і всі його активні підписники отримають це оновлення.
Метод setValue має бути викликаний з UI потоку. Для оновлення даних з інших потоків використовуйте метод postValue. Він перенаправить виклик в UI потік. Відповідно, передплатники завжди отримуватимуть значення в основному потоці.
Transformations
Ви можете поміняти типи даних у LiveData за допомогою Transformations.map (документація).
Розглянемо приклад, у якому LiveData<String> будемо перетворювати на LiveData<Integer>:
LiveData<String> liveData = ...;
LiveData<Integer> liveDataInt = Transformations.map(liveData, new Function<String, Integer>() {
@Override
public Integer apply(String input) {
return Integer.parseInt(input);
}
});
У метод map передаємо наявний LiveData<String> і функцію перетворення. У цій функції ми будемо отримувати String дані з LiveData<String>, і від нас вимагається перетворити їх в Integer. У цьому випадку просто парсимо рядок у число.
На виході методу map отримаємо LiveData<Integer>. Можна сказати, що він підписаний на LiveData<String> і всі отримані String-значення конвертуватиме в Integer і розсилатиме вже своїм підписникам.
Розглянемо складніший випадок. У нас є LiveData<Long>, нам необхідно з нього отримати LiveData<User>. Конвертація id в User виглядає так:
private LiveData<User> getUser(long id) {
// ...
}
За id ми отримуємо LiveData<User> і на нього треба буде підписуватися, щоб отримати об'єкт User.
У цьому разі ми не можемо використовувати метод map, тому що ми отримаємо приблизно такий результат:
LiveData<Long> liveDataId = ...;
LiveData<LiveData<User>> liveDataUser = Transformations.map(liveDataId, new Function<Long, LiveData<User>>() {
@Override
public LiveData<User> apply(Long id) {
return getUser(id);
}
});
На виході буде об'єкт LiveData<LiveData<User>>. Щоб уникнути цього, використовуємо switchMap замість map.
LiveData<Long> liveDataId = ...;
LiveData<User> liveDataUser = Transformations.switchMap(liveDataId, new Function<Long, LiveData<User>>() {
@Override
public LiveData<User> apply(Long id) {
return getUser(id);
}
});
switchMap прибере вкладеність LiveData і ми отримаємо LiveData<User>.
Свій LiveData
У деяких ситуаціях зручно створити свою обгортку LiveData.
Розглянемо приклад:
public class LocationLiveData extends LiveData<Location> {
LocationService.LocationListener locationListener = new LocationService.LocationListener() {
@Override
public void onLocationChanged(Location location) {
setValue(location);
}
};
@Override
protected void onActive() {
LocationService.addListener(locationListener);
}
@Override
protected void onInactive() {
LocationService.removeListener(locationListener);
}
}
Клас LocationLiveData розширює LiveData<Location>.
Усередині нього є якийсь locationListener - слухач, який можна передати в LocationService і отримувати оновлення поточного місця розташування. При отриманні нового Location від LocationService, locationListener буде викликати метод setValue і тим самим оновлювати дані цього LiveData.
LocationService - це просто якийсь сервіс, який надає нам поточну локацію. Його реалізація в цьому прикладі не важлива. Головне - це те, що ми підписуємося (addListener) на сервіс, коли нам потрібні дані, і відписуємося (removeListener), коли дані більше не потрібні.
Зверніть увагу, що ми перевизначили методи onActive і onInactive. onActive буде викликаний, коли у LiveData з'явиться хоча б один підписник. А onInactive - коли не залишиться жодного підписника. Відповідно ці методи зручно використовувати для підключення/відключення нашого слухача до LocationService.
Вийшла зручна обгортка, яка за появи підписників сама підписуватиметься до LocationService, отримуватиме Location і передаватиме його своїм підписникам. А коли підписників не залишиться, то LocationLiveData відпишеться від LocationService.
Залишилося зробити з LocationLiveData синглтон і можна використовувати його в різних Activity і фрагментах.
MediatorLiveData
MediatorLiveData дає можливість збирати дані з декількох LiveData в один. Це зручно, якщо у вас є кілька джерел, з яких ви хочете отримувати дані. Ви об'єднуєте їх в одне і підписуєтеся тільки на нього.
Розглянемо, як це робиться, на простому прикладі.
MutableLiveData<String> liveData1 = new MutableLiveData<>();
MutableLiveData<String> liveData2 = new MutableLiveData<>();
MediatorLiveData<String> mediatorLiveData = new MediatorLiveData<>();
У нас є два LiveData<String>: liveData1 і liveData2. Ми хочемо об'єднати їх в один. Для цього нам знадобиться MediatorLiveData.
Додаємо LiveData до MediatorLiveData
mediatorLiveData.addSource(liveData1, new Observer<String>() {
@Override
public void onChanged(@Nullable String s) {
mediatorLiveData.setValue(s);
}
});
mediatorLiveData.addSource(liveData2, new Observer<String>() {
@Override
public void onChanged(@Nullable String s) {
mediatorLiveData.setValue(s);
}
});
Метод addSource вимагає від нас два параметри.
Перший - це LiveData, з якого MediatorLiveData збирається отримувати дані.
Другий параметр - це колбек, який буде використаний для підписки на LiveData з першого параметра. Зверніть увагу, що в колбеку нам треба самим передавати в MediatorLiveData дані, одержувані з LiveData. Це робиться методом setValue.
Таким чином mediatorLiveData буде отримувати дані з двох LiveData і постити їх своїм одержувачам.
Підпишемося на mediatorLiveData
mediatorLiveData.observe(this, new Observer<String>() {
@Override
public void onChanged(@Nullable String s) {
log("onChanged " + s);
}
});
Сюди тепер мають приходити дані з liveData1 і liveData2. Будемо їх просто логувати.
Відправимо дані в liveData1 і liveData2:
liveData1.setValue("1");
liveData2.setValue("a");
liveData1.setValue("2");
liveData2.setValue("b");
liveData1.setValue("3");
liveData2.setValue("c");
Дивимося лог:
onChanged 1
onChanged a
onChanged 2
onChanged b
onChanged 3
onChanged c
Усі дані, що ми передавали в liveData1 і liveData2, надійшли в загальний mediatorLiveData.
Трохи ускладнимо приклад. Припустимо, нам треба відписатися від liveData2, коли з нього прийде значення "finish".
Код підписки mediatorLiveData на liveData1 і liveData2 матиме такий вигляд:
mediatorLiveData.addSource(liveData1, new Observer<String>() {
@Override
public void onChanged(@Nullable String s) {
mediatorLiveData.setValue(s);
}
});
mediatorLiveData.addSource(liveData2, new Observer<String>() {
@Override
public void onChanged(@Nullable String s) {
if ("finish".equalsIgnoreCase(s)) {
mediatorLiveData.removeSource(liveData2);
return;
}
mediatorLiveData.setValue(s);
}
});
У випадку з liveData1 нічого не змінюється.
А ось при отриманні даних від liveData2 ми дивимося, що за значення прийшло. Якщо це значення "finish", то методом removeSource відписуємо mediatorLiveData від liveData2 і не передаємо це значення далі.
Відправимо кілька значень
liveData1.setValue("1");
liveData2.setValue("a");
liveData2.setValue("finish");
liveData1.setValue("2");
liveData2.setValue("b");
liveData1.setValue("3");
liveData2.setValue("c");
liveData2 відправляє тут значення "a", "finish", "b" і "c". Через mediatorLiveData має пройти тільки "a". А значення з liveData1 мають пройти всі.
Запускаємо, дивимося лог:
onChanged 1
onChanged a
onChanged 2
onChanged 3
Усе правильно. Під час отримання "finish" від liveData2, mediatorLiveData відписався від нього, і наступні його дані ми вже не отримували.
RxJava
Ми можемо конвертувати LiveData в Rx і навпаки. Для цього є інструмент LiveDataReactiveStreams.
Щоб його використовувати додайте в dependencies:
implementation "android.arch.lifecycle:reactivestreams:1.0.0"
Щоб отримати LiveData з Flowable або Observable, використовуємо метод fromPublisher:
Flowable<String> flowable = ... ;
LiveData<String> liveData = LiveDataReactiveStreams.fromPublisher(flowable);
LiveData буде підписаний на Flowable, поки у нього (у LiveData) є передплатники.
LiveData не зможе обробити або отримати onError від Flowable. Якщо у Flowable виникне помилка, то буде креш.
Неважливо, в якому потоці працює Flowable, результат у LiveData завжди прийде в UI потоці.
Щоб отримати Flowable або Observable з LiveData потрібно виконати два перетворення. Спочатку використовуємо метод toPublisher, щоб отримати Publisher. Потім отриманий Publisher передаємо в метод Flowable.fromPublisher:
LiveData<String> liveData = … ;
Flowable<String> flowable = Flowable.fromPublisher(
LiveDataReactiveStreams.toPublisher(this, liveData));
Інші методи LiveData
hasActiveObservers()- перевірка наявності активних підписниківhasObservers()- перевірка наявності будь-яких підписниківobserveForever(Observer<T> observer)- дозволяє підписатися без урахування Lifecycle. Тобто цей передплатник буде завжди вважатися активним.removeObserver(Observer<T> observer)- дає змогу відписати підписникаremoveObservers(LifecycleOwner owner)- дає змогу відписати всіх підписників, які зав'язані наLifecycleвід зазначеногоLifecycleOwner.