Используем стили в Android (и не сходим при этом с ума)

Eugene Saturov
8 min readSep 18, 2015

--

Перевод из blog.danlew.net.

Доклад на Droidcon NYC 2015.

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

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

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

Пристегнитесь. Это длинный пост.

Когда использовать стили.

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

Правило #1: Используйте стили только для семантически идентичных элементов.

Это правило хорошо иллюстрируется следующими примерами:

  • Вы разрабатываете калькулятор. Все кнопки должны выглядеть одинаково, следовательно, имеет смысл использовать единый стиль CalculatorButtonStyle;
  • У вас есть несколько экранов, содержащих текстовые метки разных форматов — заголовки, подзаголовки и текст. Вы можете выделить атрибуты, описывающие внешний вид каждого формата в стили HeaderStyle, SubheaderStyle и TextStyle;
  • По всему вашему приложению вы показываете миниатюры изображений. Вы хотели бы, чтобы все они выглядели идентично. Так появился ThumbnailStyle;

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

Вам мало этой экономии? Используйте ресурсы!

Правило #2: Используйте ресурсы в стилях, когда это необходимо.

Вы можете описать стиль таким образом:

Но что если вы захотите менять значение атрибута minWidth в зависимости от размеров экрана? Вы можете продублировать стиль для каждой размерности экрана (например, sw600dp и sw900dp), но в таком случае вам придётся дублировать и атрибут minHeight, который будет иметь одинаковое значение для всех конфигураций. А что если вам потребуется изменить оба атрибута? В итоге, у вас в проекте образуется невероятное количество стилей MyButtons, всякий раз дублирующих полный набор атрибутов. Это прямой путь к катастрофе. Стоит забыть откорректировать хотя бы один атрибут хотя бы у одного стиля — всё пойдёт вкривь и вкось.

Стиль — это просто набор атрибутов. Намного проще описывать стиль таким образом:

Теперь вы используете один и тот же атрибут для всех наборов ресурсов. Было бы полным абсурдом дублировать весь layout, чтобы настроить, к примеру,изменение ширины одного View в портретном и ландшафтном режимах. Вы используете для этого dimens-ресурсы. Эту же стратегию стоит использовать и при работе со стилями.

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

Никто не говорит о том, что вам никогда не придётся дублировать стили для различных наборов ресурсов, но это явление стоит свести к минимуму. Обычно, единственной причиной прибегнуть к дублированию стилей в своих проектах является существенное различие используемых платформ (к примеру, если требуется замена атрибутов paddingLeft и paddingRight на paddingStart и paddingEnd).

Комбинирование стилей.

Было бы здорово, если бы мы могли применять несколько стилей к одному View, также, как в CSS.

Но мы не можем. Такие дела.

Хотя, постойте, в ряде случаев это ограничение удаётся обойти.

Правило #3: Используйте темы для модификации стилей по умолчанию.

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

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

Если такого стиля нет в AppCompat (или вы его не испольузете), задача несколько усложняется, так как вам нужно менять родительский стиль в зависимости от текущей темы. Ниже приведён пример кастомного стиля Button, который по умолчанию, наследуется от Holo, а когда это необходимо — от Material.

Эту тему вы создаёте в директории /values/values.xml:

А эту, в /values-v21/values.xml:

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

Если вы серьёзно намереваетесь явно определить все необходимые атрибуты (вместо того, чтобы модифицировать дефолтные), вам не следует использовать наследование вовсе.

Правило #4: По возможности используйте TextAppearance.

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

Сперва, вам придётся объявить TextAppearance:

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

Теперь, вы можете применить стиль к TextView:

Самое важное, что вы по-прежнему можете применить стиль к этой TextView. Фактически, перед вами возможность применения ДВУХ раных стилей к одному элементу! Конечно, не так здорово, как реальная поддержка нескольких стилей, но это всё, чем мы располагаем.

Где можно использовать TextAppearance? С любыми классами, которые наследуются от TextView. Это значит, что EditText, Button и т.д. поддерживают стилизацию текста через TextAppearance.

Распространённые ошибки.

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

Правило #5: НЕ создавайте стиль, если вы планируете использовать его единожды.

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

Какой код вызывает меньше вопросов: этот?

Или этот?

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

Правило #6: НЕ создавайте стиль только потому что несколько View используют один и тот же набор атрибутов.

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

Проблема в том, что эти View могут не всегда использоваться в одном контексте, а значит нет никакой гарантии, что однажды мы не захотим видоизменить какой-то из этих элементов. И в этот момент, общий стиль, который вы создали для всех этих View, не раз напомнит о себе рядом непредвиденных побочных эффектов.

