Android

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

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

  • Читабельность. Компоненты хорошего интерфейса легко читаются. Не нужно открывать вкладку Design в Android Studio, XML вполне понятен.
  • Тестируемость. Каждый компонент интерфейса легко тестируется без зависимостей или внешних компонентов.
  • Повторное использование. Компоненты интерфейса можно повторно использовать и расширять без изменения начального поведения.

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

1. Бизнес модель в Custom Views

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

Вытащить пользовательскую информацию с бэкенда нам помогает userApiClient:

userApiClient.getUser(userId)
.subscribe(
{ userModel -> userView.render(userModel) },
{ error -> Log.e("TAG", "Failed to find user", error) }
)

Если все прошло удачно, то получаем следующую модель:

data class UserModel(
val id: Int,
val name: String,
val age: Int,
val address: String,
val createdAt: Date,
val updatedAt: Date)

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

Почему это плохо?

  • Основная причина, почему нельзя переиспользовать бэкенд-модели заключается в том, что мы не сможем повторно использовать этот Viewв другой части приложения. Мы связываем интерфейс с ответом из конечной точки, и теперь для отображения интерфейса нужна эта конечная точка.
  • В бэкенд-модели, скорее всего, содержится больше информации, чем требуется для View. Лишняя информация = лишняя сложность. Чем больше информации содержится в модели, тем проще ею неправильно воспользоваться. В примере выше можно было ограничиться упрощенной моделью:
data class UserUIModel(val name: String, val address: String)
  • Что случится, если изменится бэкенд? Мы склонны верить, что бэкенд-сервисы, поддерживающие приложение, никогда не меняются. Конечно же, это не так. Бэкенд поменяется, и надо быть к этому готовым. Но не нужно менять интерфейс только потому, что что-то изменилось в бэкенде.

Как это исправить?

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

В нашем примере мы преобразуем UserModel кUserUIModelи передадим ее в виде параметра в UserView:

userApiClient.getUser(userId)
.map{ userModel -> convertToUIModel(userModel) }
.subscribe(
{ userUIModel -> userView.render(userUIModel) },
{ error -> Log.e("TAG", "Failed to find user", error) }
)

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

2. Монолитные View

Доводилось ли вам когда-нибудь открывать файл layout и находить там огромный документ со всеми Activity из интерфейса? К сожалению, это довольно распространенная практика в Android, и она доставляет массу неудобств.

Почему это плохо?

Монолитные View противоречат сразу трем принципам:

  • Такие файлы трудно читаемы — в них слишком много информации и слоев компонентов.
  • Вы не можете протестировать каждую часть XML по отдельности. Мы привыкли тестировать интерфейс в составе интеграционного тестирования, которое делается в espresso. Интеграционные тесты — это, конечно, хорошо, но они сочетают в себе UI и бизнес-логику, из-за чего крайне трудно обнаружить первопричину ошибки. При разделении монолитных XML на небольшие Custom View и удалении бизнес-логики из интерфейса, можно протестировать только интерфейс. По сравнению с обычным интеграционным тестированием, такие тесты позволяют найти проблемы в UI на более точном и глубинном уровне.
  • Мы не можем повторно использовать отдельные части XML, поэтому вынуждены копировать эти компоненты для переиспользования на отдельном экране. Со временем дублирующие компоненты начнут противоречить друг другу, нарушая согласованность приложения.

Как это исправить?

Создание интерфейсной логики в одном XML файле равносильно включению всей бизнес-логики в класс activity. Мы должны разделять интерфейс на небольшие части — точно также, как при создании DAO, моделей и бэкенд-клиентов для бизнес-логики.

