Что такое delegate android

Содержание
  1. Android preferences delegate
  2. Что такое делегат и как его готовят
  3. Применяем делегат
  4. Generic
  5. Кастомная ошибка
  6. Заключение
  7. Android preferences delegate
  8. Что такое делегат и как его готовят
  9. Применяем делегат
  10. Generic
  11. Кастомная ошибка
  12. Заключение
  13. Delegate Adapter — зачем и как
  14. Проблема
  15. Готовые решения
  16. Другое решение
  17. Реализация
  18. Отображение картинок в Android-приложении: делегаты, тесты и никакой боли
  19. Авторизуйтесь
  20. Отображение картинок в Android-приложении: делегаты, тесты и никакой боли
  21. Lead Android Engineer в Revolut
  22. Приложение Revolut
  23. Как работает стандартный способ отображения картинок
  24. Как улучшить адаптер
  25. Как отображать картинки
  26. Как объединить делегаты
  27. Как запустить трансформации
  28. Как создать делегат для отображения транзакции
  29. Как создать делегат для генерируемой картинки
  30. Как создать кастомную трансформацию
  31. Как тестировать
  32. Кейс 1 — Перевод контакту без аватарки
  33. Кейс 2 — Перевод контакту с аватаркой
  34. Кейс 3 — Покупка в магазине, у которого в системе есть аватар
  35. Кейс 4 — Покупка в магазине без аватарки
  36. Выводы

Android preferences delegate

В данной статье разобран пример создания делегата для SharedPreferences, который уменьшает boilerplate и делает использование SharedPrefernces более удобным. Те кто хочет посмотреть результат, может перейти к готовому решению, остальным добро пожаловать под кат.

Одной из насущных задач разработки под android является сохранение между сессиями приложение каких-либо данных. Основные способы для этого: хранить на сервере или в файлах на исполняемом устройстве. Одним из самых первых способов с котором знакомиться любой начинающий разработчик на android это хранение в файле при помощи уже готового инструмента SharedPreferences.

Допустим что нам нужно записывать имя пользователя и далее его отображать где либо в приложении.

Что такое делегат и как его готовят

В двух словах это класс, который инкапсулирует установку и получение свойства. Кто хочет узнать большее официальная документация.

Что бы сделать класс делегатом необходимо реализовать интерфейс ReadOnlyProperty для val и ReadWriteProperty для var. Передаем SharedPreferences, ключ по которому будет храниться свойство и дефолтное значение через конструктор. В setValue устанавливаем значение в getValue получаем значение.

Применяем делегат

Назначение свойству делегата осуществляется по ключевому слову by. Теперь каждый раз, когда данное свойство будет запрашиваться или устанавливаться будут запускаться методы getValue и setValue созданного делегата.

Так же аналогично теперь можно поступить с другими полями, к примеру, если нужно так же сохранять телефон пользователя.

Generic

Чтобы не делать для каждого типа данных отдельный делегат воспользуемся обобщениями generic официальная документация.

Обычно первое не осознанное знакомство с generic происходит при создании экземпляра класса List. Для него определяется конкретный тип данных с которым он работает.

Чтобы задать обобщенный тип данных у класса после его название необходимо указать название этой переменной в угловых скобках.

Теперь необходимо задать, что устанавливаемое значение и дефолтное значение имеют тип TValue.

Соответственно теперь создание экземпляра класса выглядит так:

Осталось сделать маппинг получения и установки свойств, определяем тип данных по delaultValue, заодно это дает smart cast этого значения к конкретному типу данных, если что то пошло не так и свойство не типа TValue возвращаем defValue.

С остальными типами данных аналогично.

Кастомная ошибка

Остается вопрос, что делать с веткой else, поскольку тип TValue может быть абсолютно любым.
Хорошим тоном будет сделать свою кастомную ошибку. Если произойдет исключение, тогда будет максимально понятно, что произошло.

Заключение

Итого получаем готовый к применению делегат:

Источник

Android preferences delegate

В данной статье разобран пример создания делегата для SharedPreferences, который уменьшает boilerplate и делает использование SharedPrefernces более удобным. Те кто хочет посмотреть результат, может перейти к готовому решению

Одной из насущных задач разработки под android является сохранение между сессиями приложение каких-либо данных. Основные способы для этого: хранить на сервере или в файлах на исполняемом устройстве. Одним из самых первых способов с котором знакомиться любой начинающий разработчик на android это хранение в файле при помощи уже готового инструмента SharedPreferences.