Подумайте о таком сценарии: у вас есть несколько TextView с одинаковыми атрибутами форматирования текста и фоном. Вы думаете: “Здорово! Сейчас я выделю все повторяющиеся атрибуты в отдельный стиль и этим сильно оптимизирую код”. Поначалу всё чудесно, но рано или поздно наступил момент, когда вы захотели немного изменить какой-то один из этих TextView. Очевидно, проблема в том, что теперь один стиль распространяется на все View, поэтому вы не можете отредактировать один элемент без какого-либо ущерба для остальных.

Хорошо, тогда вы скажете: “Я просто переопределю нужные атрибуты прямо в XML!”. И проблема на самом деле будет решена. А потом это случится ещё. И ешё раз. Всё придёт к тому, что само существование этого стиля станет абсолютно бессмысленным, потому что он будет везде так или иначе переопределён. То, что выглядело как оптимизация, оборачивается в конце концов лишней работой.

Самое времея вернуться к правилу #1, которого гласит о том, что применять один стиль следует только лишь к семантически идентичным View. Говоря иначе, редактируя стиль вы должны быть уверены в том, что действительно хотите видеть эти изменения на всех View, к которым применён этот стиль.

Наследование: явное vs. неявное.

Стили поддерживают наследование — дочерний стиль содержит в себе все атрибуты родительского. Ещё бы они его не поддерживали.

Предположим, я хочу, чтобы все Button в моём приложении выглядели одинаково. Очевидно, что я обощую все атрибуты в ButtonStyle. Позже, я решил, что половина Button должна отличаться. Используя возможности наследования, я могу создать дочерний стиль ButtonStyle.Different, который будет содержать все атрибуты родительского стиля + внесённые мной изменения.

Как вы знаете, существует два способа организации наследования — явный и неявный:

Всё просто, так ведь? Но, как вы думаете, что произойдёт, если мы реализуем наследование сразу обеими способами?

Вы можете ответить, что у этого стиля два родителя и будете не правы. Родительский стиль в этом случае будет только один: AnotherParent.

Стиль может иметь только одного родителя. Неявное наследование (через атрибут) имеет приоритет над явным. Отсюда возникает следующее правило:

Правило #7: НЕ смешивайте явное и неявное наследование.

Использование сразу двух типов наследования приводит к путанице. Допустим, у нас есть layout:

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

Исходя из названия стиля, можно сделать вывод, что внешний вид Button определяется стилем MyWidgets.Button, но это не так! Имя стиля лишь вводит в заблуждение, и единственный способ узнать, какой же стиль на самом деле является родительским — найти его вручную, изучив содержимое стилевых XML-файлов.

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

Объектно-ориентированные стили! Выглядит интересно, неправда ли? На самом деле, всё, что вы получaаете — иллюзию того, что стили находятся в отношениях наследования, хотя это не так. Со стороны выглядит так, словно MyButton.Borderless унаследован от MyButton, но на самом деле у них нет ничего общего! В этом легко убедиться просто убрав точки из наименования стилей:

Теперь это не выглядит иерархией, зато понятно реальное положение вещей.

Стили vs. темы.

Стили и темы — это две совершенно разные концепции. Их отличие в том, что стили применяются к отдельным View, а темы — сразу к набору Views (или ко всей Activity).

Например, предположим, вы используете AppCompat и хотите задать “primary color” для экрана. Для этой цели наиболее оптимально применить тему ко всей Activity:

Темы используют ту же самую структуру данных, что и стили, даже тэг в XML используется тот же самый. Но суть в том, что используются темы в совершенно иных обстоятельствах! Они не оперируют с одними и теми же атрибутами. Например, вы можете задать textColor на View, но в теме атрибут textColor задан не будет. Аналогично, в теме будет задан атрибут colorPrimary, а в стилях он останется необъявлен. Поэтому:

Правило #8: НЕ смешивайте стили и темы.

Вот две самых распространённых ошибки:

  • Применение темы к View (как стиля):

Это просто не сработает, так как View не может работать с атрибутами темы. Ничего не произойдёт.

  • Комбинирование тем/стилей в иерархии наследования:

Глупо! Очень глупо! Обычно это не даст вообще никакого эффекта, но иногда всплывает в виде непредсказуемых последствий. Просто не делайте так!

Маленькая ремарка: в Lollipop появилась возможность применять темы к View и его дочерним классам. Но это не отменяет того, что вам не следует использовать стили и темы параллельно, применяя их к одному элементу:

В AppCompat тема применяется для стилизации Toolbar, но это всё, что вы можете использовать до тех пор, пока Lollipop не станет самой старой версией, которое будет поддерживать ваше приложение. Другими словами — поиграете этой фичей через пару лет :P

Заключение.

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

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

--

--