Наши лучшие друзья — это теги Custom views, <include> и <merge>. Мы должны всегда пользоваться ими для разграничения UI в начале каждой новой функции. Надежда на переиспользование UI-компонента с вытаскиванием его из монолитного XML может привести к серьезным проблемами. К тому времени интерфейс слишком тесно свяжется с нашим (Activity)/фрагментом (Fragment), чем осложнит рефакторинг и поставит под угрозу функциональность приложения.

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

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout ... >
  <android.support.design.widget.AppBarLayout ... >

    <!-- ToolBar -->
    <android.support.v7.widget.Toolbar ... >
      <RelativeLayout ...
        <TextView/>
        <TextView/>
      </RelativeLayout>
    </android.support.v7.widget.Toolbar>
  </android.support.design.widget.AppBarLayout>

  <ScrollView ... >
    <FrameLayout ... >
      <LinearLayout ... >

        <!-- Header -->
        <LinearLayout ... >
          <TextView ... />
          <TextView ... />
        </LinearLayout>

        <!-- User Message -->
        <LinearLayout ... >
          <TextView ... />
        </LinearLayout>

        <!-- User Information -->
        <LinearLayout ... >
          <TextView ... />
        </LinearLayout>

        <!-- Option selector -->
        <LinearLayout ... >
          <TextView ... />
          <Spinner ... />
        </LinearLayout>
      </LinearLayout>

      <!-- progress overlay -->
      <FrameLayout ... >
        <ProgressBar ... />
      </FrameLayout>
    </FrameLayout>
  </ScrollView>
</android.support.design.widget.CoordinatorLayout>

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

В XML можно обнаружить как минимум 6 хорошо определенных компонентов интерфейса. Давайте посмотрим, как будет выглядеть тот же шаблон при создании Custom View для компонентов:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout ... >
  <android.support.design.widget.AppBarLayout ... >
    <CompanyToolbar ... />
  </android.support.design.widget.AppBarLayout>

  <ScrollView ... >
    <FrameLayout ... >
      <LinearLayout ... >
        
        <InformationHeader ... />

        <UserMessageView ... />

        <UserInformationView ... />
        
        <MultipleChoiceView ... />
      </LinearLayout>

      <ProgressOverlay ... >
    </FrameLayout>
  </ScrollView>
</android.support.design.widget.CoordinatorLayout>

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

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

Наша любовь к разделению интерфейса на меньшие компоненты и стремление отойти от монолитной XML зародились после прочтения книги Брэда Фроста Атомарный дизайн. Настоятельно рекомендуем, особенно, если вы любите копаться в UI.

3. Бизнес-логика в Custom View

В предыдущем пункте мы обсуждали плюсы использования Custom View. Custom View — это отличный инструмент. Но это также и палка о двух концах, если пользоваться им бездумно. Связать бизнес-логику с Custom View можно на удивление просто. Добавление логов, А/В тесты, логика принятия решений — все это кажется настолько классным для структурирования кода. Но это не так.

Почему это плохо?

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

Как это исправить?

Существует множество способов по извлечению бизнес-логики из View. Схема действий зависит от желаемой архитектуры приложения. При использовании MVP вся логика находится в Представителе (Presenter). Если вы предпочитаете MVVM, то логика становится частью Модели представления (ViewModel).

Идеальным решением для нашей компании оказался MVI и однонаправленный поток данных (unidirectional data flows). Бизнес-логика ушла в Интент (Intent). Он создает неизменяемое состояние объекта, которое и отображает View.

4. Чрезмерная оптимизация

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

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

Существуют две большие проблемы, влияющие на производительность интерфейса в Android: вложенный layout и двойное использование layout. Из-за этого нас постоянно заваливают статьями, подкастами и разговорами о том, что нужно пользоваться ConstrainLayout и избегать вложенных layout. ConstrainLayout — это очень классный инструмент, более мощный, чем RelativeLayout. И он не влечет за собой двойного использования layout. Но как это всегда и бывает, проблемы возникают, когда мы ударяемся в крайности.

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

