[Разработка мобильных приложений, Dart, Flutter] Моя история реализации офлайн приложения Хабра

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
27-Апр-2021 13:32


Создание своего приложения Хабра уже вошло в традицию среди хабрюзеров. Я решил не отставать и сделать своё. В данной статье я расскажу в первую очередь о том, как создавался клиент для Хабра, архитектурные и технические решения, их предпосылки и анализ, какие трудности были и в последнюю очередь о функционале приложения. Предыстория Все началось с предложения друга попробовать 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 прикладываю.А дальше пара скриншотов приложения:
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_razrabotka_mobilnyh_prilozhenij (Разработка мобильных приложений), #_dart, #_flutter, #_mobilnoe_prilozhenie (мобильное приложение), #_flutter, #_habr_app, #_habr.com, #_razrabotka_mobilnyh_prilozhenij (
Разработка мобильных приложений
)
, #_dart, #_flutter
Профиль  ЛС 
Показать сообщения:     

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

Текущее время: 22-Ноя 15:35
Часовой пояс: UTC + 5