
Создание своего приложения Хабра уже вошло в традицию среди хабрюзеров. Я решил не отставать и сделать своё.
В данной статье я расскажу в первую очередь о том, как создавался клиент для Хабра, архитектурные и технические решения, их предпосылки и анализ, какие трудности были и в последнюю очередь о функционале приложения.
Предыстория
Все началось с предложения друга попробовать Flutter, а я оказался не против.
Создавать приложение, но какое? На момент создания репозитория мне показалось, что мобильному клиенту Хабра крайне не хватает офлайн режима, собственно это и стало причиной и на этом строится идея всего приложения — смотреть статьи в офлайне и по возможности продолжить чтение с того момента где остановился и слегка настроить отображение текста под себя (задача минимум которую я постарался выполнить).
До этого у меня был опыт пет проектов на React+TypeScript и VanilaJs, поэтому было интересно сравнить React и Flutter.
В момент старта я предполагал, что html это легко и просто, а его отображение — это плевое дело, есть одно “но”: я захотел отображать html нативными виджетами, а не какими-то web-view. Вжух и проект усложнился, но не будем пока об этом.
Выделяем слои
Изначально я не предполагал чего-то сверх сложного. Да, об изолятах в Dart я слышал, но не думал, что буду их использовать (на что Flutter сказал: ты будешь использовать изоляты, т.к. тяжёлые вычисления, парсинг и сетевые запросы мешают ui потоку).
На момент старта я выделил три слоя:
-
ui
-
habr-storage — к нему мы обращаемся за информацией, а он сам решает, идти ли в бд или обратится к апи хабра.
-
habr-api — обычные запросы по http с обработкой полученных данных.
В принципе, это оказался не такой уж и плохой вариант. Архитектурно похоже на MVVM.

UI
По API Хабр высылает html и иногда js для отображения красивых мегапостов. По этой причине перед мной встала задача отображение html и в идеале красиво.
Для отображения html на Flutter, на сколько мне известно, есть два варианта:
-
web-view
-
нативные (для Flutter) виджеты
Я решил пренебречь первым вариантом, так как если бы хотел его использовать, писал приложение на React Native, а хотелось ощутить всю мощь нового фреймворка.

Рендер HTML
Это, на мой взгляд, самая большая проблема. Чтобы отображать некоторые элементы вроде iframe, нужно поизвращаться, либо использовать web-view. На данный момент я не очень доволен, но я просто выдаю пользователю ссылку, и он спокойно может посмотреть, что внутри. Это компромисс, но без этого никуда.
Рендер я выполняю в три этапа:
-
Парсинг
-
Препроцессинг
-
Рендер
Первый и второй этап я стараюсь делать единожды, т.к. они достаточно трудоёмкие.
Парсинг html достаточно тривиальная задача, особенно если парсишь с помощью библиотеки, что я и сделал.
А вот препроцессинг не самый очевидный этап. После того как библиотека прошлась и выдала распарсенный html, хочется просто взять корневой div, пройтись по всем нодам и отобразить. На мой взгляд, это достаточно проблемно, т.к. придется поддерживать множество элементов как самого html, так и комбинаций с css классами. К тому же некоторые авторы иногда любят добавлять изюминки к оформлению своих статей, из-за чего все это выглядит не очень, если отображать «как есть».
Пример необычного html который приходится детектировать:
<div class="spoiler" role="button" tabindex="0"> <b class="spoiler_title">Заголовок спойлера</b> <div class="spoiler_text">Контент спойлера</div> </div>
Данный кусок часто встречается в статьях, как и тег details — приходится поддерживать обе версии.
Препроцессинг проходит в 2 этапа:
-
Конвертация html блоков в дерево смысловых элементов
-
Оптимизация дерева
Идейно дерево смысловых элементов не особо отличается от html5. В нем есть основной набор высокоуровневых элементов, которые привычны для верстальщика:
-
Изображение (с подписью)
-
Абзац
-
Заголовок
-
Список (с разделением на упорядоченный/неупорядоченный)
-
Таблица
-
Код
-
Iframe
-
Цитата
-
Спойлер
Комментарии
Отображение комментариев — один из самых интересных этапов. Комментариев может быть много и если реализовать их отображение не оптимально, то все будет лагать или рендериться овердофига лет.
Flutter для таких задач имеет аналог RecyclerView из мира Android, и называется он ListView. Каждый элемент он рендерит только когда он виден и не рендерит (и соответственно не тратит мощности аппарата), когда не виден.

Изначально я реализовал просто дерево комментариев, то есть у каждого комментария есть две части: контент и область дочерних элементов. От этой реализации я отказался из-за её нежизнеспособности — скрол лагает, т.к. дочерних элементов почти всегда много.

