April 18, 2020

Android Architecture Components. Часть 1. Введение

На Google I/O 2017 был представлен набор библиотек под названием Android Architecture Components. В нескольких словах — это ряд вспомогательных библиотек, которые призваны помочь с такими вещами, как проектирование, тестирование и сопровождение приложений. В целом Android Architecture Components можно разделить на четыре блока: Lifecycle, LiveData, ViewModel и Room Persistence. В этой части мы кратко рассмотрим каждый из них, в следующих частях данного цикла статей мы рассмотрим их более подробно.

Примечание: данный цикл статей был написан летом 2017 года, поэтому некоторая информация может быть немного устаревшей, но общая концепция архитектурных компонентов с тех пор не изменилась.

Lifecycle

Компонент Lifecycle призван упростить работу с жизненным циклом. Выделены основные понятия, такие как LifecycleOwner и LifecycleObserver.

LifecycleOwner – это интерфейс с одним методом getLifecycle(), который возвращает состояние жизненного цикла. Являет собой абстракцию владельца жизненного цикла (Activity, Fragment).

LifecycleObserver – интерфейс, обозначает слушателя жизненного цикла owner-а. Не имеет методов, завязан на OnLifecycleEvent, который, в свою очередь, разрешает отслеживать жизненный цикл.

Что это нам дает?

Назначение этого компонента – избавить разработчика от написания рутинного кода и сделать его более читаемым. Довольно частая ситуация, когда в нашем приложении работает ряд процессов, которые зависят от этапа жизненного цикла. Будь то воспроизведение медиа, локация, связь с сервисом и т.д. Как итог, нам приходится вручную отслеживать состояние и уведомлять о нём наш процесс. Что неудобно по двум причинам: захламление основного класса (Activity или Fragment) и снижение модульности, ведь нам нужно позаботиться про поддержку передачи состояния. С помощью же этого компонента мы можем переложить всю ответственность на наш компонент и все, что для этого нужно, – это объявить интересующий наш класс как observer и передать ему в onCreate() методе ссылку на owner. В общем, это выглядит так:

class MyActivity : AppCompatActivity() {

    private lateinit var playerWrapper: PlayerWrapper

    override fun onCreate(…) {
        …
        playerWrapper = PlayerWrapper(this, getLifecycle())
    }
}
class PlayerWrapper(
    context: Context,
    lifecycle: Lifecycle
) : LifecycleObserver {

    private val controller: PlayerController

    init {
        //init controller
    }

    @OnLifecycleEvent(Lifecycle.Event.ONSTART)
    fun start() {
        controller.play()
    }

    @OnLifecycleEvent(Lifecycle.Event.ONPAUSE)
    fun stop() {
        controller.stop()
    }
}

Наш обсервер абсолютно осведомлен о состоянии и может самостоятельно обрабатывать его изменение.

LiveData

Компонент LiveData являет собой holder класс для хранения объекта, а также разрешает подписаться к нему. LiveData знает про жизненный цикл и разрешает от него абстрагироваться.

Для имплементации компонента нам нужно расширить класс LiveData. Для работы нам нужно знать всего три метода из этого класса.

onActive() – этот метод вызывается, когда у нашего экземпляра есть активный(-ые) обсервер(ы). В нем мы должны инициировать интересующий нас сервис или операцию.

onInactive() – вызывается, когда у LiveData нет активных слушателей. Соответственно, нужно остановить наш сервис или операцию.

setValue() – вызываем, если изменились данные и LiveData информирует об этом слушателей.

object ChatLiveDataHolder : LiveData<Message>() {
  
    private val chatManager: ChatManager

    private val сhatListener = object : ChatListener() {
        override fun newMessage(message: Message) {
            setValue(message)
        }
    }

    init {
        //init chatManager and set listener
    }

    override fun onActive() {
         chatManager.start()
    }

    override fun onInactive() {
        chatManager.stop()
    }
}

Если вы обратили внимание, то мы реализовали наш класс как singleton (object). Это дает нам возможность использовать LiveData в других Activity, Fragment и т.д. без переинициализации, если это нам не нужно.

Для того чтобы подписать слушателя, тоже никаких проблем нет. Все, что нужно, – это вызвать метод observe у нашего экземпляра LiveData, передать в него LifeCycleOwner и реализацию интерфейса Observer. Интерфейс Observer имеет всего один метод onChanged(t: T), с помощью него LiveData будет информировать слушателей об изменении в данных (вызов метода setValue(t: T) в LiveData).

Что это нам дает?

Плюсов действительно много. Наприме, защита от memory leaks (Observer связан со своим Lifecycle и автоматически отписывается, когда его Lifecycle уничтожен), защита от остановленной Activity (если Lifecycle не активен (stopped), то и нотификации на Observer не будут отправляться). Из функциональных особенностей – единый доступ к данным (с помощью singleton) и сохранение наших данных (в случае пересоздания активити или фрагмента). В целом же, назначение все то же – избавить разработчика от рутинной работы, связанной с жизненным циклом.

ViewModel

Компонент ViewModel спроектирован для хранения и управления данными, которые связаны с представлением.

