Компонент 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
Также полезные ссылки:
https://developer.android.com/topic/libraries/architecture/livedata.html
https://developer.android.com/reference/android/arch/lifecycle/LiveData.html
Автор: Юрий