Мы уже рассмотрели много вопросов в нашей серии Компоненты для архитектуры Android. Мы начали говорить об идее новой архитектуры и рассмотрели ключевые компоненты, представленные в Google I / O. Во втором посте мы начали наше глубокое исследование основных компонентов пакета , внимательно LiveModel
компоненты Lifecycle
и LiveModel
. В этом посте мы продолжим исследовать компоненты архитектуры, на этот раз анализируя LiveData
компонент LiveData
.
Я предполагаю, что вы знакомы с концепциями и компонентами, описанными в последних руководствах, такими как Lifecycle
, LifecycleOwner
и LifecycleObserver
. Если нет, взгляните на первый пост в этой серии , где я обсуждаю общую идею новой архитектуры Android и ее компонентов.
Мы продолжим работу над примером приложения, которое мы начали в последней части этой серии. Вы можете найти его в репозитории GitHub .
1. Компонент LiveData
LiveData
является держателем данных. Его можно наблюдать, он может содержать любые данные, и, кроме того, он также поддерживает жизненный цикл . С практической точки зрения LiveData
может быть настроен на отправку обновлений данных только тогда, когда активен наблюдатель. Благодаря своей осведомленности о LifecycleOwner
LiveData
компонент LiveData
, когда его наблюдает LifecycleOwner
будет отправлять обновления только тогда, когда Lifecycle
наблюдателя все еще активен, и удаляет наблюдаемую связь после разрушения Lifecycle
наблюдателя.
Компонент LiveData
имеет много интересных характеристик:
- предотвращает утечки памяти, когда наблюдатель привязан к
Lifecycle
- предотвращает сбои из-за прекращения деятельности
- данные всегда актуальны
- обрабатывает изменения конфигурации плавно
- позволяет делиться ресурсами
- автоматически обрабатывает жизненный цикл
2. Наблюдение LiveData
Компонент LiveData
отправляет обновления данных только тогда, когда его наблюдатель «активен». При наблюдении LifecycleOwner
компонент LiveData
считает, что наблюдатель активен, только когда его Lifecycle
находится в состоянии STARTED
или RESUMED
, в противном случае он будет считать наблюдателя неактивным. Во время неактивного состояния наблюдателя LiveData
останавливает поток обновления данных, пока его наблюдатель снова не станет активным. Если наблюдатель уничтожен, LiveData
удалит его ссылку на наблюдателя.
Чтобы добиться такого поведения, LiveData
создает тесную связь с Lifecycle
наблюдателя, когда наблюдается LifecycleOwner
. Эта емкость позволяет избежать утечек памяти при просмотре LiveData
. Однако, если процесс наблюдения вызывается без LifecycleOwner
, компонент LiveData
не будет реагировать на состояние Lifecycle
, и статус наблюдателя должен обрабатываться вручную.
Чтобы наблюдать LiveData
, вызовите observe(LifecycleOwner, Observer<T>)
или observeForever(Observer<T>)
.
-
observe(LifecycleOwner, Observer<T>)
: это стандартный способ наблюденияLiveData
. Он привязывает наблюдателя кLifecycle
LiveData
, изменяя активное и неактивное состояниеLiveData
соответствии с текущим состояниемLifecycleOwner
. -
observeForever(Observer<T>)
: этот метод не используетLifecycleOwner
, поэтомуLiveData
не сможет отвечать на событияLifecycle
. При использовании этого метода очень важно вызыватьremoveObserver(Observer<T>)
, в противном случае наблюдатель может не собирать мусор, что приводит к утечке памяти.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// called from a LifecycleOwner
location.observe(
// LifecycleOwner
this,
// creating an observer
Observer {
location ->
info(«location: ${location!!.latitude},
${location.longitude}»)
})
}
// Observing without LifecycleOwner
val observer = Observer {
location ->
info(«location: ${location!!.latitude},
${location.longitude}»)
})
location.observeForever(observer)
// when observer without a LivecyleOwner
// it is necessary to remove the observers at some point
location.removeObserver( observer )
|
3. Реализация LiveData
LiveData<T>
тип в классе LiveData<T>
определяет тип данных, которые будут храниться. Например, LiveData<Location>
содержит данные о Location
. Или LiveData<String>
содержит String
.
В реализации компонента необходимо учитывать два основных метода: onActive()
и onInactive()
. Оба метода реагируют на состояние наблюдателя.
Пример реализации
В нашем примере проекта мы использовали много объектов LiveData
, но мы реализовали только один: LocationLiveData
. Класс имеет дело с GPS Location
, передавая текущую позицию только для активного наблюдателя. Обратите внимание, что класс обновляет свое значение в методе onLocationChanged
, передавая текущему активному наблюдателю обновленные данные Location
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
class LocationLiveData
@Inject
constructor(
context: Context
) : LiveData<Location>(), LocationListener, AnkoLogger {
private val locationManager: LocationManager =
context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
@SuppressLint(«MissingPermission»)
override fun onInactive() {
info(«onInactive»)
locationManager.removeUpdates(this)
}
@SuppressLint(«MissingPermission»)
fun refreshLocation() {
info(«refreshLocation»)
locationManager.requestSingleUpdate(LocationManager.GPS_PROVIDER, this, null )
}
}
|
4. Вспомогательный класс MutableLiveData
MutableLiveData<T>
— это вспомогательный класс, который расширяет LiveData
и предоставляет postValue
и setValue
. Кроме этого, он ведет себя точно так же, как его родитель. Чтобы использовать его, определите тип данных, которые он содержит, например MutableLiveData<String>
для хранения String
, и создайте новый экземпляр.
1
|
val myData: MutableLiveData<String> = MutableLiveData()
|
Чтобы отправить обновления наблюдателю, позвоните postValue
или setValue
. Поведение этих методов очень похоже; однако setValue
будет непосредственно устанавливать новое значение и может вызываться только из основного потока, в то время как postValue
создает новую задачу в основном потоке для установки нового значения и может вызываться из фонового потока.
1
2
3
4
5
6
7
8
|
fun updateData() {
// must be called from the main thread
myData.value = api.getUpdate
}
fun updateDataFromBG(){
// may be called from bg thread
myData.postValue(api.getUpdate)
}
|
Важно учитывать, что поскольку метод postValue
создает новую Task
и публикует сообщения в главном потоке, он будет медленнее, чем прямые вызовы setValue
.
5. Преобразования LiveData
В конце концов, вам нужно изменить LiveData
и LiveData
его новое значение наблюдателю. Или, может быть, вам нужно создать цепную реакцию между двумя объектами LiveData
, чтобы один реагировал на изменения другого. Чтобы справиться с обеими ситуациями, вы можете использовать класс Transformations
.
Преобразования карты
Transformations.map
применяет функцию к экземпляру LiveData
и отправляет результат его наблюдателям, давая вам возможность манипулировать значением данных.
Это очень легко реализовать Transformations.map
. Все, что вам нужно сделать, это предоставить наблюдаемые LiveData
и Function
которая будет вызываться при изменении наблюдаемых LiveData
, помня, что Function
должна возвращать новое значение преобразованных LiveData
.
Предположим, что у вас есть LiveData
который должен вызывать API при изменении значения String
, например поля поиска.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
// LiveData that calls api
// when ‘searchLive’ changes its value
val apiLive: LiveData<Result> = Transformations.map(
searchLive,
{
query -> return@map api.call(query)
}
)
// Every time that ‘searchLive’ have
// its value updated, it will call
// ‘apiLive’ Transformation.map
fun updateSearch( query: String ) {
searchLive.postValue( query )
}
|
Преобразования SwitchMap
Transformations.switchMap
очень похож на Transformations.map
, но в результате он должен возвращать объект LiveData
. Его немного сложнее в использовании, но он позволяет создавать мощные цепные реакции.
В нашем проекте мы использовали Transformations.switchMap
чтобы создать реакцию между LocationLiveData
и ApiResponse<WeatherResponse>
.
- Наша
Transformation.switchMap
наблюдает за изменениямиLocationLiveData
. - Обновленное значение
LocationLiveData
используется для вызоваMainRepository
чтобы получить погоду для указанного местоположения. - Хранилище вызывает
OpenWeatherService
который в результате выдаетLiveData<ApiResponse<WeatherResponse>>
. - Затем возвращенные
LiveData
отслеживаютсяMediatorLiveData
, который отвечает за изменение полученного значения и обновление погоды, отображаемой в слое вида.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
class MainViewModel
@Inject
constructor(
private val repository: MainRepository
)
: ViewModel(), AnkoLogger {
// Location
private val location: LocationLiveData =
repository.locationLiveDa()
private var weatherByLocationResponse:
LiveData<ApiResponse<WeatherResponse>> =
Transformations.switchMap(
location,
{
l ->
info(«weatherByLocation: \nlocation: $l»)
return@switchMap repository.getWeatherByLocation(l)
}
)
}
|
Однако LiveData
трудоемких операций в ваших преобразованиях LiveData
. В приведенном выше коде оба метода Transformations
выполняются в основном потоке.
6. MediatorLiveData
MediatorLiveData
— это более продвинутый тип LiveData
. Он обладает возможностями, очень похожими на возможности класса Transformations
: он способен реагировать на другие объекты LiveData
, вызывая Function
при изменении наблюдаемых данных. Тем не менее, он имеет много преимуществ по сравнению с Transformations
, так как он не должен запускаться в основном потоке и может наблюдать несколько LiveData
одновременно.
Чтобы наблюдать LiveData
, вызовите addSource(LiveData<S>, Observer<S>)
, чтобы наблюдатель реагировал на метод onChanged
из данной LiveData
. Чтобы остановить наблюдение, вызовите removeSource(LiveData<S>)
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
val mediatorData: MediatorLiveData<String> = MediatorLiveData()
mediatorData.addSource(
dataA,
{
value ->
// react to value
info(«my value $value»)
}
)
mediatorData.addSource(
dataB,
{
value ->
// react to value
info(«my value $value»)
// we can remove the source once used
mediatorData.removeSource(dataB)
}
)
|
В нашем проекте данные, наблюдаемые слоем вида, который содержит выставляемую погоду, представляют собой MediatorLiveData
. Компонент наблюдает за двумя другими объектами LiveData
: weatherByLocationResponse
, который получает обновления погоды по местоположению, и weatherByCityResponse
, который получает обновления погоды по названию города. Каждый раз, когда эти объекты обновляются, weatherByCityResponse
будет обновлять слой представления с текущей запрошенной погодой.
В MainViewModel
мы наблюдаем LiveData
и предоставляем объект weather
для просмотра.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
|
class MainViewModel
@Inject
constructor(
private val repository: MainRepository
)
: ViewModel(), AnkoLogger {
// …
// Value observed by View.
// It transform a WeatherResponse to a WeatherMain.
private val weather:
MediatorLiveData<ApiResponse<WeatherMain>> =
MediatorLiveData()
// retrieve weather LiveData
fun getWeather(): LiveData<ApiResponse<WeatherMain>> {
info(«getWeather»)
return weather
}
private fun addWeatherSources(){
info(«addWeatherSources»)
weather.addSource(
weatherByCityResponse,
{
w ->
info(«addWeatherSources: \nweather: ${w!!.data!!}»)
updateWeather(w.data!!)
}
)
weather.addSource(
weatherByLocationResponse,
{
w ->
info(«addWeatherSources: weatherByLocationResponse: \n${w!!.data!!}»)
updateWeather(w.data!!)
}
)
}
private fun updateWeather(w: WeatherResponse){
info(«updateWeather»)
// getting weather from today
val weatherMain = WeatherMain.factory(w)
// save on shared preferences
repository.saveWeatherMainOnPrefs(weatherMain)
// update weather value
weather.postValue(ApiResponse(data = weatherMain))
}
init {
// …
addWeatherSources()
}
}
|
В MainActivity
наблюдается погода и ее результат показывается пользователю.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
private fun initModel() {
// Get ViewModel
viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(MainViewModel::class.java)
if (viewModel != null) {
// observe weather
viewModel!!.getWeather().observe(
this@MainActivity,
Observer {
r ->
if ( r != null ) {
info(«Weather received on MainActivity:\n $r»)
if (!r.hasError()) {
// Doesn’t have any errors
info(«weather: ${r.data}»)
if (r.data != null)
setUI(r.data)
} else {
// error
error(«error: ${r.error}»)
isLoading(false)
if (r.error!!.statusCode != 0) {
if (r.error!!.message != null)
toast(r.error.message!!)
else
toast(«An error occurred»)
}
}
}
}
)
}
}
|
MediatorLiveData
также использовалась в качестве основы для объекта, который обрабатывает вызовы API OpenWeatherMap. Посмотрите на эту реализацию; он более продвинутый, чем приведенный выше, и его действительно стоит изучить. Если вам интересно, взгляните на OpenWeatherService
, обратив особое внимание на класс Mediator<T>
.
7. Заключение
Мы почти завершили исследование компонентов архитектуры Android. К настоящему времени вы должны понимать достаточно, чтобы создать несколько мощных приложений. В следующем посте мы рассмотрим Room
, ORM, который оборачивает SQLite
и может давать результаты LiveData
. Компонент Room
идеально вписывается в эту архитектуру, и это последний кусок головоломки.
До скорого! А пока, посмотрите другие наши посты о разработке приложений для Android!