Русский
Русский
English
Статистика
Реклама

Ui kit

Taiga UI библиотека компонентов под Angular, которую вам стоит попробовать

12.01.2021 14:15:46 | Автор: admin

Привет!

Саша Инкин и я регулярно пишем на Хабр статьи по Angular. Почти все они основаны на нашем опыте разработки большой библиотеки компонентов.

Эту библиотеку мы развиваем, перерабатываем и дополняем уже несколько лет, а свои идеи проверяем на нескольких десятках проектов Тинькофф Бизнеса и внутренних систем компании. Мы рады сообщить: выложили нашу библиотеку в открытый доступ!

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

Как развивался наш UI Kit и как он организован

Если вам интересно узнать, зачем нам понадобилась отдельная библиотека компонентов и как она исторически развивалась, то сначала рекомендую прочитать крутейшую статью Юли Царевой Как организовать работу над библиотекой общих компонентов.

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

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

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

Эта часть обзавелась собственным дизайном и полной независимостью от контекста, а недавно оправдала свое название переездом в опенсорс. Давайте познакомимся с нашим UI Kit поближе!

Полностью модульный

Начнем с того, как организован проект. Taiga UI состоит из нескольких слоев, которые являются отдельными пакетами.

@taiga-ui/cdk

Фундаментальный пакет. Представляет собой набор директив, сервисов, токенов, абстрактных классов и утилит для абстрагирования и упрощения работы с Angular. Может быть использован как дополнительный мультитул с удобствами для вашего Angular-приложения любой сложности, а может и стать основой для построения собственного кита.

Пример сущностей:

  • TuiDestroyService для избавления от постоянного создания сабжектов destroy$ в компонентах.

  • TuiFilterPipe и TuiMapperPipe для обработки значений в шаблонах без лишних вызовов ChangeDetection.

  • Декоратор tuiPure для мемоизации значений геттеров и методов класса.

@taiga-ui/core

Пакет с базовыми компонентами для построения интерфейсов, а также с инструментами для основы веб-приложения. Это инструменты вроде рутового компонента, порталов для диалогов и выпадашек, настройки темизации и анимаций. Core задает вектор остальным пакетам c UI-элементами. На этом уровне появляется дизайн и все общие стили лежат тут.

@taiga-ui/kit

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

@taiga-ui/addon-*

Ряд тематических пакетов, которые основаны на первых трех. Например, есть пакет charts для графиков, commerce для работы с валютами, деньгами и вводом карт или даже отдельный пакет doc для построения собственной витрины аналогично нашей (ссылка на нее будет дальше).

Получается вот такая иерархия, при которой пакеты более высокого уровня строятся на базовых пакетах:

Вопрос: зачем тянуть в зависимости несколько пакетов, если я хочу лишь пару компонентов? Сколько они весят?

Несколько месяцев назад мы перешли от концепции единых точек входа в каждый пакет к полной разбивке всех пакетов на Secondary Entry Points. Чтобы сохранить удобный и привычный контракт импортов, мы устроили все пакеты по уникальной схеме, когда каждый последующий уровень вложенности является Secondary-поинтом к текущему, но Primary к сущностям своего содержимого.

Теперь все наши пакеты дают возможность импортировать сущности как из Primary точки входа, так и из любого уровня вложенности:

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

Такой подход к работе с Secondary Entry Points дает целый ряд плюсов в организации библиотек:

  • Бандл-приложений меньше, библиотека становится максимально tree shakable.

  • Любые циклические зависимости отлавливаются на этапе сборки.

  • Больше структурности в проекте, нет лишних связей между сущностями.

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

Кастомизируемый

Все стили и цвета задаются через CSS custom properties. Это позволяет легко собирать кастомные темы или даже подменять их в приложении на ходу.

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

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

Агностичный

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

Мы не завязываемся на интерфейсы, не ограничиваем визуальное отображение и думаем о том, как сделать компонент максимально гибким в использовании, а не продумываем все возможные юзкейсы. Подробнее о таких подходах можно почитать в статье Саши Инкина Компоненты-агностики в Angular.

Тем не менее мы контролируем базовый UX, чтобы вам не приходилось о нем думать: например, при фокусировке инпута с клавиатуры тултип покажет подсказку через секунду автоматически, чтобы screen reader прочитал ее:

Технологичный

Мы очень чтим девелоперские практики и стараемся придерживаться близкого к идеям самого фреймворка Angular-подхода.

Мы не боимся работать с DI, все наши компоненты в OnPush, а на всем проекте включен strict-режим TypeScriptа к типизации мы тоже относимся трепетно. Одним днем вы решите перейти на SSR, и наши компоненты в нем не сломаются.

Вы можете не беспокоиться о неожиданном значении, выходящем за рамки возвращаемого типа, а наши компоненты сами выведут ассерт в дев моде, если вы передадите в них что-то не то :)

Многообразный

Наш кит очень большой, в нем больше 130 UI-компонентов, десятки директив, токенов и различных инструментов.

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

Как начать использовать

Заходите на официальный сайт и в документацию библиотеки. Смотрите, изучайте и следуйте инструкциям:

taiga-ui.dev

Если вам хочется поддержать нас или было бы интересно понаблюдать за развитием библиотеки ставьте звездочку и подписывайтесь на Taiga UI в Github. Там можно задать любые вопросы, предложить идею или даже контрибьютить в код.

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

Хотите узнать больше?

В ближайший четверг, 14 января, мы проведем стрим на нашем новом Twich канале. Начнем в 19-00 по МСК.

Расскажем больше про проект, презентуем его структуру, основные части и особенности. Постараемся ответить на любые ваши вопросы про Taiga UI, Angular или разработку библиотек компонентов.

Не прощаемся

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

Поделитесь мнением о Taiga UI и расскажите, о каких компонентах, инструментах или процессах вам хотелось бы почитать в первую очередь?

Подробнее..

Упрощаем работу с Angular с помощью taiga-uicdk 5 наших лучших практик

