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

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

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

потоки (контента) :: Каждая социальная сеть (SN) предоставляет разные источники контента. Например, если вы добавляете учетную запись Twitter, у вас может быть поток Мои сообщения (со всеми вашими сообщениями) или вы можете определить свой собственный, определив поиск для хэштега/запроса.

вкладка :: Группа потоков контента. Загрузка вкладки позволит вам пролистать все содержащиеся в ней потоки.

Обзор проблемы

Вот подробное описание функций:

  • Список разнородных элементов — вкладки (заголовки категорий), потоки (элемент), пустой заполнитель (предлагает пользователю добавить поток, если вкладка пуста), нижние колонтитулы (зависит от пользователя).
  • Вкладки, которые не содержат потоков, вместо этого имеют заполнитель, который выполняет ту же функцию, что и «добавить поток» из меню переполнения вкладок.
  • Нижние колонтитулы с собственными действиями, которые добавляются в самый низ списка
  • Всплывающее меню на каждом элементе вкладки для: добавления потока, переименования, изменения порядка, удаления
  • Перетащите ручку/длительное нажатие на Stream, что позволяет перетаскивать
  • Проведите влево по потоку, чтобы удалить его с вкладки.

До перепроектирования этой функциональности в нашей существующей реализации были некоторые, но не все эти функции.

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

  • Неблокирующие действия по перераспределению (оптимистичное обновление пользовательского интерфейса)
  • Уведомление снэк-бара с действием «Отменить»

Наша реализация отдает приоритет:

  1. Сопровождаемость и расширяемость — это самый важный технический урок, который я усвоил во время совместной работы. Ремонтопригодность означает написание кода с осознанием того, что в какой-то момент кому-то еще придется копаться в нем, чтобы изменить/добавить функциональность. Как говорится: есть читаемый код, а есть код, который никто не использует.
  2. Гладкость — мы в значительной степени полагаемся на сенсорное взаимодействие для изменения состояния. Мы хотим избежать таких случаев, как принуждение пользователя бросить удерживаемый элемент, когда мы обновляем/отображаем закусочную, или ненужное обновление дисплея и блокирование взаимодействия с пользователем.

Компоненты пользовательского интерфейса Android

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

Список разнородных элементов: RecyclerView с несколькими типами представлений и держателями представлений. Мы поддерживаем RecyclerView.Adapter<Any>; мы воздерживаемся от добавления дополнительного уровня абстракции, например, RecyclerView<ManageItem>, так как нет реальной дополнительной выгоды. Мы по-прежнему будем использовать getItemViewType в любом случае. Наша кодовая база уже абстрагирует onBindViewHolder и onCreateViewHolder в суперкласс с интерфейсом SAM, который обрабатывает обратные вызовы взаимодействия.

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

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

Шаблон архитектуры

Перво-наперво — какая архитектура представления облегчит нашу жизнь? В последнее время мы больше движемся к архитектуре Model-View-ViewModel (MVVM) для функций Android в Hootsuite. Вот несколько причин, почему я считаю, что это лучший шаблон для этого варианта использования:

  • Хорошо поддерживается Android Framework, сохранение состояния через изменение ориентации.
  • Отделение бизнес-логики от представления, что упрощает модульное тестирование сложных преобразований. Когда мы вносим/отменяем изменение, повторное заполнение данных или откат состояний сбоя можно протестировать отдельно. У нас есть полный контроль над моделью данных, и нам не нужно программно запускать перетаскивание или смахивание.
  • Абстрагирование общей логики между похожими вариантами использования. Переупорядочивание вкладок имеет ту же логику, что и переупорядочивание потоков (хотя и намного проще).
  • На View требуется уже высокая связь. В нашем пользовательском RecyclerView уже есть прослушиватели взаимодействия для поддержки Swipe-to-Refresh и Jump-to-Top. Каждый элемент списка может иметь дополнительные взаимодействия просмотра. Мы также добавим ItemTouchHelper. Можем ли мы централизовать эту ответственность?