Текущая реализация, которой я вполне доволен — также отображаем дерево, но лишь один раз в памяти, трансформируем в массив нод-комментариев с известным уровнем вложенности, после строим по нему «плоское» отображение по вычисленной глубине. Получается, что ui выглядит также, но теперь ListView не обязан отображать все дочерние элементы разом, да и отобразить N отступов не проблема, особенно, когда N известно.

Благодаря такому решению приложение способно отображать дерево комментариев популярных статей, 1000 комментариев — не проблема, главное — скорость интернета.
Статьи
Их отображением я пока не очень доволен. Дело в том, что при некотором количестве очень больших картинок видны лаги. При повседневном чтении обычно это не заметно, но бывают прожорливые статьи. В целом решение есть, и оно такое же, как с комментариями — использовать ListView и разбить статью на «плоские» ноды и отображать только те, которые видны. Работа над этим ведётся, но не спешно.

Habr-API
Я обращаюсь напрямую по адресу мобильной версии m.habr.com/kek/v2/
Из того что поддерживает мобильная версия хабра и не поддерживает приложение с точки зрения API:
-
Авторизация и доступ к информации пользователя
-
Хабы
В целом, тут нет ничего особо интересного, кроме моего небольшого возмущения, например, страницы основной ленты, если их не подгружать все разом, могут устареть, пока ты читаешь какой-то пост, и при подгрузке следующей страницы возникают дубликаты, либо посты теряются. Исправлять это приходится на клиенте, что так себе. Лично мое впечатление – API не самый удобный и построен так чтобы делать больше запросов чем нужно.
Да, и в целом Хабр любит обновлять API из-за чего приходится обновлять приложение. Я думал о прокси сервере, чтобы он обрабатывал API Хабра и посылал данные в том формате, которое ожидает приложение. От такого решения я отказался из-за его непрозрачности по отношению к пользователям приложения.
Habr-Storage
На старте я начал использовать Moor (Room, но для Dart) совместно с SQLite, но на каком-то из этапов разработки вдруг понял, что в проекте уже две БД. Вторая – Hive, NoSQL Key-Value база данных, и она чертовски удобна. Когда подключал её как зависимость, чтобы хранить пару настроек приложения (темы и опции отображения текста), я и не представлял, как удобно ей будет пользоваться.
В SQLite я храню html текст статьи, авторов (аватарка, никнейм и прочее) и изображения, остальное лежит в Hive. Отдельно хочу рассказать про изображения.
Изображения я храню лишь частично в SQLite. Сами изображения я храню на файловой системе. В базе хранится url, по которому запрашивается изображение, и path файловой системы. Path я формирую как хеш-код url`а и конкатенирую с текущим временем вставки в миллисекундах. Примерно вот так:
path = str(hash(url)) + str(datetime.now().millisecondsSinceEpoch)
Isolates
По своей природе Dart однопоточный язык, но однопотоком сыт не будешь, поэтому есть возможность работы нескольких потоков, работают они схоже с web-workers и называются Isolate. Обычно их стоит использовать в любой необычной ситуации, вроде HTTP запросов, парсинга JSON, сложные вычисления, так вы уменьшите количество подвисаний вашего приложения на Flutter.
Для организации вычислений вне основного потока вы можете использовать отдельный изолят с какой-то продуманной логикой общения с другими потоками, либо организовать пул потоков-изолятов.
Примечание: Flutter предоставляет возможность быстро запустить изолят выполнить нужную функцию и закрыть изолят. Метод называется compute.
Я пришел к компромиссу – выделил основные операции, которые занимают достаточно много времени, и создал для каждой операции отдельные воркеры. Преимущества данного подхода в двух вещах:
-
Простота. Она как у метода compute, но при этом производительность на уровне пула потоков, иногда чуть ниже, иногда чуть выше, тут зависит от реализации пула потоков.
-
Разные операции не мешают друг другу. Гипотетически есть вероятность, что у двух задач может быть одинаковый приоритет, например, middle, но при этом одна из них достаточно долгая, а вторая короткая, и в случае если нам важно, чтобы короткая не дожидалась долгой задачи, пул потоков придется подстраивать ради баланса. Отдельные воркеры в этом плане на мой взгляд удобнее.
Следующие операции я выделил в отдельные воркеры:
-
Хеширование
-
Загрузка изображений
-
Подсветка синтаксиса
Для операции парсинг и препроцессинг пока использую метод compute.
Заключение
На этом в принципе все, что я хотел бы рассказать о внутреннем устройстве приложения.
Данный пост я не задумывал как рекламу, но желание потыкать приложение и посмотреть код может возникнуть, поэтому ссылку на github прикладываю.
А дальше пара скриншотов приложения:




ссылка на оригинал статьи https://habr.com/ru/post/554600/
Добавить комментарий