11.05.2021 12:14:21 | Автор: admin

CDK базовый пакет библиотеки компонентов Taiga UI. Он не имеет никакой привязки к визуальной составляющей библиотеки, а скорее служит набором полезных инструментов для упрощения создания Angular-приложений.

Среди всех этих инструментов я выделил мою пятерку фаворитов. Я использую их во всех своих проектах и уже давно не представляю, как писать на Angular без них, потому что они ежедневно экономят мне массу времени.

Дисклеймер о весе библиотеки

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

По результатам bundlephobia мы получим следующую картинку:

23 КБ результат не самый страшный, но и не очень приятный. Но все сущности наших библиотек лежат в отдельных Secondary Entry Point, что делает их полностью tree shakable. Это значит, что такой объем в бандле мы получим только в случае импорта и использования всех сущностей библиотеки в нашем приложении. Если вы импортите пару сущностей только они и попадут к вам в бандл, добавив к нему в результате меньше 1 КБ.

tuiPure продвинутая мемоизация вычислений

Это декоратор, который можно вешать на геттеры и чистые методы классов. Давайте разберем оба сценария.

Как геттер

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

Пример 1. Скрываем и показываем сороковое число Фибоначчи, когда пользователь нажимает на кнопку

// template<div *ngIf="show">fibonacci(40) = {{ fibonacci40 }}</div>// component@tuiPureget fibonacci40(): number {  return calculateFibonacci(40);}

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

Пример 2. У нас есть компонент pull to refresh, который эмулирует поведение под iOS и Android. Один из его стримов вызывается только для Андроида, а для iOS нет. Завернем его в getter с pure, и ненужный Observable не будет создан для iOS.

<tui-mobile-ios-loader   *ngIf="isIOS; else angroidLoader"></tui-mobile-ios-loader><ng-template #angroidLoader>   <tui-mobile-android-loader       [style.transform]="loaderTransform$ | async"   ></tui-mobile-android-loader></ng-template>
@tuiPureget loaderTransform$(): Observable<string> {    return this.pulling$.pipe(        map(distance => translateY(Math.min(distance, ANDROID_MAX_DISTANCE))),    );}

Также можно обратиться к changes от ContentChild / ContentChildren: если мы вызываем такой геттер из шаблона, то уже можем быть уверены, что content готов. При соблюдении порядка также это можно провернуть и с ViewChild / ViewChildren.

Как метод

На метод тоже можно повесить декоратор @tuiPure. Тогда он будет работать следующим образом: при первом вызове метода посчитает значение и вернет его. Все последующие вызовы с теми же самыми аргументами будут возвращать уже посчитанный результат. Если аргумент изменится результат пересчитается.

Это полезно не только для сложных вычислений, но и в связке с геттером для создания непримитивов объектов или массивов.

get filteredItems(): readonly string[] {   return this.computeFilteredItems(this.items);}@tuiPureprivate computeFilteredItems(items: readonly string[]): readonly string[] {   return items.filter(someCondition);}

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

Документация по tuiPure

*tuiLet

Это простая структурная директива для объявления локальных переменных в шаблонах.

<ng-container *tuiLet="timer$ | async as time">   <p>Timer value: {{time}}</p>   <p>       It can be used many times:       <tui-badge [value]="time"></tui-badge>   </p>   <p>       It subsribed once and async pipe unsubsribes it after component destroy   </p></ng-container>

Вместо *tuiLetможно использовать *ngIf если вам не нужно показывать шаблон при falsy-значении (или если оно не предусмотрено). Но если вы работаете, например, с числами, то 0, скорее всего, является вполне адекватным значением. Тут и поможет *tuiLet

Документация по tuiLet

Метапайпы tuiMapper и tuiFilter

Мы создали пайп, чтобы не создавать другие пайпы, tuiMapper.

Он очень простой. Это pure-пайп, который принимает в себя чистую функцию для преобразования и произвольное количество аргументов к ней. В шаблоне это выглядит так:

{{value | tuiMapper : mapper : arg1 : arg2 }}

Также удобно и преобразовывать данные для инпутов компонентов в шаблоне или использовать через *ngIf / *tuiLet:

<div    *ngIf="item | tuiMapper : toMarkers : itemIsToday(item) : !!getItemRange(item) as markers"    class="dots">    <div class="dot" [tuiBackground]="markers[0]"></div>    <div        *ngIf="markers.length > 1"        class="dot"        [tuiBackground]="markers[1]"    ></div></div>

Добавление цветных маркеров-точек в календарях @taiga-ui/core

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

А еще у нас есть tuiFilter для удобства фильтрации массивов. Фактически это частный случай маппера, но нам нужен довольно часто. Поскольку это чистый пайп, то никаких проблем с производительностью из-за пересоздания массивов тут нет.

Документация на mapper / документация на filter

destroy$

Это Observable-based сервис, который упрощает процесс отписки в компонентах и директивах.