Допустим что нам нужно записывать имя пользователя и далее его отображать где либо в приложении.

Что такое делегат и как его готовят

В двух словах это класс, который инкапсулирует установку и получение свойства. Кто хочет узнать большее официальная документация.

Что бы сделать класс делегатом необходимо реализовать интерфейс ReadOnlyProperty для val и ReadWriteProperty для var. Передаем SharedPreferences, ключ по которому будет храниться свойство и дефолтное значение через конструктор. В setValue устанавливаем значение в getValue получаем значение.

Применяем делегат

Назначение свойству делегата осуществляется по ключевому слову by. Теперь каждый раз, когда данное свойство будет запрашиваться или устанавливаться будут запускаться методы getValue и setValue созданного делегата.

Так же аналогично теперь можно поступить с другими полями, к примеру, если нужно так же сохранять телефон пользователя.

Generic

Чтобы не делать для каждого типа данных отдельный делегат воспользуемся обобщениями generic официальная документация.

Обычно первое не осознанное знакомство с generic происходит при создании экземпляра класса List. Для него определяется конкретный тип данных с которым он работает.

Чтобы задать обобщенный тип данных у класса после его название необходимо указать название этой переменной в угловых скобках.

Теперь необходимо задать, что устанавливаемое значение и дефолтное значение имеют тип TValue.

Соответственно теперь создание экземпляра класса выглядит так:

Осталось сделать маппинг получения и установки свойств, определяем тип данных по delaultValue, заодно это дает smart cast этого значения к конкретному типу данных, если что то пошло не так и свойство не типа TValue возвращаем defValue.

Читайте также:  Zte blade v10 обновление до андроид 10

С остальными типами данных аналогично.

Кастомная ошибка

Остается вопрос, что делать с веткой else, поскольку тип TValue может быть абсолютно любым.
Хорошим тоном будет сделать свою кастомную ошибку. Если произойдет исключение, тогда будет максимально понятно, что произошло.

Заключение

Итого получаем готовый к применению делегат:

Источник

Delegate Adapter — зачем и как

Практически во всех проектах, которыми я занимался, приходилось отображать список элементов (ленту), и эти элементы были разного типа. Часто задача решалась внутри главного адаптера, определяя тип элемента через instanceOf в getItemViewType(). Когда в ленте 2 или 3 типа, кажется, что такой подход себя оправдывает… Или нет? Что, если завтра придет требование ввести еще несколько типов да еще и по какой-то замысловатой логике?

В статье хочу показать, как паттерн DelegateAdapter позволяет решить эту проблему. Знакомым с паттерном может быть интересно посмотреть реализацию на Kotlin с использованием LayoutContainer.

Проблема

Начнем с примера. Предположим, у нас есть задача отобразить ленту с двумя типами данных — текст с описанием и картинка.

Минус такой реализации в нарушении принципов DRY и SOLID (single responsibility и open closed). Чтобы в этом убедиться, достаточно добавить два требования: ввести новый тип данных (чекбокс) и еще одну ленту, где будут только чекбоксы и картинки.

Перед нами встает выбор — использовать этот же адаптер для второй ленты или создать новый? Независимо от решения, которое мы выберем, нам придется менять код (об одном и том же, но в разных местах). Надо будет добавить новый VIEW_TYPE, новый ViewHolder и отредактировать методы: getItemViewType(), onCreateViewHolder() и onBindViewHolder().

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

Если решим создать новый адаптер, то будет просто масса дублирующего кода.

Готовые решения

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

Впервые с паттерном я столкнулся, читая цикл статей Жуана Игнасио о написании проекта на Котлин. Реализация Жуана, как и решение, освещенное на хабре — RendererRecyclerViewAdapter, — не нравится мне тем, что знание о ViewType распространяется по всем адаптерам и даже дальше.

создать модель, реализующую интерфейс ViewType:

зарегистрировать DelegateAdapter c нужно константой:

Таким образом, логика с типом данных размазывается по трем классам (константы, модель и место, где происходит регистрирование). Кроме того, нужно следить за тем, чтобы случайно не создать две константы с одним и тем же значением, что очень легко сделать в решении с RendererRecyclerViewAdapter:

Оба описанных подхода основаны на библиотеке AdapterDelegates Ханса Дорфмана, которая мне нравится больше, хотя и вижу недостаток в необходимости создавать адаптер. Эта часть — «бойлерплейт», без которого можно было бы обойтись.

Другое решение

