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. Введение