Почему это плохо?

  • При создании всего UI в одном ConstrainLayout мы возвращаемся к монолитному интерфейсу. Как уже говорилось ранее, он сложно читается, плохо тестируется и создает непригодный для повторного использования код.
  • Мы не относимся к интерфейсу как к объекту первого класса. Мы никогда не рассматриваем вынесение всей бизнес-логики в классы Activityили Fragment. По этим же причинам мы не создаем весь интерфейс в одном XML файле.

Как это исправить?

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

5. Исключение интерфейса из анализа кода

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

Почему это плохо?

  • Интерфейс — это «лицо» нашего приложения в глазах пользователей. Единообразный, чистый и хорошо структурированный интерфейс влияет на решение пользователя продолжить работать в приложении или удалить его.
  • Мы не видим в интерфейсе объекта первого класса. Наш интерфейс — это половина приложения. Поэтому анализ XML и View — это также важно, как и проверка бизнес-логики.

Как это исправить?

Существует ряд способов для улучшения анализа кода.

Если вы анализируете код:

  • Совершенно нормально, если вы не можете сразу понять весь XML. Возможно, у вас отсутствует нужный контекст, или интерфейс слишком сложный для восприятия. Обратитесь за советом к автору кода.
  • Не бойтесь попросить автора кода разбить XML на меньшие View. Вы же делаете это для больших классов или длинных методов.
  • Начинайте анализ кода с интерфейса. Как уже говорилось, XML файлы — это самое сложное в анализе. Не откладывайте их на потом, проверяйте сразу, пока не накопилась усталость.
  • Ознакомьтесь с принципами материального дизайна. Вот ряд простых вопросов, которые я всегда задаю себе при проверке: появляется ли волновой эффект при «нажатии»? Есть ли (нужна ли) высота кнопок? Задана ли в анимации правильная продолжительность?

Если анализируют ваш код:

  • Добавьте скриншоты интерфейса в Pull Request. Так ваша команда сможет быстрее проанализировать код.
  • Попросите дизайнера оценить реализацию интерфейса. Дизайнер — это ваш главный союзник. Попросите его проанализировать интерфейс на самых ранних стадиях разработки.
  • Избегайте монолитных XML файлов. Никогда не устану повторять: небольшие компоненты интерфейса лучше; их легче анализировать.
  • Начинайте с интерфейса. Создание каждой новой опции начинайте с добавления Pull Request об интерфейсе. Так вы будете уверены, что ваш интерфейс заслужил 100%-ное внимание аналитика кода.

Выводы

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

Повторное использование интерфейса позволяет быстрее создавать новые функциональные возможности. Если говорить о скорости и качестве, то существует большая разница между объединением уже существующих UI-компонентов и созданием интерфейса с нуля. Кроме того, в переиспользуемых компонентах уже заложены исправления ошибок и рассмотрены все «крайности», о которых мы могли и не подумать.

Подведем итог написанному выше:

  • Использовать бизнес-логику для View — это плохо. Всегда отделяйте View от бэкенда и бизнес-логики.
  • Избегайте монолитных XML. Из-за них ваш код сложно прочесть и протестировать. К тому же, монолиты являются первопричиной несогласованности внутри приложения. Атомарный дизайн научит вас разбивать интерфейс на повторно используемые компоненты.
  • Бизнес-логике не место в интерфейсе. Логированию, А/В тестированию и логике принятия решений не место во View. View должен только получать неизменяемое состояние и отображать его. Вам помогут переход на MVI, неизменяемость и однонаправленные потоки данных.
  • Оптимизация за счет качества кода должна быть исключением, а не правилом. Монолитные View следует создавать только в крайнем случае и только для избавления от последствий вложенных layout. Создавайте интерфейс правильно, а проблемы с производительностью решайте по мере поступления.
  • Относитесь к интерфейсу как к объекту первого класса. Любую опцию начинайте с создания ее интерфейса. Попросите дизайнера оценить интерфейс как можно раньше, а команду — провести отдельный анализ кода.

Перевод статьи Carlos Palacin Rubio: How to maximize Android’s UI reusability — 5 common mistakes