Поддержка тредов в Android-приложении Почты Mail.Ru: добиваемся полного синхрона
Треды, или цепочки писем в почте, — одна из фич, на которые у гиков и массовой аудитории полярные взгляды. Гики активно ими пользуются; обычные пользователи, как показывают наши опросы, относятся к ним скорее настороженно. Во-первых, непривычно, во-вторых, люди опасаются, что не смогут сориентироваться в цепочках. Когда мы реализовывали треды в веб-версии Почты Mail.Ru, мы помнили об этом челлендже — и нашли, как нам кажется, максимально удобный и интуитивно понятный алгоритм группировки, который будет удобен и гикам, и менее продвинутым юзерам. За основу в работе над мобильными тредами мы взяли систему, разработанную большой Почтой, так как мы не хотели запутать пользователей и делать разную логику. Наша задача с точки зрения продукта заключалась в том, чтобы и веб-треды, и мобильные треды работали для пользователя одинаково. Но многие вещи пришлось переделывать с учетом офлайн-работы. О том, как мы сделали в Android-приложении Почты Mail.Ru цепочки, где письма не теряются даже при сбоях в сети, я расскажу в этой статье (о том, как сделали то же самое в iOS-приложении, расскажем в одном из следующих постов).
В мобильном приложении мы делали особый упор на стабильную и бесперебойную работу в офлайне. Нашей целью было сделать так, чтобы пользователь мог в офлайне редактировать письма, перемещать их, ставить метки, работать с почтой и совершать действия с целыми тредами —, а когда сеть появляется, чтобы информация обо всех этих действиях корректно передавалась на сервер.
Самый распространенный вариант реализации тредов в почтовых приложениях — группировка писем на самом клиенте: письма выкачиваются и уже потом собираются в цепочки. Такой вариант покрывает большинство стандартных ситуаций за исключением сложных случаев, среди которых, например, корректная группировка писем в треды при работе в офлайн. Однако нам нужно было решение, которое будет гарантированно хорошо работать у всех пользователей. Мы не хотели жертвовать офлайн-работой, ведь как раз в сложных ситуациях проявляется качество продукта. Кроме того, когда мы проводили юзабилити-тестирование, люди отмечали, что пользоваться тредами удобно и круто —, но при этом многие боялись, что перестанут находить в ящике письма, если цепочки сгруппируются неинтуитивно. Поэтому мы уделяли особое внимание работе приложения в офлайне: нужно было сделать так, чтобы все изменения сохранялись отдельно, а потом при удобном случае незаметно синхронизировались. Для пользователя это должно выглядеть так: он вносит изменения, а дальше приложение само разбирается, как и когда все это отправить на сервер.
Итак, с подходом мы определились, можно было приступать к реализации. Логика группировки писем в цепочки разрабатывалась еще для большой веб-версии Почты. Так что нам не пришлось изобретать велосипед: в Android-приложении мы просто реализовали тот же самый алгоритм. На Хабре есть детальный пост о том, как он разрабатывался и чем отличается от алгоритмов, которые используют другие почтовые службы, поэтому подробно описывать его в сегодняшней статье я не буду.
Итак, важно было убедиться, что все операции с письмами в рамках нашей логики выполняются корректно. Например, при перемещении или удалении сообщений все счетчики в вебе и приложении должны отображать корректное (обновленное) количество писем в треде, все флажки и пометки «Прочитано», «Не прочитано» должны быть обновлены и синхронизированы, определенные треды должны перестать отображаться в некоторых папках и т. д. Помимо корректности синхронизации было важно сделать так, чтобы операции над письмами совершались быстро. Скорость мы тестировали на массиве из 200 писем в разных тредах и папках. В первой версии на это уходило секунд 20 — и все это время пользователю пришлось бы ждать. Естественно, мы такого допустить не могли.
Мы начали искать причину такой медлительности. Изначально мы предполагали, что больше всего времени занимали SQL-операции, потому что они зависели от количества писем: при выполнении операции с каждым из них приходилось обновлять треды, счетчики, папки, само письмо и так далее. Однако в действительности самым узким местом оказалось копирование чрезмерно сложных индексов, по которым мы потом выбирали данные из памяти. Оно занимало 70–80% времени из этих 20 секунд. Мы убрали копирование этих индексов, а также перестройку индексов на каждую операцию, сделали буферизацию и update после завершения операции в конце транзакции. В результате вместо 20 секунд у нас получилось всего порядка 20–50 мс на операцию перестроения индексов. Кроме того, мы убрали из перестроений индексов данные, которые не изменяются согласно нашей бизнес-логике.
У нас есть достаточно простые и быстрые индексы, например, по какому-то конкретному значению: flagged, unflagged, read, unread, по значению папки, по аккаунту. Если значение вариантов состояний конечно, индексы перестраиваются быстро. А текстовые индексы, основанные на префиксных деревьях, перестраиваются долго: они и по объему занимаемой памяти большие, и в целом эта структура достаточно громоздка. Она позволяет ускорить процесс поиска, но при этом очень сложная и долго меняется. Это стандартные проблемы разных структур и алгоритмов обработки данных. Там, где мы хотим выиграть на поиске, мы, естественно, теряем на изменении этих данных.
Сначала мы пробовали делать разные реализации текстовых индексов. Ощутимой разницы не заметили и выбрали те, которые были быстрее по нашим замерам. Но желаемых показателей по времени мы все еще не достигали. Тогда мы решили зайти с другой стороны. Такие данные, как тема письма, отправитель и т.д. во время операций с письмом не меняются. Значит можно просто отказаться от перестроения сложных индексов и ограничиться теми, что отвечают за параметры flagged, unflagged и т. д., то есть быстрыми. Мы отказались от всех лишних операций, оптимизировали то, от чего отказаться нельзя, и сразу на 50–60% ускорили синхронизацию.
Но и на этом мы не остановились — ведь операции можно группировать. Если не пытаться делать универсальный алгоритм, а разобрать на конечные варианты и сгруппировать входные данные по типам операций, то их можно выполнять уже без всяких условий и лишних операций с БД. Мы поштучно оптимизировали операции с флажками, с перемещением писем и т. д., и они перестали зависеть от количества передаваемых данных.
В итоге пользователь видит быстрый отклик независимо от того, какого рода операцию он совершал и над какими данными. В самом начале мы ставили для себя планку в 100 мс, но уперлись уже в ограничения самого Android: механизм транзакций довольно медленный, а если не использовать его, то мы получаем ошибку промежуточного состояния. Пришлось скорректировать требования. Мы планировали, чтобы с точки зрения пользователя операция занимала не больше 200–500 миллисекунд. Скорее всего, в следующей версии Android мы все же добьемся того, чтобы для пользователя операции выполнялись практически мгновенно.
Как я уже упоминал, логику отображения тредов Android-приложение Почты унаследовало от веб-версии. Однако в реализации интерфейса, естественно, есть и различия: экран смартфона и даже планшета гораздо меньше десктопного, и нужно было решить, как именно отображать на них треды.
Изначально мы выбирали между двумя вариантами. Первый — отображение в два уровня (список тредов и просмотр треда, совмещенный с чтением письма). Сначала он казался нам логичным и предпочтительным. Но потестировав такую реализацию в других приложениях, мы в нем разочаровались: отображение тредов на двух экранах во всех случаях получилось достаточно неудобным. Поэтому мы выбрали вариант с тремя уровнями (список тредов, просмотр писем внутри этого треда, открытие и чтение письма). В этом случае у пользователя есть возможность посмотреть заголовки и выбрать, какие письма он хочет открыть.
За счет того, что внутри треда у писем одинаковые темы, они сгруппированы, и на экране есть место для отображения более детальных сниппетов. В целом это получилось удобнее: когда нужно вспомнить, о чем шла речь в длинной переписке, можно по сниппетам писем освежить ее в памяти, либо быстро долистать до нужного письма и начать читать переписку с того места, где находится что-то важное.
Еще одно отличие между большим вебом и приложением касается положения новых писем в треде. В веб-версии это сделано в два экрана, потому что в браузере есть где разместить эту информацию. Веб всегда показывает тред и открывает либо последнее непрочитанное письмо, либо верхнее письмо из этой папки, а дальше можно на одном экране сворачивать и разворачивать любые письма. В Android-приложении пользователь в данный момент всегда «проваливается» или в тред, или в чтение письма (если цепочка содержит только одно письмо). Между письмами внутри треда можно перемещаться по свайпу влево или вправо.
Отдельно стоит рассказать про тулбар, относящийся ко всему треду. Здесь у Android-почты больше расхождений с веб-версией. В последней тулбар — это большая панель, позволяющая совершать операции с верхним письмом треда в папке. В Android-приложении Почты Mail.Ru работа ведется либо с целой цепочкой, либо с конкретными письмами, выделенными пользователем. Есть только одно исключение: когда идет работа с тредом, есть возможность ответить на последнее письмо в этой цепочке. Это один из достаточно частых и важных кейсов, поэтому мы вынесли эту кнопку из чтения письма и включили ее в инструменты работы с тредом. Кнопки «Ответить» и «Переслать», которые можно назвать главными action buttons для треда, применяются именно к последнему письму цепочки.
Мы изначально реализовали поддержку Android Wear и Android Auto, поэтому для взаимодействия Android-Почты с часами и магнитолой не пришлось делать ничего дополнительно. Если пользователь включил в настройках почты группировку писем, уведомления на часах тоже будут группироваться по тредам. Если группировку писем отключить, то все будет работать по-старому, и уведомления группироваться не будут. На магнитоле под управлением Android Auto уведомления также группируются по тредам.
На наш взгляд, с поставленными задачами мы справились. Но, как всегда, мы очень ждем от вас фидбека. Тестируйте треды на Android и рассказывайте — удобно ли вам?