@Component({   // ...   providers: [TuiDestroyService],})export class TuiDestroyExample {   constructor(     @Inject(TuiDestroyService)      private readonly destroy$: Observable<void>   ) {}   //    subscribeSomething() {       fromEvent(this.element, 'click')           .pipe(takeUntil(this.destroy$))           .subscribe(() => {               console.log('click');           });   }}

Все что нам нужно добавить его в providers компонента и заинжектить в конструкторе. Я предпочитаю писать типы сущностей из DI, которые минимально необходимы в компоненте. Здесь это Observable<void>. Но можно писать и покороче:

constructor(private destroy$: TuiDestroyService) {}

Кстати, сервис в такой ситуации привязывается не к лайфсайклу компонента, а к его DI Injectorу. Это может помочь в ситуации, когда нужно подписаться в сервисе или внутри DI фабрики. Такие кейсы довольно редки, но зато TuiDestroyService в них буквально спасает например, когда мы хотели дергать markForCheck из фабрики токена в статье о DI фокусах для проброса данных.

Ссылка на документацию

Плагины ng-event-plugins

Фактически это внешняя библиотека ng-event-plugins, которая поставляется вместе с cdk (прямая зависимость, которую не нужно устанавливать отдельно). Она добавляет свои обработчики к менеджеру плагинов Angular. В ней есть несколько очень полезных плагинов, которые добавляют ряд возможностей в шаблоны компонентов.

Например, .stopи .preventпозволяют декларативно делать stopPropagation и preventDefault на любой прилетающий ивент.

Было:

<some-input (mousedown)="handle($event)">    Choose date</some-input>
export class SomeComponent {   //    handle(event: MouseEvent) {      event.preventDefault();      event.stopPropagation();      this.onMouseDown(event);   }}

Стало:

<some-input (mousedown.prevent.stop)="onMouseDown()">    Choose date</some-input>

Или модификатор .silent который позволяет не запускать проверку изменений на событие:

<div (mousemove.silent)="onMouseMove()">    Callbacks to mousemove will not trigger change detection</div>

Можно отслеживать ивенты в capture-фазе с помощью .capture:

<div (click.capture.stop)="onClick()">    <div (click)="never()">Clicks will be stopped before reaching this DIV</div></div>

Все это работает и с @HostListenerами, и с кастомными событиями. Вы можете почитать подробнее в документации ng-event-plugins.

Итого

Мы посмотрели ряд сущностей пакета @taiga-ui/cdk. Надеюсь, какие-нибудь из них вам приглянулись и тоже будут помогать во всех дальнейших проектах!

Кстати, у меня еще есть статья про саму библиотеку Taiga UI, в которой описаны остальные пакеты и общая философия библиотеки.

Подробнее..

Тупые и умные компоненты

21.09.2020 00:19:49 | Автор: admin

Меня зовут Илона, я Senior Experience Designer в EPAM. Работа для меня удачно совпадает с хобби в EPAM я проектирую интерфейсы для зарубежных заказчиков, читаю лекции для сотрудников и студентов лабы, менторю дизайнеров. В свободное время преподаю проектирование интерфейсов в магистратуре Университета ИТМО и веду Телеграм-канал о UX-дизайне.
В работе и преподавании я часто сталкиваюсь с проблемой: сложно организовать компоненты интерфейса так, чтобы было всегда понятно, какой компонент использовать, чтобы похожие компоненты не плодились и не путали дизайнеров и разработчиков.

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

Что вообще такое компоненты

Графический интерфейс (GUI), как правило, состоит из кнопок, полей, чекбоксов, текстовых блоков и пр. Именно это мы называемкомпоненты эдакая интерактивная (или нет) обёртка контента:кнопка Оформить заказ; чекбокс Я принимаю условия соглашения и т.д.

Для UX-дизайнера интерфейс в первую очередь инструмент решения пользовательских задач. Задача всегда существует в контексте: регистрация в контексте сайта авиакомпании, покупка в контексте интернет-магазина одежды. Поэтому дизайнеру очень важно использовать реальные заголовки, названия кнопок, пункты списков. Именно так мы иллюстрируем решение определённой задачи в определённом контексте.

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

Посмотрим на простом примере

Дизайним карточку товара в интернет-магазине: картинка, информация, цена и кнопка Купить.

А ещё требуется карточка товара для корзины. Там нет кнопки Купить, зато есть кнопка Удалить и селектор количества товара. Звучит как новый компонент. Делаем!

Спустя какое-то время дизайним новую фичу личный кабинет. Карточка требуется уже не для товара, а для пользователя. Совсем другой компонент. Делаем!

И вот у нас уже 3 компонента Карточка.

Карточки во всем своем многообразииКарточки во всем своем многообразии

Теперь мы хотим показать информацию о заказе в личном кабинете. Неплохо смотрелась бы Карточка!
Какую же из трёх использовать? Ни одна толком не подходит. Проще уже сделать новую.


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

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

Три группы правок в нескольких местах на макетах и в системеТри группы правок в нескольких местах на макетах и в системе

Изменений бы потребовалось в три раза меньше, если бы карточку переиспользовали.

Зачем и как переиспользовать компоненты

Переиспользование компонентов помогает:

  1. облегчить жизнь себе и разработчикам;

  2. пользователям предсказывать поведение интерфейса (увидел компонент понял как работает знаю чего ожидать)

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

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

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

Шаблон карточки и примеры его примененияШаблон карточки и примеры его применения

Пример с карточками сделан в Figma. Тупая карточка Figma-component с применениемAuto Layout.Благодаря этому элементы карточки можно удалять и менять, а её размер подстроится под изменения. Умная карточка Figma-instance.

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

Скругление всех картинок одним движениемСкругление всех картинок одним движением

Тупой UI kit

Простая и понятная библиотека компонентов (UI kit), элементы которой легко переиспользовать и обновлять турбо-ускоритель дизайна и разработки. И состоит такая библиотека из тупых компонентов. Я называю её Тупой UI kit.
Если на вашем проекте уже есть UI kit, попробуйте сделать все компоненты в нём тупыми. Скорее всего, вы с удивлением обнаружите, что многие компоненты можно унифицировать: объединить похожие или удалить повторяющиеся. Отупить UI kit может быть непросто, понадобится время на ревизию системы и сильный дизайн-инструмент.Figmaотлично справляется, но иSketchне отстает.

Тупой UI kit облегчит работу над дизайном, избавив от необходимости плодить компоненты и изобретать велосипед. Разработчики тоже скажут вам спасибо за то, что они могут переиспользовать код компонента.
Тупой UI kit также может стать основой для созданияStorybookпроекта.

Выводы

Разделяя компоненты на умные и не очень, вы:

  1. создаете унифицированный интерфейс;

  2. оптимизируете дизайн и разработку, не выдумывая новые компоненты без необходимости;

  3. оставляете возможность легко вносить изменения в дизайн и код;

  4. делаете поведение компонентов предсказуемым для пользователей.


Больше о проектировании интерфейсов и UX можно почитать в моём телеграм-канале Поясни за UX.

Подробнее..

Android-разработчикам как сократить время реализации тёмной темы с пары месяцев до недели

20.10.2020 14:07:37 | Автор: admin

Привет, меня зовут Влад Шипугин, я Android-разработчик в Redmadrobot. В этой статье я хочу поделится опытом реализации тёмной темы, создания удобного UI Kit, как для разработки, так и для дизайнеров. Я расскажу про использование Material Components и работу с Vector Drawable. Также вы узнаете, как быстро поддержать режим edge-to-edge с использованием Window Insets и познакомитесь с моей библиотекой edge-to-edge-decorator.

Где-то полгода назад мы начали разработку тёмной темы для приложения Ростелеком Ключ. Наши дизайнеры уже писали про ценность тёмной темы, как её спроектировать и передать в разработку. В этой статье я продолжу рассказ от лица разработчика, расскажу с какими проблемами мы столкнулись в процессе реализации тёмной темы, почему это заняло у нас 3 месяца и как реализовать тёмную тему всего за одну неделю.

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

Не важно, давно ли вы начали разработку под Android или только делаете ваше первое приложение, думаю, вам будет интересно почитать эту статью, потому что в ней я поделился всем накопленным опытом и самым актуальным набором материалов по дизайну Android-приложений.

Как сделать удобный UI Kit

Изначально у нас был простой план: дизайнеры делают тёмную тему, а мы просто добавляем файл value-night/color.xml и всё. Но позже мы столкнулись с проблемами. И теперь, когда приложение давно опубликовано в сторе, я могу рассказать о решении этих проблем, чтобы вы никогда не наступали на наши грабли.

Проблема 1: сложность при выборе названий для палитры цветов

Первый вариант именования цветов использование названия цвета как есть: realblue, darkgrey и так далее.

Пример палитры цветов из ZeplinПример палитры цветов из Zeplin

Думаю, каждый сталкивается с проблемой, когда при использовании такого подхода, в будущем, при редизайне, получается, что realblue меняет свой hex на hex красного цвета или жёлтого. Это можно исправить, если переименовать цвет во всем приложении, но тогда возрастает вероятность ошибиться, а отлавливать такие ошибки крайне тяжело.

Второй вариант это абстрактные названия, мы решили использовать C1, C2 (С от слова Color) и так далее. Эта абстракция позволяет отвязать значение цвета от его названия. Сегодня это может быть красный, а потом желтый это не важно, и вам не нужно рефакторить всё приложение.

Пример палитры цветов из ZeplinПример палитры цветов из Zeplin

Но тут появляется другая проблема ты не можешь держать таблицу соответствий цветов в голове. Приходится всё время её открывать: при обсуждении с дизайнером или при разработке всегда нужно проверять конкретный цвет приложения.

Редко, когда удается запомнить конкретную цифру цвета. Какой тут должен быть цвет: С4 или С7? станет самым частым вопросом на обсуждениях дизайна. Мы долгое время пытались найти баланс между понятным названием цвета, таким как realblue и максимально абстрактным цветом для простоты рефакторинга и редизайна: С1, C2, C3 и так далее.

Есть и альтернативный, третий вариант когда названия цветов зависят от компонента, например, гайдлайны Material Design или Apple HIG.

material.iomaterial.io

Этот вариант казался самым логичным, но в нём было больше всего проблем:

  1. Цвета даёт дизайнер. И начинающим дизайнерам сложно придумывать названия цветов, а подключать всегда арт-директора для такой мелочи невыгодно.

  2. Некоторые цвета могут содержать одинаковый hex. В целом, в этом нет проблемы, но как оказалось, в палитре цветов Zeplin не может содержаться два цвета с одинаковым hex, но разными названиями.

  3. Сложно объединить рекомендации Material Design и Apple HIG в одну палитру, да и стандартных цветов может быть недостаточно для приложения.

Много встреч прошло за обсуждением названий цветов. Они должны были быть удобными для всех: Android- и iOS-разработчиков, и дизайнеров. Через некоторое время мы остановились на третьем варианте, но сформировали чёткие правила по наименованию цветов, чтобы не тратить много времени на придумывание названия. Также мы договорились добавлять цвета с одинаковым hex отличающимся на единицу.

Название цвета

iOS

Android

text/primary

textPrimary

text_primary

button/pressed

buttonPressed

button_pressed

Вот пример готовой палитры из Zeplin:

Пример финальной палитры из ZeplinПример финальной палитры из Zeplin

Позже мы отказались от Zeplin и перешли на Figma. Вот такая палитра в Figma у нас получилась. А подробнее про переход с Zeplin на Figma уже писал наш iOS разработчик Даниил Субботин @subdan в статье про утилиту экспорта UI Kit из Figma figma-export.

Проблема 2: непонятные стили шрифтов

Особых проблем в работе со штифтовыми стилями у нас не было. Дизайнеры обычно заносят все шрифтовые стили приложения в UI Kit, но их наличие не гарантирует, что в макетах будут использоваться только они. Периодически то там, то тут, Zeplin не определял указанный шрифт и приходилось отвлекать дизайнера, чтобы узнать какой именно шрифт необходимо использовать.

Сейчас в макетах всегда отображаются правильные стили шрифтов. Дизайнеры добились этого за счёт перехода со Sketch + Zeplin на Figma она лучше распознает шрифтовые стили.

Да, иногда ошибки встречаются, но это происходит крайне редко, потому что все шрифты прописаны в мастер-компонентах и UI Kit. Это уже ответственность дизайнеров. Если я находил ошибку, то оставлял комментарий и дизайнер всё исправлял.

Пример наших шрифтовых стилей можно посмотреть тут.

Проблема 3: дублирование иконок и трудности с их именованием

Пока дизайнеры переделывали палитру цветов, я столкнулся с новой проблемой иконки должны быть разных цветов в зависимости от выбранной пользователем темы. Самый простой вариант это добавить альтернативный набор иконок в директорию drawable-night. Но за простоту нужно платить:

  1. Количество иконок увеличивается в два раза, а значит, приложение весит больше. И App Bundle не поможет, потому что тема меняется динамически и все иконки должны находиться в итоговом APK.

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

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

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

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

Ещё одна проблема названия иконок придумывают дизайнеры и не всегда они совпадают с ходом мысли разработчиков: дизайнеры называют иконку исходя из визуальной составляющей, а разработчики исходя из предметной области. Поэтому разработчики переименовывают иконки у себя в проекте.

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

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

Название иконки

iOS

Android

ic/24/flash_on

ic24flashOn

ic_24_flash_on

ic/24/flash_off

ic24flashOff

ic_24_flash_off

Пример с нашим набором иконок можно посмотреть здесь.

Проблема 4: организация иллюстраций

Перекрашивать иконки удобно, но если картинка содержит больше одного цвета, то такой вариант не подходит. В данном случае остается добавить иллюстрацию альтернативного цвета в директорию drawable-night и попросить дизайнера подготавливать иллюстрации в альтернативной теме.

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

Вот пример того, что у нас получилось.

Проблема 5: отсутствие базовых компонентов или неправильное их использование

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

При данном подходе UI Kit не является единым источником правды. Можно сказать, что в данном случае это UI Kit для галочки, и так делать нельзя.

Но как только дизайн-макеты начинают передавать в разработку, то единственным источником правды должен стать UI Kit и мастер-компоненты в нем. Иначе UI Kit будет вам мешать и замедлять работу, а не ускорять её.

Ценность UI Kit в том, что вы реализуете все базовые компоненты, а потом из этих компонентов собираете экраны. Для этого можно использовать мастер-компоненты в Figma, и styles или CustomViews в Android. В таком случае UI Kit экономит вам много времени.

Вот пример с описанием кнопок приложения, а полный UI Kit можно посмотреть тут.

Пример описания кнопок приложенияПример описания кнопок приложения

Как правильно реализовать UI Kit

После создания дизайнерами UI Kit можно подключаться и разработчикам. Мой план по реализации UI Kit с учетом темной темы был такой:

  1. Привести цвета, тему приложения и стили компонентов в порядок.

    • сделать палитру цветов (color.xml);

    • описать тему приложения;

    • описать стили компонентов, которые отличаются от базовой темы.

  2. Заменить все иконки на черный и окрашивать их в нужный цвет в момент отрисовки.

  3. Добавить альтернативные цвета values-night/color.xml.

  4. Добавить выбор темы в настройках приложения.

Я уже писал выше про утилиту Figma-export и статью, в которой раскрыты подробности её реализации. Эта утилита помогает избежать проблем с экспортом цветов, иконок и иллюстраций из Figma. С её помощью экспорт компонентов происходит автоматически, что позволяет полностью исключить человеческий фактор из этого процесса.

Реализуем палитру цветов

С помощью Figma-export создание палитры цветов происходит одной командой:

./figma-export colors -i figma-export.yaml

После этого в вашем приложении добавится или изменится файл color.xml.

Реализуем тему приложения

Для реализации темы в Android-приложении следует использовать библиотеку material-components. Именно так я и поступил: создал палитру цветов в color.xml и начал делать тему приложения. Но после этого я столкнулся с проблемой toolbar, cardview и ещё пару компонентов имели не тот цвет.

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

На тот момент, в библиотеке material-components, ещё не было документации по темам и стилям. Как и многие Android-разработчики, я не знал, как правильно описывать тему приложения. Разработчики из Google даже шутили про это на Android Dev Summit 2019.

Моя тема приложения была описана неправильно и материальные компоненты, такие как кнопки, иконки и текстовые поля, выглядели не так, как я планировал. Например, цвет toolbar использовал цвет primary для светлой темы и, почему-то, переключался на surface в темной.

Позже оказалось, что в теме приложения, по умолчанию, цвет toolbar принимает значение, равное атрибуту ?attr/colorPrimarySurface. Тогда, чтобы понять почему так происходит, мне пришлось ковыряться в исходниках материальных компонентов. Мне удалось понять, какой смысл вкладывали авторы в описание темы приложения, и потом статья про темы и стили в Android-приложениях подтвердила мои догадки.

Сейчас вы можете узнать интересные детали в новой статье по материальным компонентам от Google. Картинка из этой статьи наглядно показывает, как работает тема с атрибутом colorPrimarySurface.

Примеры работы PrimarySurface атрибутаПримеры работы PrimarySurface атрибута

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

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

Доп. информацию по материальным компонентам можно найти тут и в конце статьи:

Также, при реализации темы приложения, следует подключить дизайнера, чтобы он помог создать описание темы приложения и сконвертировать удобную палитру цветов для дизайна, в тему материальных компонентов. Здесь на помощь приходит раздел материальных компонентов, MaterialThemeBuilder и примеры от Google:

В итоге, я остановился на таком варианте описания темы и стилей.

Структура организации ресурсов:

  1. colors.xml цвета приложения;

  2. type.xml шрифты приложения;

  3. shape.xml формы приложения;

  4. themes.xml темы приложения;

  5. styles_button.xml кнопки приложения;

  6. styles_text_input.xml текстовые поля;

  7. styles_list_item.xml элементы списков;

  8. styles.xml прочие стили виджетов.

Итоговая тема приложения
<style name="Theme.App" parent="Theme.MaterialComponents.DayNight.NoActionBar">    <!-- Base color attributes -->    <item name="colorPrimary">@color/tint</item>    <item name="colorSecondary">@color/tint</item>    <item name="colorControlHighlight">@color/tint_ripple</item>    <item name="android:colorBackground">@color/background_primary</item>    <item name="colorSurface">@color/background_secondary</item>    <item name="colorError">@color/error</item>    <item name="colorOnPrimary">@color/text_primary</item>    <item name="colorOnSecondary">@color/text_primary</item>    <item name="colorOnBackground">@color/text_secondary</item>    <item name="colorOnError">@color/text_primary</item>    <item name="colorOnSurface">@color/text_secondary</item>    <item name="android:windowBackground">@color/background_primary</item>    <item name="android:statusBarColor">@android:color/black</item>    <item name="android:navigationBarColor">@android:color/black</item>    <item name="android:enforceNavigationBarContrast" tools:targetApi="q">false</item>    <item name="android:listDivider">@drawable/divider_horizontal_primary</item>    <!--Material shape attributes-->    <item name="shapeAppearanceSmallComponent">@style/ShapeAppearance.App.SmallComponent</item>    <item name="shapeAppearanceMediumComponent">@style/ShapeAppearance.App.MediumComponent</item>    <item name="shapeAppearanceLargeComponent">@style/ShapeAppearance.App.LargeComponent</item>    <!--Component styles-->    <item name="appBarLayoutStyle">@style/Widget.App.AppBarLayout</item>    <item name="toolbarStyle">@style/Widget.App.Toolbar</item>    <item name="drawerArrowStyle">@style/Widget.App.DrawerArrowToggle</item>    <item name="toolbarNavigationButtonStyle">@style/Widget.App.Toolbar.Button.Navigation.Tinted</item>    <item name="bottomNavigationStyle">@style/Widget.App.BottomNavigationView</item>    <item name="cardViewStyle">@style/Widget.App.CardView</item>    <item name="textInputStyle">@style/Widget.App.TextInputLayout</item>    <item name="editTextStyle">@style/Widget.App.TextInputEditText</item>    <item name="switchStyle">@style/Widget.App.Switch</item>    <item name="materialCalendarTheme">@style/ThemeOverlay.App.Calendar</item>    <item name="dialogTheme">@style/ThemeOverlay.App.Dialog</item></style>

Реализуем стили шрифтов

В теме приложения с material-components стандартным решением для реализации шрифтов является textAppearance. В нашем приложении используются в два раза меньше шрифтов и всего три цвета для текста. А ещё textAppearance можно описывать не все атрибуты например, там нет свойства android:lineSpacingMultiplier. Поэтому, я решил не использовать textAppearance, а использовал просто стили, которые прописывались каждому текстовому полю.

Например, мы нигде не использовали стиль Header2 и вместо него применяли унаследованный от него стиль с указанием цвета: Header2.Primary или Header2.Secondary. Такой вариант позволял сразу определить и цвет текстового поля и его шрифт.

Иногда приходилось делать исключения и использовать просто стиль, как Header2, с указанием цвета прямо в верстке экрана, например, для отображения ошибок, но таких мест всего 23 в приложении.

Стиль шрифта содержит следующие атрибуты:

  • textSize размер;

  • lineHeight межстрочный интервал, когда в текстовом поле две строки;

  • android:minHeight и android:gravity нужны, чтобы указать межстрочный интервал, когда в текстовом поле всего одна строка (да, lineHeight в таком случае игнорируется и приходится выкручиваться костылями :))

  • android:fontFamily начертание шрифта.

    Вот один из примерв описания шрифта:

<style name="Header2">    <item name="android:textSize">18sp</item>    <item name="lineHeight">24sp</item>    <item name="android:minHeight">24sp</item>    <item name="android:gravity">center_vertical</item>    <item name="android:fontFamily">@font/basis_grotesque_pro_bold</item></style><style name="Header2.Primary">    <item name="android:textColor">@color/text_primary</item></style><style name="Header2.Secondary">    <item name="android:textColor">@color/text_secondary</item></style>

Переиспользуем иконки при помощи окрашивания

Как я писал ранее, все иконки в приложении следует делать одного цвета, и потом их раскрашивать. Чтобы добавить все иконки из Figmа, вновь воспользуемся утилитой Figma-export:

./figma-export icons -i figma-export.yaml

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

В Android давно добавили поддержку перекрашивания иконок, это различные tint, но до сих пор это работает плохо и не на всех версиях Android, поэтому я написал extension для работы с drawable через drawable compat, и придерживался следующего алгоритма:

  1. Если можешь сделать tint в верстке делай tint.

  2. Если это кнопка или компонент с несколькими состояниями, то тут поможет selector.

  3. Если требуется программная смена цвета, то необходимо использовать DrawableCompat для корректной установки цвета, и обязательно нужно сделать mutate, иначе иконка закешируется, и поменяет цвет во всем приложении. Для этого я написал следующие extension-функции:

fun Drawable.withTint(context: Context, @ColorRes color: Int): Drawable {    return DrawableCompat.wrap(this).mutate().apply {        DrawableCompat.setTint(this, ContextCompat.getColor(context, color))    }}fun Int.toDrawableWithTint(context: Context, @ColorRes color: Int): Drawable {    return requireNotNull(AppCompatResources.getDrawable(context, this)).withTint(context, color)}

Добавляем иллюстрации

С добавлением иллюстраций тоже всё просто. В Figma-export есть нужная команда для их добавления в проект:

./figma-export images -i figma-export.yaml

Вызываем команду, и она добавляет иллюстрации в values и, если, вы поддерживаете тёмную тему, то и в values-night.

Реализуем стили компонентов

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

Пример того, как я организовал стили компонентов кнопок в styles-button.xml
<resources>   <style name="Widget.AppTheme.Button" parent="Widget.MaterialComponents.Button.UnelevatedButton">       <item name="backgroundTint">@drawable/selector_button</item>       <item name="rippleColor">@color/button_ripple</item>       <item name="android:textAllCaps">false</item>       <item name="android:textAppearance">@style/Body1</item>       <item name="android:textColor">@color/button_text_color</item>       <item name="android:paddingStart">16dp</item>       <item name="android:paddingTop">12dp</item>       <item name="android:paddingEnd">16dp</item>       <item name="android:paddingBottom">12dp</item>       <item name="android:insetTop">0dp</item>       <item name="android:insetBottom">0dp</item>   </style>   <style name="Widget.AppTheme.Button.Secondary">       <item name="backgroundTint">@color/background_secondary</item>       <item name="rippleColor">@color/tint_ripple</item>       <item name="android:textColor">@color/button_secondary_text_color</item>   </style>   <style name="Widget.AppTheme.Button.Onboarding">       <item name="backgroundTint">@color/onboarding_button</item>       <item name="rippleColor">@color/tint_ripple</item>       <item name="android:textColor">@color/button_secondary_text_color</item>   </style>   <style name="Widget.AppTheme.Button.Accent">       <item name="backgroundTint">@color/accent</item>       <item name="rippleColor">@color/tint_ripple</item>   </style>   <style name="Widget.AppTheme.TextButton" parent="Widget.MaterialComponents.Button.TextButton">       <item name="rippleColor">@color/tint_ripple</item>       <item name="android:textAllCaps">false</item>       <item name="android:textAppearance">@style/Body1</item>       <item name="android:insetTop">0dp</item>       <item name="android:insetBottom">0dp</item>       <item name="android:paddingStart">16dp</item>       <item name="android:paddingEnd">16dp</item>   </style>   <style name="Widget.AppTheme.Button.TextButton.Dialog" parent="Widget.MaterialComponents.Button.TextButton.Dialog">       <item name="android:textAllCaps">true</item>       <item name="android:textAppearance">@style/Body1</item>       <item name="android:paddingStart">16dp</item>       <item name="android:paddingEnd">16dp</item>       <item name="rippleColor">@color/tint_ripple</item>   </style>   <style name="Widget.AppTheme.TextButton.Icon" parent="Widget.MaterialComponents.Button.TextButton.Icon">       <item name="rippleColor">@color/tint_ripple</item>       <item name="android:textAllCaps">false</item>       <item name="android:textAppearance">@style/Body1</item>       <item name="android:textColor">@color/tint</item>       <item name="android:insetTop">0dp</item>       <item name="android:insetBottom">0dp</item>       <item name="android:paddingStart">16dp</item>       <item name="android:paddingEnd">16dp</item>   </style>   <style name="Widget.AppTheme.ToolbarButton" parent="Widget.MaterialComponents.Button.TextButton">       <item name="rippleColor">@color/tint_ripple</item>       <item name="android:textAllCaps">false</item>       <item name="android:textAppearance">@style/Header1</item>       <item name="android:textColor">@color/toolbar_button_text_color</item>       <item name="android:paddingStart">16dp</item>       <item name="android:paddingEnd">16dp</item>   </style></resources>

Также стоит учитывать, что в верстке вы можете использовать просто Button или AppCompatButton, потому что есть такой компонент, как MaterialComponentsViewInflater, который автоматически будет переводить их в MaterialButton, если ваша тема наследуется от "material-components".

Вот кусочек кода из него:

@NonNull@Overrideprotected AppCompatButton createButton(    @NonNull Context context,     @NonNull AttributeSet attrs  ) {    if (shouldInflateAppCompatButton(context, attrs)) {    return new AppCompatButton(context, attrs);  }  return new MaterialButton(context, attrs);}

Как поддержать режимedge-to-edge

На этапе проектирования палитры цветов, нам очень сильно мешали цвета statusBar, да и в целом, окрашивание statusBar всегда вызывало проблемы на разных версиях Android. Раньше его цвет был равен colorPrimaryDark, а теперь Google отказались от этого варианта и рекомендуют использовать режим edge-to-edge. Кроме этого, мой OnePlus получил обновление до Android 10, поэтому я решил попробовать добавить поддержку режима edge-to-edge.

Режим edge-to-edge это новая концепция из материального дизайна, которая заключается в том, что вы отрисовываете контент под системными компонентами: statusBar и navigationBar и телефон становится визуально более безрамочным.

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

Для того, чтобы добавить поддержку режива edge-to-edge в ваше приложение нужно:

  1. Добавить поддержку системных отступов (insets).

  2. Активировать режим edge-to-edge для statusBar и navigationBar. По факту вам нужно сделать их прозрачными.

Добавляем поддержку Window Insets

Если простыми словами, то при работе с Window Insets, вы получаете размер системных компонентов и вставляете их как padding в верстку для ваших компонентов экрана AppBar или RootView. Insets поддерживается всеми версиями Android, что позволяет реализовать концепцию edge-to-edge для всех пользователей. Подробности можно почитать или посмотреть в докладе Константина Цховребова с AppsConf.

Сначала я использовал его решение, а потом, когда вышла новая версия библиотеки Insetter от Криса Бэйнса, перешел на неё. Вам предлагаю сразу использовать Insetter.

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

Материалы по режиму edge-to-edge лежат тут и в конце статьи:

Окрашиваем statusBar и navigationBar

Эффект безрамочности в режиме edge-to-edge достигается за счет того, что вы отрисовываете контент под statusBar и navigationBar и делаете их прозрачными. При этом, нужно сохранять контрастность иконок в этих компонентах.

Тут существует одна проблема, которая находится глубоко в системе и исправить её после релиза OS уже нельзя. Это изменение цвета иконок в системных компонентах (statusBar и navigationBar) со светлого на темный. Поэтому, нужно учитывать следующие правила, в зависимости от версии Android:

  • до 6.0 версии Android иконки statusBar и navigationBar всегда светлые и перекрасить их в темный цвет нельзя. Флаг View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR доступен с 23 API. Если у вас контент всегда темного цвета, то проблем не будет. Но чтобы сохранить контрастность иконок на фоне контента, следует добавлять на системные компоненты наложение фона, например, черного фона с 50% прозрачности;

  • с версии Android 6.0 можно задать, какими будут иконки в statusBar: белыми или черными. Однако navigationBar будет вести себя как в предыдущих версиях, поэтому наложение можно убрать только для statusBar. Флаг View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR доступен с 26 API;

  • с версии Android 8.0 можно выбрать белый или черный цвет иконок для обоих компонентов. Поэтому наложения можно убрать полностью.

Я нашел интересный пример WindowPreferencesManager, который реализовывал эту логику в приложении-каталоге материальных компонентов. Но там было много лишнего и разбираться в этом, думаю, захочет не каждый, поэтому я сделал мини утилиту edge-to-edge-decorator. Она хорошо кастомизируется под ваши нужды и реализует логику окрашивания statusBar и navigationBar за вас. Подробнее про реализацию можно почитать в документации.

Пример работы библиотеки:

Android 5.0

(API level 21)

Android 7.1

(API level 25)

Android 9

(API level 28)

Android 11

(API level 30)

Добавляем тёмную тему приложения

Теперь, после того, как у вас готов UI Kit приложения и вы изучили и поддержали новый подход с использованием Material Components, можно вернуться к реализации темной темы в приложении.

Я рекомендую следовать согласно следующему алгоритму:

  1. Изучаем гайды и статьи по проектированию темной темы (ссылки лежат в конце статьи).

  2. Дизайнер готовит первый прототип и цветовую схему темной темы приложения.

  3. Создание полноценной темной палитры цветов.

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

Если основная тема вашего приложения описана правильно, то добавление темной темы не создаст проблем: нужно просто добавить color.xml в values-night, как мы и планировали в самом начале (как же мы тогда ошибались :))

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

1) Поменять базовую тему приложения на DayNight.

