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 (спостерігач). Це сховище вміє робити дві речі:

  1. У нього можна помістити будь-який об'єкт
  2. На нього можна підписатися й отримувати об'єкти, які в нього поміщають.

Тобто з одного боку, хтось поміщає об'єкт у сховище, а з іншого боку, хтось підписується і отримує цей об'єкт.

Як аналогію можна навести, наприклад, канали в 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