Android Architecture Components. Часть 3. LiveData

    image

    Компонент LiveData — предназначен для хранения объекта и разрешает подписаться на его изменения. Ключевой особенностью является то, что компонент осведомлен о жизненном цикле и позволяет не беспокоится о том, на каком этапе сейчас находиться подписчик, в случае уничтожения подписчика, компонент отпишет его от себя. Для того, чтобы LiveData учитывала жизненный цикл используется компонент Lifecycle, но также есть возможность использовать без привязки к жизненному циклу.

    Сам компонент состоит из классов: LiveData, MutableLiveData, MediatorLiveData, LiveDataReactiveStreams, Transformations и интерфейса: Observer.

    Класс LiveData, являет собой абстрактный дженериковый класс и инкапсулирует всю логику работы компонента. Соответственно для создания нашего LiveData холдера, необходимо наследовать этот класс, в качестве типизации указать тип, который мы планируем в нем хранить, а также описать логику обновления хранимого объекта.

    Для обновления значения мы должны передать его с помощью метода setValue(T), будьте внимательны поскольку этот метод нужно вызывать с main треда, в противном случае мы получим IllegalStateException, если же нам нужно передать значение из другого потока можно использовать postValue(T), этот метод в свою очередь обновит значение в main треде. Интересной особенностью postValue(T) является еще то, что он в случае множественного вызова, не будет создавать очередь вызовов на main тред, а при исполнении кода в main треде возьмет последнее полученное им значение. Также, в классе присутствует два колбека:

    onActive() — будет вызван когда количество подписчиков изменит свое значение с 0 на 1.
    onInactive() — будет вызван когда количество подписчиков изменит свое значение с 1 на 0.

    Их назначение соответственно уведомить наш класс про то, что нужно обновлять даные или нет. По умолчанию они не имеют реализации, и для обработки этих событий мы должны переопределить эти методы.

    Давайте рассмотрим, как будет выглядеть наш LiveData класс, который будет хранить wife network name и в случае изменения будет его обновлять, для упрощения он реализован как синглтон.

    public class NetworkLiveData extends LiveData<String> {
      private Context context;
      private BroadcastReceiver broadcastReceiver;
      private static NetworkLiveData instance;
    
      public static NetworkLiveData getInstance(Context context){
          if (instance==null){
              instance = new NetworkLiveData(context.getApplicationContext());
          }
          return instance;
      }
    
      private NetworkLiveData(Context context) {
          this.context = context;
      }
    
      private void prepareReceiver(Context context) {
          IntentFilter filter = new IntentFilter();
          filter.addAction("android.net.wifi.supplicant.CONNECTION_CHANGE");
          filter.addAction("android.net.wifi.STATE_CHANGE");
          broadcastReceiver = new BroadcastReceiver() {
              @Override
              public void onReceive(Context context, Intent intent) {
                  WifiManager wifiMgr = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
                  WifiInfo wifiInfo = wifiMgr.getConnectionInfo();
                  String name = wifiInfo.getSSID();
                  if (name.isEmpty()) {
                      setValue(null);
                  } else {
                      setValue(name);
                  }
              }
          };
          context.registerReceiver(broadcastReceiver, filter);
      }
    
      @Override
      protected void onActive() {
          prepareReceiver(context);
      }
    
      @Override
      protected void onInactive() {
          context.unregisterReceiver(broadcastReceiver);
          broadcastReceiver = null;
      }
    }

    В целом логика фрагмента следующая, если кто-то подписывается, мы инициализируем BroadcastReceiver, который будет нас уведомлять об изменении сети, после того как отписывается последний подписчик мы перестаем отслеживать изменения сети.

    Для того чтобы добавить подписчика есть два метода: observe(LifecycleOwner, Observer<T>) — для добавления подписчика с учетом жизненного цикла и observeForever(Observer<T>) — без учета. Уведомления об изменении данных приходят с помощью реализации интерфейса Observer, который имеет один метод onChanged(T).

    Выглядит это приблизительно так:

    public class MainActivity extends LifecycleActivity implements Observer<String> {
      private TextView networkName;
    
      @Override
      protected void onCreate(Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          setContentView(R.layout.activity_main);
          networkName = (TextView) findViewById(R.id.network_name);
          NetworkLiveData.getInstance(this).observe(this,this);
          //NetworkLiveData.getInstance(this).observeForever(this);
      }
    
      @Override
      public void onChanged(@Nullable String s) {
          networkName.setText(s);
      }
    }

    Примечание: Этот фрагмент только для примера,  не используйте этот код в реальном проекте. Для работы с LiveData лучше использовать ViewModel(про этот компонент в следующей статье) или позаботиться про отписку обсервера вручную.
    В случае использования observe(this,this) при повороте экрана мы будем каждый раз отписываться от нашего компонента и заново подписываться. А в случае использование observeForever(this) мы получим memory leak.


    Помимо вышеупомянутых методов в api LiveData также входит getValue(), hasActiveObservers(), hasObservers(), removeObserver(Observer<T> observer), removeObservers(LifecycleOwner owner) в дополнительных комментариях не нуждаются.

    Класс MutableLiveData, является расширением LiveData, с отличием в том что это не абстрактный класс и методы setValue(T) и postValue(T) выведены в api, то есть публичные.
    По факту класс является хелпером для тех случаев когда мы не хотим помещать логику обновления значения в LiveData, а лишь хотим использовать его как Holder.

    void update(String someText){
          ourMutableLiveData.setValue(String);
    }
    

    Класс MediatorLiveData, как понятно из названия это реализация паттерна медиатор, на всякий случай напомню: поведенческий паттерн, определяет объект, инкапсулирующий способ взаимодействия множества объектов,  избавляя их от необходимости явно ссылаться друг на друга. Сам же класс расширяет MutableLiveData и добавляет к его API два метода: addSource(LiveData<T>, Observer<T>)  и removeSource(LiveData<T>). Принцип работы с классом заключается в том что мы не подписываемся на конкретный источник, а на наш MediatorLiveData, а источники добавляем с помощью addSource(..). MediatorLiveData в свою очередь сам управляет подпиской на источники.  

    Для примера создадим еще один класс LiveData, который будет хранить название нашей мобильной сети:

    public class MobileNetworkLiveData extends LiveData<String> {
      private static MobileNetworkLiveData instance;
      private Context context;
    
      private MobileNetworkLiveData(Context context) {
          this.context = context;
      }
    
      private MobileNetworkLiveData() {
    
      }
    
      @Override
      protected void onActive() {
          TelephonyManager telephonyManager = (TelephonyManager) context
                  .getSystemService(Context.TELEPHONY_SERVICE);
          String networkOperator = telephonyManager.getNetworkOperatorName();
          setValue(networkOperator);
      }
    
      public static MobileNetworkLiveData getInstance(Context context) {
          if (instance == null) {
              instance = new MobileNetworkLiveData(context);
          }
          return instance;
      }
    
    }

    И перепишем наше приложение так чтоб оно отображало название wifi сети, а если подключения к wifi нет, тогда название мобильной сети, для этого изменим MainActivity:

    public class MainActivity extends LifecycleActivity implements Observer<String> {
      private MediatorLiveData<String> mediatorLiveData;
      private TextView networkName;
    
    
      @Override
      protected void onCreate(Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          setContentView(R.layout.activity_main);
          networkName = (TextView) findViewById(R.id.network_name);
          mediatorLiveData = new MediatorLiveData<>();
          init();
      }
    
    
      private void init() {
          final LiveData<String> network = NetworkLiveData.getInstance(this);
          final LiveData<String> mobileNetwork = MobileNetworkLiveData.getInstance(this);
          Observer<String> networkObserver = new Observer<String>() {
              @Override
              public void onChanged(@Nullable String s) {
                  if (!TextUtils.isEmpty(s))
                      mediatorLiveData.setValue(s);
                  else
                      mediatorLiveData.setValue(mobileNetwork.getValue());
              }
          };
          Observer<String> mobileNetworkObserver = new Observer<String>() {
              @Override
              public void onChanged(@Nullable String s) {
                      if (TextUtils.isEmpty(network.getValue())){
                          mediatorLiveData.setValue(s);
                      }
              }
          };
          mediatorLiveData.addSource(network, networkObserver);
          mediatorLiveData.addSource(mobileNetwork,mobileNetworkObserver);
          mediatorLiveData.observe(this, this);
      }
    
    
      @Override
      public void onChanged(@Nullable String s) {
          networkName.setText(s);
      }
    }
    

    Как мы можем заметить, теперь наш UI подписан на MediatorLiveData и абстрагирован от конкретного источника данных. Стоит обратить внимание на то что значения в нашем медиаторе не зависят напрямую от источников и устанавливать его нужно в ручную.

    Класс LiveDataReactiveStreams, название ввело меня в заблуждение поначалу, подумал что это расширение LiveData с помощью RX, по факту же, класс являет собой адаптер с двумя static методами: fromPublisher(Publisher<T> publisher), который возвращает объект LiveData<T> и toPublisher(LifecycleOwner lifecycle, LiveData<T> liveData), который возвращает объект Publisher<T>. Для использования этого класса, его нужно импортировать отдельно:
    compile «android.arch.lifecycle:reactivestreams:$version»

    Класс Transformations, являет собой хелпер для смены типизации LiveData, имеет два static метода:
    map(LiveData<T>, Function<T,P>)  - применяет в main треде реализацию интерфейса Function и возвращает объект LiveData<P>, где T — это типизация входящей LiveData, а P желаемая типизация исходящей, по факту же каждый раз когда будет происходить изменения в входящей LiveData она будет нотифицировать нашу исходящую, а та в свою очередь будет нотифицировать подписчиков после того как переконвертирует тип с помощью нашей реализации Function. Весь этот механизм работает за счет того что по факту исходящая LiveData, является MediatiorLiveData.

    
    LiveData<Location> location = ...;
    
    LiveData<String> locationString = Transformations.map(location, new Function<Location, String>() {
      @Override
      public String apply(Location input) {
          return input.toString;
      }
    });

    switchMap(LiveData<T>, Function<T, LiveData<P>>)  - похож к методу map с отличием в том, что вместо смены типа в функции мы возвращаем сформированный объект LiveData.

    LiveData<Location> location = ...;
    LiveData<Place> getPlace(Location location) = ...;
    
    LiveData<Place> userName = Transformations.switchMap(location, new Function<Location, LiveData<Place>>() {
      @Override
      public LiveData<Place> apply(Location input) {
          return getPlace(input);
      }
    });
    

    Базовый пример можно посмотреть в репозитории: git    

    Также полезные ссылки: раз и два.

    Android Architecture Components. Часть 1. Введение
    Android Architecture Components. Часть 2. Lifecycle
    Android Architecture Components. Часть 3. LiveData
    Android Architecture Components. Часть 4. ViewModel
    • +13
    • 24k
    • 6
    Share post

    Similar posts

    Comments 6

      +2
      Отличная идея в статье про мобильную разработку, приклеить титульную бесполезную картинку на 5Мб.

      Класс виден сразу, молодцы!
        +1
        Исправлено, текущий размер 155кб.
        Спасибо за отзыв
        +1
        Хорошая статья! С нетерпением жду следующую часть :)
          +2
          Если не сложно — можете добавить в статьи оглавление по текущим статьям?
            +1
            Готово
              +1
              Спасибо большое — такой формат очень удобен!

          Only users with full accounts can post comments. Log in, please.