Pull to refresh

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

Development of mobile applications *Development for Android *
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
Tags:
Hubs:
Total votes 13: ↑13 and ↓0 +13
Views 47K
Comments Comments 6