Код лучше слов скажет за себя. Давайте попробуем реализовать ту же ленту с двумя типами данных (текст и картинка). Реализацию напишу на Kotlin с использованием LayoutContainer (подробнее расскажу ниже).

Пишем адаптер для текста:

адаптер для картинок:

и регистрируем адаптеры в месте создания главного адаптера:

Это все, что нужно сделать для решения поставленной задачи. Обратите внимание, насколько меньше кода, по сравнению с классической реализацией. Кроме того, данный подход позволяет легко добавлять новые типы данных и комбинировать DelegateAdapter-ы между собой.

Давайте представим, что поступило требование добавить новый тип данных (чекбокс). Что нужно будет сделать?

и добавить строчку к созданию адаптера:

Новый тип данных в ленте — это layout, ViewHolder и логика байндинга. Предложенный подход мне нравится еще и тем, что все это находится в одном классе. В некоторых проектах ViewHolder-ы и ViewBinder-ы выносят в отдельные классы, а инфлейтинг layout-а происходит в главном адаптере. Представьте задачу — нужно просто изменить размер шрифта в одном из типов данных в ленте. Вы заходите во ViewHolder, там видите findViewById(R.id.description). Щелкаете по description, и Идея предлагает 35 layout-ов, в которых есть view с таким id. Тогда вы идете в главный адаптер, затем в ParentAdapter, затем в метод onCreateViewHolder, и наконец, надо найти нужный внутри switch в 40 элементов.

В разделе «проблема» было требование с созданием еще одной ленты. С delegate adapter задача становится тривиальной — просто создать CompositeAdapter и зарегистрировать нужные типы DelegateAdapter-ов:

Т.е. адаптеры не зависимы друг от друга и их можно легко комбинировать. Еще одним преимуществом является удобство передачи обработчиков (onСlickListener). В BadAdapter (пример выше) обработчик передавался адаптеру, а тот уже передавал его ViewHolder-у. Это увеличивает связность кода. В предложенном же решении обработчики передаются через конструктор только тем классам, которым они необходимы.

Реализация

Для базовой реализации (без Котлина и LayoutContainer), нужно 4 класса:

Источник

Отображение картинок в Android-приложении: делегаты, тесты и никакой боли

Авторизуйтесь

Отображение картинок в Android-приложении: делегаты, тесты и никакой боли

Lead Android Engineer в Revolut

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

Читайте также:  Что делать при блокировке android

Приложение Revolut

В нашем приложении много типов картинок для отображения — есть списки транзакций с разными иконками, списки карточек, Lottie-анимации, гифки. Покажу, как мы работаем с картинками на примере списка транзакций.

У нашего списка транзакций насчитывается несколько десятков типов ячеек. Для примера мы возьмем пять:

Разные типы транзакций, где мы показываем картинку

В каждом случае картинка взята из отдельного источника или сгенерирована.

Как работает стандартный способ отображения картинок

Создадим адаптер для такого списка.

Так будет выглядеть стандартный шаблон адаптера для RecyclerView . Реализуем биндинг значений:

Появляется портянка условий, потому что внутри адаптера для каждого вида транзакции мы строим отдельную логику. Можно усложнить и использовать свой ViewType под каждый источник. Тем более к этому подталкивает контракт адаптера:

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

Как улучшить адаптер

Можем выделить два основных подхода к расширению — ViewType или делегаты. Остальные не упоминаю специально: по своей сути они будут похожи на второй подход.

Первый вариант — ViewType — можно использовать, когда приложение простое, то есть содержит один простой список и, например, пару экранов. Нам этот способ не подходит, потому что такие адаптеры нельзя переиспользовать. Если мы будем расширять адаптер, добавляя новые ViewType , адаптер будет неконтролируемо расти. Кроме того, под каждый экран нам придётся создавать свои адаптеры.

Revolut , Удалённо , По итогам собеседования

Второй подход — с делегатами — выглядит привлекательнее. Он позволяет не создавать разные адаптеры под каждый экран, а использовать делегаты. Четыре года назад об этом писал Ханс Дорфман, и на GitHub можно найти много библиотек с реализацией такого подхода. Мы будем использовать реализацию самого Дорфмана.

Смотрим на пример простого делегата, который отображает ProgressBar.

Внутри делегата, как и в стандартном адаптере, создаем ViewHolder . Происходит биндинг. Главное отличие от стандартного адаптера в том, что у каждого делегата есть своя модель. Она будет использоваться, чтобы отобразить нужный тип ячейки. В свою очередь, у каждой модели есть интерфейс ListItem с полем listId и методом calculatePayloads внутри.

