Android Architecture Components. Часть 3. LiveData
Компонент LiveData предназначен для хранения объекта и разрешает подписаться на его изменения. Ключевой особенностью является то, что компонент осведомлен о жизненном цикле и позволяет не беспокоится о том, на каком этапе сейчас находиться подписчик, в случае уничтожения подписчика компонент отпишет его от себя. Для того чтобы LiveData учитывала жизненный цикл, используется компонент Lifecycle, но также есть возможность использовать LiveData без привязки к жизненному циклу.
Примечание: данный цикл статей был написан летом 2017 года, поэтому некоторая информация может быть немного устаревшей, но общая концепция архитектурных компонентов с тех пор не изменилась.
Сам компонент состоит из классов LiveData, MutableLiveData, MediatorLiveData, LiveDataReactiveStreams, Transformations и интерфейса Observer.
Класс LiveData является абстрактным дженериковым классом и инкапсулирует всю логику работы компонента. Соответственно, для создания нашего LiveData холдера необходимо наследовать этот класс, в качестве типа-дженерика указать тип, который мы планируем в нем хранить, а также описать логику обновления хранимого объекта.
Для обновления значения мы должны передать новое значение с помощью метода setValue(T)
. Будьте внимательны, поскольку этот метод нужно вызывать с главного потока, в противном случае мы получим IllegalStateException
. Если же нам нужно передать значение из другого потока, можно использовать postValue(T)
, этот метод в свою очередь обновит значение в main треде. Интересной особенностью postValue(T)
является еще то, что он в случае множественного вызова не будет создавать очередь вызовов на главный поток, а при исполнении кода в главном потоке возьмет последнее полученное им значение. Также в классе присутствует два колбека:
onActive()
— будет вызван, когда количество подписчиков изменит свое значение с 0 на 1;
onInactive()
— будет вызван, когда количество подписчиков изменит свое значение с 1 на 0.
Их назначение – уведомить наш класс о том, нужно ли обновлять данные или нет. По умолчанию они не имеют реализации, для обработки этих событий мы должны переопределить эти методы.
Давайте рассмотрим, как будет выглядеть наш LiveData класс, который будет хранить wife network name и в случае изменения будет его обновлять.
class NetworkLiveData( private val context: Context ) : LiveData<String>() { private var broadcastReceiver: BroadcastReceiver? = null private fun prepareReceiver(context: Context) { val filter = IntentFilter() filter.addAction("android.net.wifi.supplicant.CONNECTIONCHANGE") filter.addAction("android.net.wifi.STATECHANGE") broadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val wifiMgr = context.getSystemService(Context.WIFI_SERVICE) as WifiManager val wifiInfo = wifiMgr.getConnectionInfo() val name = wifiInfo.getSSID() if (name.isEmpty()) { setValue(null) } else { setValue(name) } } } context.registerReceiver(broadcastReceiver, filter) } override fun onActive() { prepareReceiver(context) } override fun onInactive() { context.unregisterReceiver(broadcastReceiver) broadcastReceiver = null } }
В целом логика фрагмента следующая. Если кто-то подписывается, мы инициализируем BroadcastReceiver, который будет нас уведомлять об изменении сети. После того как отписывается последний подписчик, мы перестаем отслеживать изменения сети.
Для того чтобы добавить подписчика, есть два метода: observe(LifecycleOwner, Observer<T>)
— для добавления подписчика с учетом жизненного цикла и observeForever(Observer<T>)
— без учета. Уведомления об изменении данных приходят с помощью реализации интерфейса Observer
, который имеет один метод onChanged(T)
.
Выглядит это приблизительно так:
class MainActivity : LifecycleActivity(), Observer<String> { private lateinit var networkName: TextView override fun onCreate(savedInstanceState: Bundle) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) networkName = findViewById(R.id.network_name) NetworkLiveData.getInstance(this).observe(this, this) //NetworkLiveData.getInstance(this).observeForever(this) } override fun onChanged(s: String?) { networkName.setText(s) } }
Примечание: Этот фрагмент только для примера, не используйте этот код в реальном проекте. Для работы с LiveData лучше использовать ViewModel (про этот компонент расскажу в следующей статье) или позаботиться об отписке обсервера вручную.
В случае использования observe(this, this)
при повороте экрана мы будем каждый раз отписываться от нашего компонента и заново подписываться. А в случае использование observeForever(this)
мы получим memory leak.
Помимо вышеупомянутых методов в api LiveData также входят методы getValue()
, hasActiveObservers()
, hasObservers()
, removeObserver(Observer<T>)
, removeObservers(LifecycleOwner)
. Их назначение в дополнительных комментариях не нуждается.
Класс MutableLiveData является расширением LiveData с отличием в том, что это не абстрактный класс и методы setValue(T)
и postValue(T)
выведены в api, то есть являются публичными.
По факту класс является хелпером для тех случаев, когда мы не хотим помещать логику обновления значения в LiveData, а лишь хотим использовать его как Holder.
fun update(someText: String) { ourMutableLiveData.setValue(String) }
Класс MediatorLiveData, как понятно из названия, это реализация паттерна медиатор. На всякий случай напомню: поведенческий паттерн, определяет объект, инкапсулирующий способ взаимодействия множества объектов, избавляя их от необходимости явно ссылаться друг на друга. Сам же класс расширяет MutableLiveData и добавляет к его API два метода: addSource(LiveData<T>, Observer<T>)
и removeSource(LiveData<T>)
. Принцип работы с классом заключается в том, что мы не подписываемся на конкретный источник, а на наш MediatorLiveData, а источники добавляем с помощью addSource(...)
. MediatorLiveData, в свою очередь, сам управляет подпиской на источники.
Для примера создадим еще один класс LiveData, который будет хранить название нашей мобильной сети:
class MobileNetworkLiveData( private val context: Context ) : LiveData<String> { override fun onActive() { val telephonyManager = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager val networkOperator = telephonyManager.getNetworkOperatorName() setValue(networkOperator) } }
И перепишем наше приложение так, чтобы оно отображало название wifi сети, а если подключения к wifi нет, тогда название мобильной сети. Для этого изменим MainActivity:
class MainActivity : LifecycleActivity(), Observer<String> { private lateinit var mediatorLiveData: MediatorLiveData<String> private lateinit var networkName: TextView override fun onCreate(savedInstanceState: Bundle) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) networkName = findViewById(R.id.network_name) mediatorLiveData = MediatorLiveData<String>() init() } private fun init() { val network = NetworkLiveData(this) val mobileNetwork = MobileNetworkLiveData(this) val networkObserver = object : Observer<String>() { override fun onChanged(s: String?) { if (!TextUtils.isEmpty(s)) mediatorLiveData.setValue(s) else mediatorLiveData.setValue(mobileNetwork.getValue()) } } val mobileNetworkObserver = object : Observer<String>() { override fun onChanged(s: String) { if (TextUtils.isEmpty(network.getValue())) { mediatorLiveData.setValue(s) } } } mediatorLiveData.addSource(network, networkObserver) mediatorLiveData.addSource(mobileNetwork,mobileNetworkObserver) mediatorLiveData.observe(this, this) } override fun onChanged(s: String?) { networkName.setText(s) } }
Как мы можем заметить, теперь наш UI подписан на MediatorLiveData и абстрагирован от конкретного источника данных. Стоит обратить внимание на то, что значения в нашем медиаторе не зависят напрямую от источников и устанавливать его нужно вручную.
Класс LiveDataReactiveStreams. Название ввело меня в заблуждение поначалу – подумал, что это расширение LiveData с помощью RX, по факту же класс являет собой адаптер с двумя static методами: fromPublisher(Publisher<T>)
, который возвращает объект LiveData<T>, и toPublisher(LifecycleOwner, LiveData<T>)
, который возвращает объект Publisher<T>. Для использования этого класса его нужно импортировать отдельно:
compile «android.arch.lifecycle:reactivestreams:$version»
Класс Transformations является хелпером для смены типизации LiveData и имеет два static метода:
map(LiveData<T>, Function<T, P>)
— применяет в главном потоке реализацию интерфейса Function и возвращает объект LiveData<P>, где T — это типизация входящей LiveData, а P — желаемая типизация исходящей. По факту же, каждый раз когда будет происходить изменение во входящей LiveData, она будет нотифицировать нашу исходящую, а та, в свою очередь, будет нотифицировать подписчиков, после того как переконвертирует тип с помощью нашей реализации Function. Весь этот механизм работает за счет того, что по факту исходящая LiveData является MediatiorLiveData.
val location: LiveData<Location> = ... val locationString: LiveData<String> = Transformations.map(location) { input -> input.toString() }
switchMap(LiveData<T>, Function<T, LiveData<P>>)
— похож на метод map
с отличием в том, что вместо смены объекта в функции мы возвращаем сформированный объект LiveData.
val location: LiveData<Location> = ... fun getPlace(location: Location): LiveData<Place> {...} val userName: LiveData<Place> = Transformations.switchMap(location) { input -> getPlace(input) }
Базовый пример можно посмотреть в репозитории на github.
Также полезные ссылки: раз и два.
Android Architecture Components. Часть 1. Введение
Android Architecture Components. Часть 2. Lifecycle
Источник: Android Architecture Components. Часть 3. LiveData