<style name="Theme.App" parent="Theme.MaterialComponents.DayNight.NoActionBar">

2) Установить нужный режим отображения через метод AppCompatDelegate.setDefaultNightMode.

В системе доступно 4 варианта темы:

  • всегда светлая: AppCompatDelegate.MODE_NIGHT_NO;

  • всегда тёмная: AppCompatDelegate.MODE_NIGHT_YES;

  • выбирается в зависимости от режима энергосбережения (Android 9 и ниже): AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY;

  • переключается в зависимости от настроек системы (Android 10 и выше): AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM;

Добавляем выбор темы приложения

Светлая тема

Темная тема

После того, как вы реализовали тёмную тему, вишенкой на торте станет выбор темы в настройках приложения. Почему это важно? Потому что ресурсы values-night были добавлены ещё в API level 8, но включение темной темы на уровне системы реализовали только в Android 10. Чтобы темная тема работала у всех пользователей, необходимо добавить возможность её выбора в приложении.

Для удобного API я написал вот такой класс:
enum class NightModeType(   val customOrdinal: Int,   @NightMode val value: Int,   @StringRes val title: Int) {   MODE_NIGHT_NO(       0,       AppCompatDelegate.MODE_NIGHT_NO,       R.string.mode_night_no   ),   MODE_NIGHT_YES(       1,       AppCompatDelegate.MODE_NIGHT_YES,       R.string.mode_night_yes   ),   MODE_NIGHT_FOLLOW_SYSTEM(       2,       AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM,       R.string.mode_night_follow_system   ),   MODE_NIGHT_AUTO_BATTERY(       2,       AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY,       R.string.mode_night_auto_battery   );   companion object {       fun fromValue(@NightMode value: Int) = values().firstOrNull { it.value == value } ?: getDefaultMode()       fun fromCustomOrdinal(ordinal: Int): NightModeType {           return if (ordinal == 2) {               getDefaultMode()           } else {               values().firstOrNull { it.customOrdinal == ordinal } ?: getDefaultMode()           }       }       fun getDefaultMode(): NightModeType {           return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {               MODE_NIGHT_FOLLOW_SYSTEM           } else {               MODE_NIGHT_AUTO_BATTERY           }       }   }}
А выбор темы можно реализовать таким образом:
private fun createNightModeChooserDialog(command: ShowNightModeChooserDialog): AlertDialog {   return AlertDialog       .Builder(ContextThemeWrapper(requireContext(), R.style.ThemeOverlay_AppTheme_AlertDialog))       .apply {           setTitle(getString(R.string.item_dark_theme_text_view_title_text))           val nightModes = arrayOf(               getString(NightModeType.MODE_NIGHT_NO.title),               getString(NightModeType.MODE_NIGHT_YES.title),               getString(NightModeType.getDefaultMode().title)           )           val selectedMode = command.selectedMode.customOrdinal           setSingleChoiceItems(nightModes, selectedMode) { dialog, which ->               val nightMode = NightModeType.fromCustomOrdinal(which)               persistentStorage.saveNightMode(nightMode.value)               AppCompatDelegate.setDefaultNightMode(nightMode.value)               dialog.dismiss()           }           setNegativeButton(               getString(R.string.fragment_dialog_night_mode_chooser_button_cancel_text),               null           )       }       .create()}

