В прошлом уроке мы разобрались, как отправлять и получать данные в LiveData. В этом уроке рассмотрим несколько дополнительных возможностей. Как преобразовать тип данных с помощью map. Как создать свой LiveData. Как объединить несколько LiveData в один с помощью MediatorLiveData.
Полный список уроков курса:
- Урок 1. Lifecycle
- Урок 2. LiveData
- Урок 3. LiveData. Дополнительные возможности
- Урок 4. ViewModel
- Урок 5. Room. Основы
- Урок 6. Room. Entity
- Урок 7. Room. Insert, Update, Delete, Transaction
- Урок 8. Room. Query
- Урок 9. Room. RxJava
- Урок 10. Room. Запрос из нескольких таблиц. Relation
- Урок 11. Room. Type converter
- Урок 12. Room. Миграция версий базы данных
- Урок 13. Room. Тестирование
- Урок 14. Paging Library. Основы
- Урок 15. Paging Library. PagedList и DataSource. Placeholders.
- Урок 16. Paging Library. LivePagedListBuilder. BoundaryCallback.
- Урок 17. Paging Library. Виды DataSource
- Урок 18. Android Data Binding. Основы
- Урок 19. Android Data Binding. Код в layout. Доступ к View
- Урок 20. Android Data Binding. Обработка событий
- Урок 21. Android Data Binding. Observable поля. Двусторонний биндинг.
- Урок 22. Android Data Binding. Adapter. Conversion.
- Урок 23. Android Data Binding. Использование с include, ViewStub и RecyclerView.
- Урок 24. Navigation Architecture Component. Введение
- Урок 25. Navigation. Передача данных. Type-safe аргументы.
- Урок 26. Navigation. Параметры навигации
- Урок 27. Navigation. NavigationUI.
- Урок 28. Navigation. Вложенный граф. Global Action. Deep Link.
- Урок 29. WorkManager. Введение
- Урок 30. WorkManager. Критерии запуска задачи.
- Урок 31. WorkManager. Последовательность выполнения задач.
- Урок 32. WorkManager. Передача и получение данных
- Урок 33. Практика. О чем это будет.
- Урок 34. Практика. TodoApp. Список задач.
- Урок 35. Практика. TodoApp. Просмотр задачи
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.
Присоединяйтесь к нам в Telegram:
- в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.
- в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Compose, Kotlin, RxJava, Dagger, Тестирование, Performance
- ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня