Как я ускорил бэкапы в 20 раз и обошёл ловушку Jsoup: развитие самописной Android-читалки MRead (v1.3.0)
Всем привет! Не так давно я рассказывал, как боль от перегруженных интерфейсов заставила меня открыть Android Studio и написать собственную читалку с кастомным движком рендеринга и точным выделением текста.
Статья получила теплый отклик и в комментариях набежало много отличных предложений. В этом посте я хочу поделиться техническими решениями, которые вошли в крупное обновление 1.3.0.
1. Бэкапы и боль от Storage Access Framework (SAF)
В приложении есть функция бэкапа: упаковка базы данных Room, настроек и распакованных HTML-глав с картинками в один ZIP-архив. Изначально я писал файлы напрямую в OutputStream, полученный через ContentResolver (SAF). Итог: библиотека на 500 МБ архивировалась около 5 минут. SAF проводит проверки безопасности для каждого записываемого чанка, что убивает I/O операции.
Решение: сборка архива переехала во внутренний кэш приложения. Туда пишем без ограничений SAF — буфером по 64 КБ и уровнем сжатия BEST_SPEED (картинки уже сжаты, гнать их через BEST_COMPRESSION бессмысленно). Когда ZIP готов целиком, он одним куском копируется в пользовательскую папку через SAF — вместо тысяч мелких защищённых записей получается одна
2. Material You: как получить правильные цвета обоев
При внедрении динамических тем (Android 12+) я столкнулся с тем, что стандартный вызов dynamicLightColorScheme().background на многих устройствах выдает просто унылый белый или бледно-серый цвет, игнорируя сочные оттенки обоев.
Решение: Самые насыщенные цвета из системной палитры Monet хранятся в secondaryContainer и surface. Решение нашлось в самой палитре Monet: наиболее насыщенные цвета живут в secondaryContainer и surface, а не в background. Переориентировал маппинг цветов приложения на эти слоты и интерфейс действительно ожил. Теперь интерфейс действительно реагирует на смену обоев. Плюс привязал OnSharedPreferenceChangeListener, чтобы тема менялась мгновенно на всех экранах без перезапуска.
3. Странности парсинга FB2 и баги Jsoup
Иногда вместо обложки FB2 парсер ставил черно-белую картинку из середины книги. FB2 хранит все изображения в тегах <binary> в конце файла в хаотичном порядке. Если тег <coverpage> отсутствует, старый алгоритм просто брал первую попавшуюся картинку из бинарной кучи.
Я переписал фоллбэк: теперь, если явной обложки нет, Jsoup ищет первый тег <image> прямо внутри <body> книги.
Попутно всплыло неочевидное поведение Jsoup: если атрибут отсутствует, attr() возвращает пустую строку, а не null — это задокументировано, но интуитивно ожидаешь null. Из-за этого Элвис-операторы (?:) молча проглатывали пустую строку вместо ухода в fallback. Написал строгую обертку takeIf { it.isNotEmpty() }, и теперь обложки извлекаются безошибочно.
4. Изолированный свайп яркости в Compose
Нужно было добавить регулировку яркости свайпом по левому краю экрана. Проблема: в режиме вертикального скролла (VerticalPager) свайпер страниц перехватывает вертикальные жесты на себя.
Решение: перехватывать жест на фазе Initial — до того, как пейджер успевает его обработать. Если касание началось в левых 15% ширины экрана, событие забирается себе и до пейджера не доходит.
Помимо этого в релизе 1.3.0:
• Добавлен полноэкранный просмотрщик иллюстраций с pinch-to-zoom (на основе detectTransformGestures).
• Написан собственный File Picker со сканированием вложенных папок и извлечением книг прямо из ZIP-архивов на лету.
• Добавлен поворот страниц для PDF с сохранением состояния в SharedPreferences.
• Разделен UI верхнего меню: закладки теперь можно переименовывать, а тап по номеру страницы открывает быстрый переход.
• Добавлен множественный выбор в библиотеке (массовое добавление на полки/удаление/скрытие).