Android Architecture Components. Часть 1. Введение
На Google I/O 2017, было представлено набор библиотек под названием Android Architecture Components. В нескольких словах — это ряд вспомогательных библиотек, которые призваны помочь с такими вещами как, проектирование, тестирование и сопровождение приложений. То, что команда разработки Android начала акцентировать внимание на архитектуре не может не радовать, поскольку проблема является действительно актуальной. Ведь изначально не было предоставлено никаких требований или гайдлайнов по проектированию, и разработчику приходилось отталкиваться от своего предыдущего опыта. Что, в свою очередь, вызывало сложности в сопровождении проекта, а также сомнительные решения для специфических для ОС ситуаций. По факту это не первые шаги в этом направлении. Ранее уже Google представил репозиторий android-architecture с примерами применения разных архитектурных концептов. Надеемся, что развитие будет дальше и может на следующем Google I/O мы сможем увидеть полноценный фреймворк.
В целом Android Architecture Components можно разделить на четыре блока: Lifecycles, LiveData, ViewModel и Room Persistence.
Компонент Lifecycle – призван упростить работу с жизненным циклом. Выделены основные понятия такие как LifecycleOwner и LifecycleObserver.
LifecycleOwner – это интерфейс с одним методом getLifecycle(), который возвращает состояние жизненного цикла. Являет собой абстракцию владельца жизненного цикла (Activity, Fragment). Для упрощения добавлены классы LifecycleActivity и LifecycleFragment.
LifecycleObserver – интерфейс, обозначает слушателя жизненного цикла owner-а. Не имеет методов, завязан на OnLifecycleEvent, который в свою очередь разрешает отслеживать жизненный цикл.
Что это нам дает?
Назначение этого компонента – избавить разработчика от написания рутинного кода и сделать его более читаемым. Довольно частая ситуация, когда в нашем приложении работает ряд процессов, которые зависят от этапа жизненного цикла. Будь-то воспроизведение медиа, локация, связь с сервисом и т.д. Как итог нам приходится вручную отслеживать состояние и уведомлять о нём наш процесс. Что неудобно по двум причинам, захламление основного класса (Activity или Fragment) и снижение модульности, ведь нам нужно позаботиться про поддержку передачи состояния. С помощью же этого компонента мы можем переложить всю ответственность на наш компонент и все что для этого нужно это объявить интересующий наш класс как observer и передать ему в onCreate() методе ссылку на owner. В общем это выглядит так:
Наш обсервер абсолютно осведомлен о состоянии и может самостоятельно обрабатывать его изменение.
Компонент LiveData – являет собой holder класс для хранения объекта, а также разрешает подписаться к нему. LiveData знает про жизненный цикл и разрешает от него абстрагироваться.
Для имплементации компонента нам нужно расширить класс LiveData. Для работы нам нужно знать всего три метода из этого класса.
onActive() – этот метод вызывается когда у нашего экземпляра есть активный(-ые) обсервер. В нем мы должны инициировать интересующий нас сервис или операцию.
onInactive() – вызывается когда у LiveData нет активных слушателей. Соответственно нужно остановить наш сервис или операцию.
setValue() – вызываем если изменились данные и LiveData информирует об этом слушателей.
Если вы обратили внимание, то мы реализовали наш класс как Singleton. Это дает нам возможность использовать 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 — спроектирован для хранения и управления данными которые связанные с представлением.
Задача же данного компонента, помочь разработчику абстрагировать данные и предоставить их хранение между такими операциями как пересоздание Activity. Если же нам необходимо сохранить небольшой набор данных, таких как item в RadioButtonGroup или же введенный данные, нам отлично подходит Bundle в onSaveInstanceState(). Но если это большой список к примеру: пользователей, товаров, каталог чего-нибудь нам приходилось заново доставать этот список. Вот в этом случае ViewModel является нашим основным помощником. Особенностью данного компонента является то что он привязывается к Activity и автоматически сохраняет свое состояние во время таких операций как onCofigurationChange().
Класс ViewModel, являет собой абстрактный класс, но не имеет абстрактных методов. Для реализации нашего класса нам нужно лишь наследоваться от ViewModel и описать данные которые мы хотим хранить и методы для их получения.
И это все, наш холдер для userList готов. Для того чтоб использовать наш холдер необходимо в методе onCreate(..) активити вызвать instance нашей модели:
С помощью ViewModelProviders, мы берем instance нашей модели. A c помощью if конструкции смотрим есть ли у нас уже данные в нашей модели или еще нет.
Что это нам дает?
Также как и предыдущие компоненты, этот помогает нам справится с особенностями и связанными с ними проблемами жизненного цикла Android. В этом случае, это отделение нашей модели представления данных от Activity и обеспечение безопасного механизма их хранения. Также при использовании совместно с LiveData не составляет проблем реализовать асинхронные запросы.
Компонент Room Persistence — Предлагает уровень абстракции над SQLite, предлагая более простой и продвинутый способ управления.
В целом же мы получили дефолтную ORM, этот компонент можно разделить на три части: Entity, DAO (Data Access Object), Database.
Entity — объектное представление таблицы. С помощью аннотаций можно легко и без лишнего кода описать наши поля.
Для создания нашей Entity нам нужно создать класс POJO (Plain Old Java Object). Пометить класс аннотацией Entity.
@PrimaryKey — Для обозначения ключа. @ColumnInfo — для связи поля в таблице. Создавать методы get и set не обязательно, но тогда нужно предоставить доступ к переменным с помощью модификатора public.
Установление связей объявляется также в теле аннотации Entity: Entity(foreignKeys = @ForeignKey(entity = Other.class, parentColumns = «id», childColumns = «author_id»))
DAO — Интерфейс который описывает методы доступа к БД.
Для реализации создаем интерфейс который помечаем аннотацией DAO и объявляем наши методы. Основные аннотации для методов это Insert Update Delete Query, в комментариях не нуждаются.
В обычном состоянии попытка получить доступ к БД с основного потока закончиться Exception. Если вы все же уверены в своих действиях, то можно воспользоваться методом allowMainThreadQueries(). Для асинхронного доступа рекомендовано использовать LiveData или RxJava.
Database — используется для создания Database Holder и является точкой доступа к соединению с БД.
Для создания нашего класса нужно наследоваться от RoomDatabase и использовать аннотацию Database, которой передаем параметры такие как используемые entity и версию базы. В теле описываем абстрактные методы доступа к нашим DAO.
Для создания instance для нашей БД используем код:
Для хранения db рекомендуют использовать singleton.
Что это нам дает?
Упрощение работы с базой данных, отпадает потребность в использовании сторонних ORM.
Кроме описанных выше плюшек NestedObject, параметры в запросах, коллекции аргументов, TypeConverter, database migration и т.д. Эти темы не входят в скоуп данного материала и будут рассмотрены позже.
К завершению материала хочу добавить еще несколько слов об архитектуре. С представлением Android Architecture Components была предложена архитектура решений, которая для большинства разработчиков уже и так привычна в той или иной форме. Суть заключается в том, что мы разбиваем архитектуру на 3 основных слоя: View (Activity/Fragment), который общается с ViewModel, а ViewModel работает непосредственно уже с Repository. Для наглядности преведу картинку с developer.android.com.
Выглядит абсолютно просто, а как на самом деле мы рассмотрим в следующих статьях.
Где сначала разберем более подробно каждый компонент и в конце реализуем приложение, как говорится с базой данных и… коннектом к сервису.
Источник
Создание композитных компонентов на Android
Приветствую всех Хабра-жителей и Андроид-ценителей!
Композитный в нашем случае означает «состоящий из нескольких», но вы это и так знаете.
Итак, есть Задача:
- Необходимо вывести блок данных, включающий в себя текст, картинки, кнопки и т.д.
(В нашем случае это будет короткий анонс передачи по ТВ) - дизайн блока нарисован специально нанятым дизайнером и вам нельзя отсупать от него ни на пиксель
- Это блок может иметь какую-то внутреннюю логику работы и компоненты могут влиять друг на друга (у нас «внутренней логикой», будет установка символа «*» в заголовок передачи и смена цвета фона если была нажата кнопка «Буду смотреть»)
- Таких блоков может быть много и информация для них получается уже в процессе работы приложения
- как всегда, в процессе работы, дизайн может быть пересмотрен, и вам надо быстро внести изменения в программу не переписывая все с самого начала
На практике это может быть все что угодно — простая строка таблицы состоящей из пары текстовых полей
или сложный финансовый блок с графиками
Для начала, рассмотрим альтернативные варианты и их недостатки в применении к нашему случаю.
Кастом-компоненты (custom component)
Позволяет менять дизайн и поведение компонента, но только в пределах одного компонента, что нам не подходит по-определению.
Пример:
Динамическое создание UI программными средствами
С помощью этого способа придется написать километры килобайты кода, в котором вы каждый TextView будете создавать вручную, передавать в него контекст, создавать для него LayoutParams для описания выравнивания, все это помещать в заранее созданные LinearLayout/FrameLayout/RelativeLayout, сотни раз запускать ваш код что бы добиться соответствия дизайну.
И как только дизайнер пришлёт вам новую версию дизайна, вы, мягко говоря, будете не очень этому рады…
Абстрактный пример создания нескольких полей в коде:
Табличный Layout
По сути — это тоже что и предыдущий пункт, только для выравнивания используются ячейки родительской таблицы.
Данный способ лишает возможности тонко следовать дизайну, так как все выравнивание будет только по горизонтали и вертикали, и что бы сместить компонент в нестандартную позицию нужно будет прибегать к объединению ячеек и вложенным в табличные ячейки лейаутам.
Canvas Draw
Суть данного метода в простом рисовании на канве, вашего UI компонента.
Данный метод мало того что обладает недостатками 2-го пункта (сложная ручная подгонка всех элементов UI в соответствии с дизайном), но и имеет еще один существенный недостаток — невозможность использования стандартных элементов управления EditText, Botton, CheckBox, SeekBar, в этом случае их либо придется писать вручную, либо накладывать поверх нашего UI. В любом случае это будет неадекватные затраты времени и сил на решение задачи.
Создание композитного компонента при помощи LayoutInflater
Наконец мы подошли к самой сути статьи — созданию компонента по приведенному заданию оптимальным способом.
Для начала мы, как уже привыкли, верстаем наш дизайн layout в XML вручную или при помощи визуального редактора который является частью Eclipse плагина ADT.
Обязательно всем ключевым элементам UI даем свои уникальные ID.
Для верстки воспользуемся RelativeLayout, для того что бы иметь возможность задавать относительное положение компонентов внутри родителя и друг относительно друга. Конкретно в этом случае было бы достаточно и вертикального LinearLayout, но мы в образовательных целях лёгких путей не ищем.
Ширина компонента выставлена жестко(288dip), что бы было как в исходной картинке, но ничего не мешает сделать «fill_parent».
Для задания свойств текста, можно было бы создать пару стилей, но обойдемся тем что есть, для наглядности. Так же не пинайте за то, что не вынес текстовые надписи в strings.xml, ухудшилась бы читаемость и пришлось бы цитировать еще один файл в статью.
Далее создаем класс нашего компонента и наследуем его от класса который мы использовали в нашей верстке — RelativeLayout.
Для того, что бы соединить наш класс и лейаут channel_layout, используем LayoutInflater.
Так же мы внутри класса определяем переменные для всех полей что бы связать поля класса с UI.
Теперь в двух словах что я тут наделал: сначала инициализируем все поля и создаем удобные методы для установки значений полей, так например, для установки логотипа есть 2 способа — через указание Id ресурса и через передачу Bitmap.
Так же наш класс является оберткой над «TVProgram parentProgram» — это еще один способ установки полей нашего UI компонента — вызывая setParentProgram и передавая заполненный объект программы, мы автоматом устанавливаем значения всех UI полей из парента.
Компонент готов, остается создать его экземпляры, установить значения полей и добавить их на форму:
И, на последок, скриншот того что у нас получилось:
И исходники.
Удачных проектов Вам!
Источник
Рисование собственных представлений (View) в Android
Получите полный контроль над представлением и оптимизируйте его производительность
В преддверии старта курса «Android Developer. Professional» приглашаем всех желающих принять участие в открытом вебинаре на тему «Пишем gradle plugin».
А пока делимся переводом полезного материала.
Введение
Разработчики постоянно проектируют различные виды пользовательских интерфейсов с помощью XML, но в дополнение к этому можно довольно легко освоить создание собственных представлений, которые открывают новые преимущества и позволяют избежать повторного использования шаблонного кода.
В Android доступен широкий набор готовых виджетов и макетов для создания пользовательского интерфейса, однако они не могут удовлетворить все требования наших приложений. И здесь на помощь приходит возможность создания собственных представлений. Создав собственный подкласс представления, можно получить максимально полный контроль за внешним видом и функционалом экранного элемента.
Прежде чем приниматься за работу с собственными представлениями, полезно изучить жизненный цикл представления.
Зачем создавать собственные представления?
Чтобы реализовать собственное представление, в большинстве случаев понадобится больше времени, чем если использовать обычные представления. Создавать собственные представления стоит лишь в том случае, если нет другого, более простого способа реализовать нужную вам возможность или если у вас есть какие-либо из указанных ниже проблем, которые можно устранить за счет создания собственного представления.
Производительность: в вашем макете много представлений и вы хотите оптимизировать их, нарисовав одно, более легкое собственное представление.
Имеется сложная иерархия представлений, которую трудно использовать и поддерживать.
Необходимо создать специализированное представление, требующее рисования вручную.
Общий подход
Чтобы приступить к созданию компонентов для реализации собственных представлений, необходимо выполнить следующие основные шаги.
Создать класс, расширяющий базовый класс или подкласс представления.
Реализовать конструкторы, использующие атрибуты из XML-файла.
Переопределить некоторые методы родительского класса (onDraw(), onMeasure() и т. д.) в соответствии с нашими требованиями.
После выполнения этих шагов созданный расширяющий класс можно использовать вместо представления, на основе которого он был создан.
Пример
В одном из моих проектов мне нужно было создать круглый виджет TextView для отображения количества уведомлений. Чтобы достичь этой цели, нужно создать подкласс TextView.
Шаг 1. Создадим класс с именем CircularTextView .
Шаг 2. Расширим класс виджета TextView. Здесь под TextView в IDE выдается ошибка, в которой сообщается, что у этого типа есть конструктор и он должен быть инициализирован.
Шаг 3. Добавим конструкторы в класс.
Это можно сделать двумя способами.
Первый способ добавления конструкторов в класс показан ниже.
Другой способ заключается в добавлении аннотации @JvmOverloads к вызову конструктора, как показано ниже.
Часто нас сбивает с толку то, что у представления есть несколько разных типов конструкторов.
View(Context context)
Простой конструктор для динамического создания представления из программного кода. Здесь параметр context — это контекст, в котором работает представление и через который можно получить доступ к текущей теме, ресурсам и т. д.
View(Context context, @Nullable AttributeSet attrs)
Конструктор, который вызывается при формировании представления из XML-файла. Он вызывается, когда представление создается из XML-файла, содержащего атрибуты представления. В этом варианте конструктора используется стиль по умолчанию (0), поэтому применяются только те значения атрибутов, которые есть в теме контекста и заданном наборе AttributeSet .
Шаг 4. Самый важный шаг в отрисовке собственного представления — это переопределение метода onDraw() и реализация необходимой логики отрисовки внутри этого метода.
Метод OnDraw (canvas: Canvas?) имеет параметр Canvas (холст), с помощью которого компонент представления может отрисовывать себя. Для рисования на холсте необходимо создать объект Paint.
Как правило, процесс рисования определяется двумя аспектами:
что рисовать (определяется объектом Canvas);
как рисовать (определяется объектом Paint).
Например, Canvas предоставляет метод для рисования линии, а Paint предоставляет методы для определения цвета этой линии. В нашем случае объект Canvas в классе CircularTextView предоставляет метод для рисования окружности, а объект Paint заполняет ее цветом. Проще говоря, Canvas определяет, какие фигуры можно нарисовать на экране, а Paint определяет свойства нарисованных фигур — цвет, стиль, шрифт и т. д.
Давайте займемся кодом. Мы создаем объект Paint и присваиваем ему некоторые свойства, а затем рисуем фигуру на холсте (объект Canvas), используя наш объект Paint. Метод onDraw() будет выглядеть так:
IDE показывает предупреждение о том, что следует избегать выделения объектов во время операций отрисовки или операций с макетом. Это предупреждение возникает потому, что метод onDraw() много раз вызывается при отрисовке представления, в котором каждый раз создаются ненужные объекты. Поэтому, чтобы избежать ненужного создания объектов, мы вынесем соответствующую часть кода за пределы метода onDraw() , как показано ниже.
При выполнении отрисовки всегда помните о том, что следует повторно использовать объекты вместо создания новых. Ваша IDE может указать на потенциальные проблемы, но полагаться на нее не стоит. Например, она не сможет отследить случай, когда объекты создаются внутри методов, вызываемых из метода onDraw() . Поэтому лучше проверять все самостоятельно.
Шаг 5. Мы закончили с рисованием. Теперь давайте внесем этот класс представления в XML.
Добавьте этот XML-макет в вашу активность (Activity) и запустите приложение. Вот что будет на экране.
Выглядит неплохо, правда? Теперь сделаем так, чтобы значение динамическому свойству цвета в circlePaint назначалось из активности, а также добавим контур к кружку. Для этого в классе CircularTextView необходимо создать несколько методов-сеттеров, чтобы можно было вызывать эти методы и устанавливать свойства динамически.
Для начала давайте реализуем настройку цвета отрисовки. Для этого создадим сеттер, как показано ниже.
Теперь мы можем устанавливать цвет из нашей активности динамически, вызывая этот метод.
Неплохо, правда? Теперь давайте добавим контур к кружку. Контур будет задаваться двумя входными параметрами: шириной линии контура и ее цветом. Чтобы задать цвет линии контура, нам нужно создать объект Paint точно так же, как мы это делали для кружка. Чтобы задать ширину линии контура, мы создадим переменную, установим для нее нужное значение и используем его в методе onDraw() . Полный код будет выглядеть так:
Теперь в активности можно динамически настраивать эти атрибуты нужным образом.
Далее давайте запустим приложение, устанавливая различные цвета для нашего виджета.
Итак, теперь стало ясно, как динамически устанавливать свойства из активности, но возникает вопрос о том, как устанавливать атрибуты из XML. Продолжим наше исследование.
Для начала создадим файл с именем attrs.xml в папке values. Этот файл будет содержать все атрибуты для различных представлений, которые мы создаем сами. В приведенном ниже примере у нашего представления под названием CircularTextView имеется атрибут ct_circle_fill_color , который принимает значение цвета. Аналогичным образом мы можем добавить и другие атрибуты.
Затем нам нужно будет прочитать эти свойства в классе, который мы создали для реализации собственного представления. В блоке инициализации мы считываем набор атрибутов, как показано ниже.
Теперь просто переходим к XML-макету и устанавливаем значение свойства, соответствующее нужному цвету, после чего запускаем приложение. На выходе мы увидим нужный результат.
В моем случае результат был таким:
Примечание. При рисовании не задавайте жестко размер вашего представления, так как им могут воспользоваться другие разработчики с применением других размеров. Рисуйте представление в соответствии с его текущим размером.
Обновление представления
Итак, мы задали собственное представление. Если мы хотим обновлять представление при изменении какого-нибудь свойства или по какой-то другой причине, этого можно добиться двумя основными способами.
invalidate()
invalidate() — это метод, который инициирует принудительную перерисовку определенного представления. Проще говоря, метод invalidate() следует вызывать в случае, когда требуется изменение внешнего вида представления.
requestLayout()
Если в какой-то момент происходит изменение состояния представления, то метод requestLayout() сообщает системе представлений, что необходимо сделать перерасчет фаз «измерение» (Measure) и «макет» (Layout) для данного представления (измерение → макет → рисование). Проще говоря, метод requestLayout() следует вызывать в случае, когда требуется изменение границ представления.
Теперь, я надеюсь, вы знаете в общих чертах, как создавать собственные представления. Чтобы они демонстрировали отличную производительность, необходимо освоить все описанные здесь методы.
Источник