Перейдём к реализации адаптера, который умеет отображать делегаты.

В этой реализации видно, зачем нужен интерфейс ListItem — его удобно использовать для ListDiffCallback , чтобы DiffUtil не обновлял ячейки, которые не изменились, и не запускал лишние анимации. Кроме того, так как для моделей используется Data class, нам из коробки доступен equals . Вся работа с DiffUtil сводится к правильному созданию модели делегата.

Под каждый экран адаптер создаётся так: мы в конструкторе передаём список делегатов, который экран должен поддерживать.

Благодаря делегатам создание адаптера под каждый экран упрощается.

Как отображать картинки

Теперь уберём логику загрузки и отображения картинок из адаптера, разгрузим onBindViewHolder . Мы должны реализовать две сущности — модель картинки и делегат, который будет уметь загружать и отображать её. Рассмотрим пример модели, где загружаем картинку из ресурсов.

Сначала сделаем интерфейс Image . Затем опишем набор параметров для ResourceImage , по которым хотим настраивать отображение. В данном случае — id ресурса картинки и цвета, если хотим её закрасить.

Теперь перейдём к делегату загрузки и определим его интерфейс. Отсюда понятно, зачем нам интерфейс Image .

Каждый делегат должен уметь делать две вещи:

  1. определять, умеет ли он отображать переданную картинку или нет;
  2. отображать картинку в ImageView .

Так будет выглядеть делегат загрузки картинки из ресурсов.

  • метод suitsFor() проверяет, что image — ResourceImage ;
  • внутри метода displayTo() мы устанавливаем картинку в ImageView и, если colorRes не null , то выставляем tint .

Это самый простой из возможных делегатов.

Как объединить делегаты

Объединим все поддерживаемые делегаты в одном месте и сократим интерфейс взаимодействия до метода displayTo() .

Обращаю внимание на строку 18. При помощи метода first() мы находим первый подходящий делегат для отображения картинки. Если нужный делегат не найден, возможен краш, и это не ошибка проектирования. Мы намеренно придерживаемся принципа fail-fast, чтобы быстро избавиться от неочевидного поведения. Например, когда картинка не отобразилась, а мы не знаем причину.

Как запустить трансформации

Разберёмся, зачем в отображении транзакции могут понадобиться трансформации. Предположим, у нас есть аватарка контакта или продавца, которую мы получаем из сети. Она может иметь любую форму и размер, но в приложении Revolut мы должны отобразить её круглой и определённого размера — 40х40 dp.

Аватары в приложении Revolut

Настроим модель и добьёмся такого поведения.

Возьмём UrlImage . Любая картинка, которой нужна поддержка трансформаций, должна иметь соответствующие настройки. Можно ввести интерфейс TransformableImage со свойством transformations :

Класс настроек может выглядеть так:

Для отображения картинок используем Glide. Соответственно, трансформации ориентированы под эту библиотеку.

Так создаётся массив преобразований. Отмечу два момента, которые помогут избежать лишней работы.

Во-первых, поля помечены как nullable, и это позволяет задавать только нужные трансформации. Во-вторых, не очевидно, но критично, в каком порядке трансформации будут вызваны.

Представим, что на входе — очень широкая картинка, которую надо повернуть, отмасштабировать и скруглить. Сравним два сценария развития событий.

Читайте также:  Хорошие беспроводные наушники для андроид до 3000

В первом случае сначала мы поворачиваем картинку на 90 градусов, затем закругляем и только после этого меняем ширину. Во втором случае мы сначала меняем ширину.

Второй сценарий эффективнее, так как поворот и скругление «дешевле» делать на меньших изображениях.

Вернёмся к реализации. Ранее мы создали массив, который теперь должны передать в Glide, когда он будет отображать картинку по URL. Создаём объект RequestOptions и передаём ему массив. Помним, что нельзя передавать пустой массив — Glide упадёт. Поэтому обязательно добавляем проверку.

Так как будем переиспользовать трансформации в разных делегатах, будет удобно вынести их в экстеншн applyImageTransformations .

Также добавляем метод в интерфейс TransformableImage — getGlideTransformsArray() . Сам интерфейс и экстеншен applyImageTransformations помечены как internal . Так мы избегаем утечки абстракции, и конечный пользователь моделей и делегатов не знает, что используется внутри — в публичных интерфейсах Glide не виден. Удобно, если захотим заменить Glide на другую библиотеку.