И тут тоже есть проблема: выбранная пользователем тема нигде не запоминается, поэтому её необходимо сохранять. Сделать это можно вот такой проверкой в методе onCreate у вашего activity:

override fun onCreate(savedInstanceState: Bundle?) {   checkNightMode()   setTheme(R.style.AppTheme)   super.onCreate(savedInstanceState)}private fun checkNightMode() {   val savedNightModeValue = persistentStorage.getSavedNightMode(AppCompatDelegate.MODE_NIGHT_UNSPECIFIED)   val selectedNightMode = NightModeType.fromValue(savedNightModeValue)   AppCompatDelegate.setDefaultNightMode(selectedNightMode.value)}

Заключение

Вместо пары недель на реализацию тёмной темы у нас ушло три месяца. Но мы не просто сделали тёмную тему на проекте Ростелеком Ключ, но и подняли дизайн приложения на новый уровень:

  • сформировали четкий и полный UI kit в Figma;

  • автоматизировали экспорт UI kit в Figma и опубликовали утилиту figma-export;

  • правильно реализовали все базовые компоненты в Android приложении;

  • поддержали новый режим edge-to-edge, и опубликовали библиотеку edge-to-edge-decorator, которая поможет быстро добавить режим edge-to-edge на других проектах.

Так сказать, мы вышли из зоны комфорта, ради темной темы, и поправили кучу моментов в основном процессе работы :)

Материалы для глубокого изучения

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

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru