Android 10 уже давно предлагает темную тему для устройств, и со временем число ее пользователей растет. Темный режим имеет много преимуществ, таких как снижение расхода заряда батареи, улучшение видимости, снижение нагрузки на глаза и т.д.
В этой статье будет рассказано и объяснено, как команда Tokopedia реализовала в своем Android-приложении поддержку темного режима.
Вступление
Мы знаем, что у пользователей есть огромный спрос на функцию темного режима. Что же, ожидание окончено. Tokopedia официально объявила о поддержке темного режима на этапе бета-тестирования. Мы потратили много времени и усилий на преобразование нашего приложения Tokopedia, состоящего из сотен модулей, для обеспечения поддержки темного режима. Было много проблем, с которыми мы столкнулись в этом внедрении. Я поделюсь ими всеми в этой статье, а также нашими решениями.
Что нужно для начала
Базовое понимание темного режима
Лучше всего иметь базовые знания о том, как создать приложение, поддерживающее темный режим. Поэтому в этой статье мы расскажем только об основных препятствиях на пути преобразования нашего приложения. Официальную документацию по темным темам можно найти по этой ссылке.
Вспомогательная библиотека цветов
У нас также есть внутренняя библиотека для хранения и управления цветами. Эта библиотека представляет собой модуль, который содержит colors.xml с values-night ресурсами. Мы рекомендуем использовать этот подход. Таким образом, цвета в вашем приложении централизованы в одном месте, и требуется меньше усилий для их обслуживания.
Принудительно перевести приложение в светлый режим
Пока мы были на этапе разработки, мы применили код, который заставлял приложение работать только в светлом режиме. Принудительный переход в светлый режим не позволит приложению использовать темный режим в продакшене, пока разработчики все еще находятся на этапе разработки.
Представьте, если у нас есть десять страниц, и только две из них уже поддерживают темный режим, а устройство пользователя находится как раз в ночной теме. Тогда у пользователя будет нарушена работа с режимами, перемешанными в приложении. Если мы не переведем приложение в светлый режим, оно автоматически будет следовать системной теме устройства.
Используйте приведенный ниже код, чтобы перевести ваше приложение в светлый режим (поместите его в свой класс application).
AppCompatDelegate.setDefaultNightMode( AppCompatDelegate.MODE_NIGHT_NO);
После того, как все экраны смогут поддерживать темную тему, мы сможем начать использовать системную тему по умолчанию и удалить код, который мы создали для принудительного перехода в светлую тему.
Как с минимальными усилиями преобразовать сотни модулей в темный режим?
Поскольку у нас есть сотни модулей, принадлежащих нескольким командам, ручной подход невозможен и требует слишком много усилий. Ручной подход означает, что мы должны менять цвета один за другим, создавать папки ночных ресурсов для цветов, проверять и менять тему. Преобразование может занять целую вечность, и вероятность совершить ошибку высока.
Наше решение — создать скрипт для перевода каждого конкретного модуля в темный режим.
Скрипт автоматизации
Скрипт создан на Python. По сути, он переходит к выбранному модулю и сканирует все файлы, которые обычно содержат цвета, такие как .kt, .java, .xml. Скрипт будет игнорировать файлы с типами, отличными от упомянутых выше.
Если скрипт найдет файл, например, ProductFragment.kt, он откроет его и прочитает строки одну за другой для поиска любого упоминания цвета.
for line in fileinput.input({path_to_product_fragment}, inplace=1): # find if line contains color with regex
Регулярное выражение используется для получения конкретного цвета на конкретном шаге, потому что каждый шаг имеет разные ссылки на цвета, которые необходимо заменить. Например, есть такое регулярное выражение:
'com\.[\w_]+\.[\w_]+\.R\.color\.[\w_]+|R\.color\.\w+|\.color\.\w+|@color/\w+|android.R.color.\w+|@android:color/\w+
После того, как в строке найден цвет, скрипт проверит, доступен ли он в нашей библиотеке темного режима. Если он доступен, то цвет необходимо преобразовать в правильный. Скрипт получит соответствующий цвет и заменит его. Пример библиотеки:
Old Color, Dark Mode ColorR.color.Blue_B100,R.color.Unify_B100 @color/Blue_B100,@color/Unify_B100 R.color.Blue_B200,R.color.Unify_B200 @color/Blue_B200,@color/Unify_B200 R.color.Blue_B300,R.color.Unify_B300 @color/Blue_B300,@color/Unify_B300 R.color.Blue_B400,R.color.Unify_B400
Например, в этой строке файла xml мы получаем цвет как @color/Blue_B100, и скрипт изменяет его на основе библиотеки цветов.
android:textColor="@color/Blue_B100" <- before converted android:textColor="@color/Unify_B100" <- after converted
Как это работает
На самом высоком уровне все можно представить в виде блок-схемы:
В Tokopedia мы сделали три шага, чтобы полностью преобразовать все цвета в поддержку темного режима:
1. Преобразование всех существующих цветов, которые не поддерживаются в темном режиме, в библиотечные.
Как мы упоминали ранее, у нас есть внутренняя библиотека, которая централизовала цвета. К сожалению, предыдущие цвета, которые мы использовали, не всегда поддерживаются в темном режиме, потому что у них нет night значения. Для преобразования мы запускаем скрипт, как мы описали выше.
2. Преобразование всех кастомных цветов в наши внутренние цвета.
В Tokopedia у нас есть сотни модулей, каждый модуль имеет свои собственные цвета (обычно расположенные в res -> values -> colors.xml). Это еще одна проблема, если мы хотим преобразовать их для поддержки темного режима. Таким образом, скрипт будет работать так:
- Получит все пользовательские цвета в нашем исходном коде, а затем поместит их в одном месте.
- Получит шестнадцатеричное значение каждого цвета и с помощью скрипта найдет ближайший аналогичный цвет в библиотеке цветов.
- Изменит цвет в классе на получившийся.
3. Преобразование все жестко прописанных цветов в наши внутренние цвета.
В этом случае мы будем менять жестко закодированный шестнадцатеричные цвета в нашем классе, например, android:background=»#123456″. Подход очень похож на первый:
- Пройти по всем классам Android
- Найти шестнадцатеричный цвет, используя это регулярное выражение: r’#([0–9a-fA-F]{3}|[0–9a-fA-F]{6}|[0–9a-fA-F]{8})\b’
- Получить шестнадцатеричный цвет и перейти в сценарий выбора ближайшего цвета. Скрипт вернет правильный цвет из нашей библиотеки. Например, #123213 превратится в @color/Unify_Black.
- Изменить цвет в классе на правильный.
Другие инструменты
- Скрипт ближайшего цвета. Вы можете создать свой собственный алгоритм поиска ближайшего цвета, указав список цветов. В Python уже есть библиотека scipy для поиска ближайшего цвета, которая использует алгоритм KNN. Пример доступен здесь. Превратите цвета в библиотеки и исходные в RGB, отправьте scipy и получите ближайший RGB из списка.
- Скрипт смены темы. Этот скрипт изменит нашу существующую тему на тему, рекомендованную Google для поддержки темного режима (Theme.AppCompat.DayNight).
- Панель отслеживания покрытия темного режима. Чтобы понимать поглощение темной темой нашего приложения и убедиться, что все готово для внедрения, мы создали дашборд. Он позволяет отслеживать, готовы ли модули или нет. Этот инструмент также связан с проверкой пул-реквестов в нашем Jenkins. Если обнаружен новый запрос на слияние с покрытием менее 100%, то PR будет заблокирован, и разрабатываемая фича не будет объединена с нашей основной веткой.
Лучшие практики
- Запретить создание стилей с одинаковыми именами. Это может привести к тому, что темный режим не будет работать.
- Если вы используете CardView, используйте app:cardBackgroundColor вместо android:background, чтобы применить цвет темного режима.
- Используйте кастомный цвет, который не имеет night-values значения в качестве цвета, который вы не хотите менять в темном режиме.
- Остерегайтесь strings.xml; всегда проверяйте этот класс, потому что ресурс цвета может находиться в файле strings.xml.
- Избегайте ссылки на цвет (@color/blue_night) в .xml, содержащем vector. Просто жестко пропишите цвет с помощью шестнадцатеричного кода (#003c94), чтобы предотвратить сбой в более ранних ОС.
- Создайте уникальный идентификатор, чтобы сценарий мог обходить выбранный цвет. Всякий раз, когда скрипт будет запускаться, ваш цвет не будет преобразовываться. Например, я использую _dms_ (поддержка темного режима) в качестве идентификатора: <color name=»product_detail_dms_tradein_blue»>#32afff</color>.
- Если ваше приложение содержит webview, необходимо поддерживать и в нем темный режим. Сам браузер должен поддерживать темный режим. Чтобы сообщить ему, нужно ли использовать темный режим или нет, приложению необходимо отправить дополнительный флаг в веб-просмотр в заголовке. У Google есть подход к такой проверке.
- Для того, чтобы темный режим был правильно реализован, нам нужна некоторая профилактика и автоматизация, чтобы заблокировать фичи, которые не используют темные цвета. Обычно мы блокируем автоматическое слияние пул-реквестов и ждем, пока разработчик не добавит нужную тему.
Результат
Вот некоторые результаты поддержки темного режима в нашем клиентском приложении Tokopedia.
Примерно так мы перевели наше приложение в темный режим. Автоматизация очень помогает нам и снижает количество человеческих ошибок. Но нам все еще нужно постоянно улучшать работу с темным режимом, потому что некоторые варианты необходимо преобразовывать вручную, и они не учитываются в сценарии.