Одной из самых разочаровывающих и полезных функций, над которой я работал во время совместной игры, была переработка страницы настроек для пользователей, которая включала перетаскивание и оптимистичные обновления. Возникшие проблемы показали мне тонкие сложности такой функции. Они также предупредили меня, чтобы я писал более простой, читаемый (и, следовательно, отлаживаемый) код, чтобы недостатки дизайна реже путались с техническими ошибками.
Общий вариант использования: управление группами заказанных товаров, когда отдельные товары можно перемещать между группами.
История пользователя
Как пользователь Android Hootsuite, я хочу иметь возможность управлять контентом, который мне отображается. Для этого я должен иметь возможность группировать связанные социальные сети/потоки во вкладки и менять порядок вкладок, а также потоков. внутри каждой вкладки, чтобы потоки контента с наивысшим приоритетом были легко доступны, а связанный контент был сгруппирован по моему вкусу.
потоки (контента) :: Каждая социальная сеть (SN) предоставляет разные источники контента. Например, если вы добавляете учетную запись Twitter, у вас может быть поток Мои сообщения (со всеми вашими сообщениями) или вы можете определить свой собственный, определив поиск для хэштега/запроса.
вкладка :: Группа потоков контента. Загрузка вкладки позволит вам пролистать все содержащиеся в ней потоки.
Обзор проблемы
Вот подробное описание функций:
- Список разнородных элементов — вкладки (заголовки категорий), потоки (элемент), пустой заполнитель (предлагает пользователю добавить поток, если вкладка пуста), нижние колонтитулы (зависит от пользователя).
- Вкладки, которые не содержат потоков, вместо этого имеют заполнитель, который выполняет ту же функцию, что и «добавить поток» из меню переполнения вкладок.
- Нижние колонтитулы с собственными действиями, которые добавляются в самый низ списка
- Всплывающее меню на каждом элементе вкладки для: добавления потока, переименования, изменения порядка, удаления
- Перетащите ручку/длительное нажатие на Stream, что позволяет перетаскивать
- Проведите влево по потоку, чтобы удалить его с вкладки.
До перепроектирования этой функциональности в нашей существующей реализации были некоторые, но не все эти функции.
Помимо проблем с возможностью обнаружения, основным источником нареканий является блокировка ProgressDialog (устарела) для каждого действия. Таким образом, было бы неплохо (но крайне неприятно для меня) добавить следующие функции:
- Неблокирующие действия по перераспределению (оптимистичное обновление пользовательского интерфейса)
- Уведомление снэк-бара с действием «Отменить»
Наша реализация отдает приоритет:
- Сопровождаемость и расширяемость — это самый важный технический урок, который я усвоил во время совместной работы. Ремонтопригодность означает написание кода с осознанием того, что в какой-то момент кому-то еще придется копаться в нем, чтобы изменить/добавить функциональность. Как говорится: есть читаемый код, а есть код, который никто не использует.
- Гладкость — мы в значительной степени полагаемся на сенсорное взаимодействие для изменения состояния. Мы хотим избежать таких случаев, как принуждение пользователя бросить удерживаемый элемент, когда мы обновляем/отображаем закусочную, или ненужное обновление дисплея и блокирование взаимодействия с пользователем.
Компоненты пользовательского интерфейса 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.