В итоге код сокращается до такого вида:

Как создать делегат для отображения транзакции

Посмотрим снова на наш список транзакций. Мы уже знаем, как работает адаптер делегатов. Теперь создадим делегат для отображения транзакции.

Базовая реализация адаптера выглядит так:

Отображение текста убрано для упрощения. Мы научим этот делегат отображать транзакции с картинками из сети, из ресурсов, показывать аватар контакта, который создаётся из инициалов.

Сначала модифицируем модель.

В каждом случае передаём свои параметры, все в одном месте. Примерно так будет отображаться картинка:

Сразу видим минусы:

  • такое решение тяжело расширять;
  • важен порядок, который в свою очередь может быть неочевидным;
  • бизнес-логика находится внутри адаптера (делегата).

Начнём сначала и попробуем использовать делегаты. Сделаем несколько доработок.

В модели вместо всех параметров оставляем только картинку на отображение:

В итоге список транзакций примет такой вид:

Поведение становится более явным, и мы вынесли логику из адаптера.

Как создать делегат для генерируемой картинки

Теперь рассмотрим частный случай и создадим делегат, который будет генерировать изображение из двух символов. В первую очередь, определим требования к этому делегату. Очевидно, он должен уметь отображать буквы и настраивать отображение.

Модель будет выглядеть следующим образом:

Для настройки фона используем ImageTransformations .

Перейдём к генерации битмапы. Можем использовать обертку TextDrawable , где внутри отрисовка идёт при помощи Canvas . Далее эту битмапу нужно обработать и установить в ImageView .

За счёт использования экстеншена реализация делегата занимает пару строк. Покажу, как он работает.

Первый вариант, где заданы базовые настройки:

Во втором варианте добавляем трансформацию скругления:

И в третьем — поворачиваем картинку. Нам ничего не стоит отображать иконку аватара в том виде, в котором это требуется согласно дизайну:

Как создать кастомную трансформацию

Представим, что нам надо сделать флип по горизонтали. Сначала создадим каркас класса трансформации:

В случае с Glide базовый класс должен быть BitmapTransformation . Glide снова упрощает жизнь, так как содержит TransformationUtils с нужными методами. Остаётся лишь добавить эту трансформацию к остальным.

Как тестировать

Одна из главных причин, почему стоит использовать именно этот способ работы с картинками — тестирование.
Нарисуем примерную схему архитектуры (clean) и покажем, как данные доходят до слоя UI. В качестве данных будем рассматривать список транзакций.

Пример чистой архитектуры

Получилась довольно стандартная схема. База данных возвращает список моделей, на уровне репозитория мы мапим их в модели доменного уровня. Тот, в свою очередь, передаст их на уровень выше — до UI. Каждый этап маппинга моделей покрывается тестами.

Рассмотрим, как может выглядеть доменная модель транзакции:

У неё есть доступ к id транзакции, сумме и дате. Как понять, что показывать — это денежный перевод или покупка в магазине? Откуда брать название, URL? Нам помогут sealed class.

Здесь мы видим два типа транзакций — перевод и покупка. Каждый имеет уникальный набор параметров.

Далее разберёмся, что является моделью для слоя UI, и для этого вспомним, как выглядел наш делегат для адаптера RecyclerView .

Модель делегата отлично подходит в качестве UI-модели.

Рассмотрим несколько сценариев, которые можем протестировать только за счёт использования делегатов для адаптера и картинок.

Кейс 1 — Перевод контакту без аватарки

Проверяем, создаётся ли модель картинки для отображения инициалов, если отсутствует URL аватара .

Кейс 2 — Перевод контакту с аватаркой

Ожидаем, что будет создана UrlImage с одной трансформацией.

Кейс 3 — Покупка в магазине, у которого в системе есть аватар

Идентично кейсу 2: ожидаем, что будет создана UrlImage с одной трансформацией.

Кейс 4 — Покупка в магазине без аватарки

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

Выводы

Отображение картинок с помощью делегатов даёт несколько преимуществ.

Во-первых, мы освобождаем адаптер от логики, которой в нём быть не должно. Он не должен отвечать за выбор источника картинки в зависимости от набора параметров.

Во-вторых, мы абстрагировались от способа загрузки и обработки изображений. Теперь в любой момент времени мы почти без боли можем заменить Glide на что-то другое.

В-третьих, как следствие, мы можем тестировать отображение нужного типа картинки. То есть фактически тестировать отображение данных на экране.

Источник

Оцените статью