Задача же данного компонента – помочь разработчику абстрагировать данные и осуществлять их хранение между пересозданием Activity или Fragment. Если же нам необходимо сохранить небольшой набор данных (item в RadioButtonGroup или же введенные данные), нам отлично подходит Bundle в onSaveInstanceState(). Но если это большой список (список пользователей или товаров, каталог чего-нибудь), нам пришлось бы заново получать этот список. Вот в этом случае ViewModel является нашим основным помощником. Особенностью данного компонента является то, что он привязывается к Activity и автоматически сохраняет свое состояние во время таких операций, как onCofigurationChange().

Класс ViewModel является абстрактным классом, но не имеет абстрактных методов. Для реализации нашего класса нам нужно лишь унаследоваться от ViewModel и описать данные, которые мы хотим хранить, и методы для их получения.

class OurModel extends ViewModel() {

    private var userList: List<User>? = null

    fun getUserList(): List<User>? {
        return userList
    }

    fun setUserList(list: List<User>) {
        this.userList = list
    }
}

И это все, наш холдер для userList готов. Для того чтобы использовать наш холдер, необходимо в методе onCreate(...) в Activity вызвать нашу модель:

override fun onCreate(savedInstanceState: Bundle) {
    ...

    val model = ViewModelProvider(this).get(OurModel::class.java)
    if (model.getUserList() == null) {
        downloadData()
    } else {
        showData()
    }
}

С помощью ViewModelProvider мы получаем instance нашей модели. A c помощью конструкции if смотрим, есть ли у нас уже данные в нашей модели или еще нет.

Что это нам дает?

Так же, как и предыдущие компоненты, этот помогает нам справиться с особенностями и связанными с ними проблемами жизненного цикла Android. В данном случае, это отделение нашей модели представления данных от Activity и обеспечение безопасного механизма хранения этих данных. Также при использовании совместно с LiveData не составляет проблем реализовать асинхронные запросы.

Room Persistence

Компонент Room Persistence является дополнительным уровнем абстракции над SQLite, предлагая более простой и продвинутый способ управления. В целом же мы получили дефолтную ORM.

Этот компонент можно разделить на три части: Entity, DAO (Data Access Object) и Database.

Entity — объектное представление таблицы. С помощью аннотаций можно легко и без лишнего кода описать наши поля.

Для создания нашей Entity нам нужно создать data класс и пометить его аннотацией @Entity. Пример:

@Entity(tableName = «book»)
data class Book(
    @PrimaryKey
    val id: Int,
    val title: String,
    @ColumnInfo(name = «author_id»)
    val authorId: Int,
    ...
}

@PrimaryKey — Для обозначения ключа. @ColumnInfo — для связи поля в таблице.

Установление связей объявляется так же в теле аннотации @Entity:

@Entity(
    foreignKeys = @ForeignKey(
        entity = Other::class,
        parentColumns = ["id"],
        childColumns = ["author_id"]
    )
)

DAO — интерфейс, который описывает методы доступа к БД.

Для реализации создаем интерфейс, который помечаем аннотацией @DAO, и объявляем наши методы. Основные аннотации для методов @Insert, @Update, @Delete и @Query в комментариях не нуждаются. Пример:

@Dao
interface OurDao {

    @Insert
    fun insertBook(book: Book)

    @Update
    fun updateBook(book: Book)

    @Delete
    fun deleteBook(book: Book)

    @Query(«SELECT * FROM book»)
    fun loadAllBooks(): List<Book>
}

В обычном состоянии попытка получить доступ к БД с основного потока закончится Exception. Если вы все же уверены в своих действиях, то можно воспользоваться методом allowMainThreadQueries(). Для асинхронного доступа рекомендовано использовать LiveData или RxJava.

Database используется для создания Database Holder и является точкой доступа к соединению с БД.

Для создания нашего класса нужно наследоваться от RoomDatabase и использовать аннотацию @Database, которой передаем параметры, такие как используемые entity и версию базы. В теле описываем абстрактные методы доступа к нашим DAO.

@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun ourDao(): OurDao
}

Для создания instance для нашей БД используем код:

val db = Room.databaseBuilder(
    applicationContext,
    AppDatabase::class.java,
    "database-name"
).build()

Для хранения db рекомендуют использовать singleton.

Что это нам дает?

Из описанного выше – упрощение работы с базой данных, отпадает потребность в использовании сторонних ORM. Помимо этого, из плюшек – NestedObject, параметры в запросах, коллекции аргументов, TypeConverter, database migration и т.д. Но эти темы не входят в скоуп данного материала и будут рассмотрены позже.

К завершению материала хочу добавить еще несколько слов об архитектуре. С представлением Android Architecture Components была предложена архитектура решений, которая для большинства разработчиков уже и так привычна в той или иной форме. Суть заключается в том, что мы разбиваем архитектуру на 3 основных слоя: View (Activity/Fragment), который общается с ViewModel, а ViewModel работает непосредственно уже с Repository. Для наглядности приведу картинку с developer.android.com.

Выглядит абсолютно просто, а как на самом деле – мы рассмотрим в следующих статьях.

Android Architecture Components. Часть 2. Lifecycle

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

Источник: Android Architecture Components. Часть 1. Введение