С оптимистичными обновлениями было бы неплохо иметь единый источник достоверной информации о состоянии пользовательского интерфейса. MVC может подойти. Но учтите тот факт, что некоторые изменения в модели, например. обновления базы данных, не отражайтесь мгновенно на представлении, потому что мы обновляем их оптимистично и асинхронно. Нам нужен посредник для любых изменений. В сочетании с Rx ViewModel справляется со своей задачей! Они позволят нам оперативно координировать изменения из множества взаимодействий с представлениями.

Ремонтопригодность

Я был рад обнаружить, что наша существующая кодовая база имеет отличную абстракцию для обработки множества различных макетов элементов списка в одном списке (RecyclerView). Более интересным и сложным соображением было написание читаемого кода; Читабельность отличает легко поддерживаемый и отлаживаемый код от гигантских болот ухудшающего качество кода.

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

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

Отвратительный. Это действительно так. Но это полностью работает. Что в этом плохого?

  • Предполагается, что вы знакомы с курсором итератора. Когда вы вставляете элемент, куда он попадает? Что удаляется?
  • Заглянуть вперед с нашим курсором невозможно, мы должны продвигаться вперед, чтобы проверить условие, и отступить, чтобы выполнить определенные действия.
  • Вложенные ifs внутри оператора switch внутри цикла.

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

Давайте попробуем немного другой, более функциональный подход к складыванию; хотя и не на месте, обе реализации по-прежнему O (n).

Почему я считаю эту реализацию лучшей?

Для тех, кто не знаком с методом сгиба, вот краткий обзор. Fold во многом похож на цикл for-each, за исключением того, что он возвращает одно значение. Думайте об этом как об инициализации переменной «аккумулятора», и для каждого элемента в коллекции вы помещаете в нее новое значение. В самом конце возвращается значение внутри вашего аккумулятора. Вот несколько примеров:

С нашей реализацией fold мы можем использовать синтаксис Kotlin для улучшения читабельности. Как напомнил мне в разговоре Тревор Стоквис, часто полезно использовать имена методов в качестве документации. Здесь цель этих функций очень ясна. На самом деле, они даже уточняют контекст на своем call-site — то, что мы потеряли с индексацией итератора.

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

Гладкость

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

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

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

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

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

Состояния действия

Мы передаем изменение с помощью PublishSubject, который создает запечатанный класс.

sealed class DataEvent(val data: List<Any>) {
    class RefreshSuccess : DataEvent()
    ...
}
val subject: PublishSubject<DataEvent> = PublishSubject.create()

Затем мы определяем различные возможные состояния событий: RefreshSuccess, RefreshFailure, MovePending, MoveSuccess, MoveFailure, UndoPending, UndoSuccess, UndoFailure. Это позволяет легко рассматривать каждый поток действий как серию конечных состояний, где функциями перехода могут быть пользовательский ввод или вызовы/ответы API. При тестировании модели представления мы можем имитировать/вызывать эти переходы между состояниями и проверять результат, выдаваемый субъектом модели представления.

MoveSuccess — интересный частный случай, поскольку он должен быть «отменяемым». Нам нужно записать предыдущее состояние, а также желаемое состояние. В зависимости от конечной точки вашего сервера вы должны подумать, как отменить изменение состояния. Если вся логика на стороне сервера, то вам повезло! К сожалению, это было не так, и нужно было вычислить, отправить и обработать обратное действие перемещения.

Вывод

Эта настройка достаточно гибкая для расширения функциональности.

  • Сами ItemViews имеют модульную структуру, их можно изменить скинами и изменить их действия.
  • ItemTouchHelper можно изменить для поддержки действий смахивания.
  • Бизнес-логика живет исключительно в ViewModel, изменения будут локализованы, а тестирование модели представления охватывает большинство вариантов использования.

Однако есть некоторые соображения:

  • Существует много кода из-за различных действий, которые можно выполнять на экране. Столь сложный экран можно разделить и упростить.
  • Этот поток «оптимизма» можно полностью обойти, используя режим «редактирования» с кнопкой «Готово», которая фиксирует изменения, в зависимости от варианта использования.

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

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

Об авторе

Августин Квонг является специалистом по информатике и статистике в Университете Британской Колумбии. Он работал в команде Core Mobile, работая преимущественно над приложением для Android, но перешел на работу с серверной частью в Scala.