Статьи

Компоненты архитектуры Android: LiveData

Мы уже рассмотрели много вопросов в нашей серии Компоненты для архитектуры Android. Мы начали говорить об идее новой архитектуры и рассмотрели ключевые компоненты, представленные в Google I / O. Во втором посте мы начали наше глубокое исследование основных компонентов пакета , внимательно LiveModel компоненты Lifecycle и LiveModel . В этом посте мы продолжим исследовать компоненты архитектуры, на этот раз анализируя LiveData компонент LiveData .

Я предполагаю, что вы знакомы с концепциями и компонентами, описанными в последних руководствах, такими как Lifecycle , LifecycleOwner и LifecycleObserver . Если нет, взгляните на первый пост в этой серии , где я обсуждаю общую идею новой архитектуры Android и ее компонентов.

Мы продолжим работу над примером приложения, которое мы начали в последней части этой серии. Вы можете найти его в репозитории GitHub .

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

Компонент LiveData имеет много интересных характеристик:

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

Компонент LiveData отправляет обновления данных только тогда, когда его наблюдатель «активен». При наблюдении LifecycleOwner компонент LiveData считает, что наблюдатель активен, только когда его Lifecycle находится в состоянии STARTED или RESUMED , в противном случае он будет считать наблюдателя неактивным. Во время неактивного состояния наблюдателя LiveData останавливает поток обновления данных, пока его наблюдатель снова не станет активным. Если наблюдатель уничтожен, 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 )

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 )
    }
}

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 .

В конце концов, вам нужно изменить 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 )
}

Transformations.switchMap очень похож на Transformations.map , но в результате он должен возвращать объект LiveData . Его немного сложнее в использовании, но он позволяет создавать мощные цепные реакции.

В нашем проекте мы использовали Transformations.switchMap чтобы создать реакцию между LocationLiveData и ApiResponse<WeatherResponse> .

  1. Наша Transformation.switchMap наблюдает за изменениями LocationLiveData .
  2. Обновленное значение LocationLiveData используется для вызова MainRepository чтобы получить погоду для указанного местоположения.
  3. Хранилище вызывает OpenWeatherService который в результате выдает LiveData<ApiResponse<WeatherResponse>> .
  4. Затем возвращенные 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 выполняются в основном потоке.

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> .

Мы почти завершили исследование компонентов архитектуры Android. К настоящему времени вы должны понимать достаточно, чтобы создать несколько мощных приложений. В следующем посте мы рассмотрим Room , ORM, который оборачивает SQLite и может давать результаты LiveData . Компонент Room идеально вписывается в эту архитектуру, и это последний кусок головоломки.

До скорого! А пока, посмотрите другие наши посты о разработке приложений для Android!

  • Android SDK
    Как использовать API Google Cloud Vision в приложениях для Android
    Ашраф Хатхибелагал
  • Джава
    Шаблоны дизайна Android: шаблон наблюдателя
    Чике Мгбемена
  • Котлин
    Kotlin From Scratch: переменные, базовые типы и массивы
    Чике Мгбемена
  • Котлин
    Kotlin From Scratch: обнуляемость, циклы и условия
    Чике Мгбемена
  • Android SDK
    Что такое Android Instant Apps?
    Джессика Торнсби