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