Android-приложение для офлайн-чтения EPUB в рамках тестового задания.
Цель проекта: реализовать устойчивый офлайн-сценарий чтения EPUB с корректным отображением текста/изображений, навигацией по документу и сохранением прогресса чтения.
Реализовано в текущем состоянии:
- открытие EPUB из локального источника (через выбор файла пользователем);
- распаковка EPUB и локальная обработка без сети;
- отображение контента главы (текст и изображения);
- навигация по главам и оглавлению;
- сохранение и восстановление позиции чтения;
- базовая модульная архитектура с разделением слоев.
Проект построен как модульная Clean Architecture:
:app— точка входа, DI-граф, корневая навигация;:domain— бизнес-модели, интерфейсы репозиториев, use-case;:data— реализация репозиториев, парсинг/распаковка EPUB, локальное хранение;:core:resources,:core:ui— общие ресурсы и UI-блоки;:feature:*— presentation-слой отдельных экранов (splash,library,reader,toc,settings).
domainне зависит от Android/инфраструктуры и описывает контракты.dataзависит отdomain, маппит инфраструктурные ошибки в прикладное поведение, хранит данные.featureиспользует use-case изdomainчерез ViewModel и не содержит логики доступа к данным напрямую.appсвязывает модули и инициализирует приложение.
- В экранах используется
ViewModel+StateFlow. - UI подписывается на state и отправляет user-action во ViewModel.
- Для длительных/тяжелых операций используются корутины.
- Hilt (
@HiltAndroidApp,@HiltViewModel, модули вdata). - Основные привязки репозиториев и инфраструктуры находятся в
data/.../di/DataModule.kt.
- Навигация реализована через
NavKey/Navigatorвdomainи корневой host вapp. - Экранные модули изолированы друг от друга и коммуницируют через слой
domain.
- Модульная декомпозиция вместо монолита UI: упрощает сопровождение и изоляцию изменений, но увеличивает количество Gradle-конфигурации.
- Собственный парсер EPUB вместо тяжелого SDK: дает контроль над поведением и зависимостями, но переносит стоимость edge-case поддержки на команду.
- Room + DataStore: четкое разделение between durable progress и user preferences; цена — два механизма хранения и необходимость согласованности миграций.
- StateFlow в ViewModel: предсказуемый state lifecycle и удобство тестирования; цена — необходимость дисциплины в разделении state и one-shot событий.
- Пользователь выбирает локальный EPUB (URI).
- Файл распаковывается в
filesDir/books/<bookId>черезEpubUnpacker. - Метаданные и структура книги извлекаются парсером.
- Контент (текст + изображения) читается из распакованной структуры.
- Книга добавляется в локальную БД.
- Метаданные книги и прогресс чтения: Room (
epub-reader.db). - Настройки читалки (шрифт/тема и т.п.): DataStore Preferences.
- Данные книги хранятся локально в
filesDir, сеть для чтения не требуется.
- EPUB распаковывается как ZIP.
- Контент глав извлекается из OPF/spine и XHTML.
- Изображения извлекаются из внутренних ресурсов EPUB.
- Для отрисовки изображений в UI используется Coil.
Текущая стратегия:
- операции импорта/парсинга обернуты в
runCatching; - при неуспешной распаковке/парсинге временные файлы удаляются;
- при частично проблемном EPUB используется fallback на читаемые главы/контент;
- при отсутствии корректного контента возвращается безопасный fallback-текст вместо падения.
Нестандартные сценарии, которые учитываются:
- битый/неполный EPUB;
- отсутствие отдельных глав или изображений;
- некорректные пути внутри архива (защита от zip-slip при распаковке);
- пустой или частично читаемый документ.
Текущее состояние:
- внешние production-инструменты мониторинга (Crashlytics/Sentry) пока не подключены.
Рекомендуемый минимальный production-профиль:
- Crashlytics для сбора crash/ANR;
- структурированные логи критических бизнес-событий (импорт, чтение, ошибка парсинга);
- приоритизация инцидентов по уровню: blocker/critical/major/minor.
- Детект ошибки (краш, массовая ошибка импорта, деградация рендера).
- Классификация по влиянию и воспроизводимости.
- Горячее исправление в отдельной ветке + регресс-тест критического сценария.
- Выкатка на ограниченный процент пользователей (staged rollout).
- Постмортем: корневая причина и профилактика.
- Рекомендуемый контур: Internal -> Closed testing -> Production.
- Перед релизом: smoke-проход сценария чтения, проверка импорта нескольких EPUB, проверка восстановления позиции.
- Для снижения рисков: staged rollout, фиксированный changelog, откат на предыдущий стабильный релиз при критике.
- Crash-free sessions: не ниже 99.5% на стабильном канале.
- Доля неуспешных импортов EPUB: не выше 1-2% на выборке реальных файлов.
- Время первичного открытия книги: целевой p95 в рамках приемлемого UX для офлайн-ридера (измеряется на типовом наборе EPUB).
Потенциальные узкие места:
- большие главы с большим количеством XHTML-контента;
- EPUB с большим числом изображений;
- перерасчет пагинации на изменении параметров чтения.
Что уже сделано:
- кэширование распарсенных глав в
ReaderRepositoryImpl; - тяжелые операции вынесены из UI-потока;
- аккуратная обработка контента с fallback-ветками, чтобы избегать зависаний/crash;
- пересчет прогресса и пагинации выполняется в фоне.
Что можно усилить в следующих итерациях:
- профилирование конкретных книг и оптимизация частых hot path;
- кеширование промежуточных результатов парсинга между сессиями;
- ограничение и батчинг дорогостоящих пересчетов.
- Добавить стратегию progressive loading: на старте загружать минимально достаточную часть EPUB (первая доступная глава/фрагмент), открывать экран чтения сразу, а полный парсинг и догрузку остального контента выполнять в фоне после открытия; это уменьшает perceived latency и убирает ожидание полного времени разбора файла.
- В текущем состоянии используется пользовательский импорт EPUB по URI, а не предзагрузка файла в
app/src/main/assets. - Не подключены Crashlytics/Sentry, поэтому наблюдаемость production пока базовая.
- Нет полного набора автоматических UI/интеграционных тестов для всех edge-case EPUB.
- Качество рендера зависит от структуры конкретного EPUB (нестандартная разметка может требовать расширения парсера).
- Навигация сейчас централизована в
:app; при росте числа feature и сценариев это может увеличить связность.
- Kotlin
2.0.21, AGP8.13.2, Gradle8.13: современный стабильный toolchain. - Compose BOM
2024.09.00, Material 3: основной UI-стек. - Hilt
2.57: DI и масштабируемая композиция зависимостей. - Room
2.7.2: надежное локальное хранение книг и прогресса. - DataStore Preferences
1.1.1: хранение пользовательских настроек. - Navigation 3
1.1.0: навигационная инфраструктура. - Coil
2.7.0: отрисовка изображений в reader UI.
Ограничения по зависимостям:
- Выбран собственный парсинг EPUB вместо внешнего epublib/readium, что дает контроль, но требует поддержки edge cases.
- OS: Windows/macOS/Linux.
- JDK: 11.
- Android SDK:
compileSdk 36,targetSdk 36,minSdk 26. - Android Studio / стандартная Gradle-среда без закрытых инструментов.
./gradlew assembleDebugДля Windows PowerShell:
.\gradlew.bat assembleDebug./gradlew assembleReleaseДля Windows PowerShell:
.\gradlew.bat assembleReleaseОжидаемый путь APK:
app/build/outputs/apk/release/app-release.apk
- Открыть проект в Android Studio.
- Дождаться sync Gradle.
- Запустить
app-модуль на эмуляторе или устройстве (Android 8.0+). - Добавить локальный EPUB через экран библиотеки.
- Пройти базовый сценарий: открыть книгу -> читать -> сменить главу -> перезапустить приложение -> проверить восстановление позиции.
- Unit-тесты (
domain,data): use-case, мапперы, репозитории, парсер EPUB на наборе тестовых файлов (валидный, частично поврежденный, с большим числом изображений). - Инструментальные тесты (
androidTest): проверка Room/DataStore интеграции и восстановления прогресса чтения после перезапуска. - Compose UI-тесты (
feature:*): ключевой пользовательский путь (открытие книги, перелистывание, оглавление, настройки шрифта/темы, возврат в чтение). - Smoke-регрессия перед релизом: быстрый ручной прогон на 2-3 реальных EPUB разных размеров.
Рекомендуемый набор стадий:
lint->./gradlew lintunit_tests->./gradlew testDebugUnitTestandroid_tests(по возможности с эмулятором/раннером) ->./gradlew connectedDebugAndroidTestassemble->./gradlew assembleDebug assembleRelease
Практический минимум, если нет доступного эмулятора в CI: запускать lint + unit_tests + assemble, а connectedDebugAndroidTest выполнять по расписанию или на выделенном раннере.
- Единый и воспроизводимый контроль качества на каждом merge request.
- Прозрачные quality gates до попадания в основную ветку.
- Удобная интеграция с веточной моделью, protected branches и approvals.
- Кэш Gradle и артефакты APK ускоряют обратную связь и упрощают проверку сборок.
- Настройка Android UI-тестов в CI сложнее из-за эмулятора и требований к раннеру.
- Более длинный pipeline может замедлить цикл разработки без грамотного кэширования.
- Поддержка инфраструктуры раннеров и образов требует отдельного внимания команды.
- Ложные падения инструментальных тестов (flaky) требуют стабилизации и ретраев.
- Добавить Crashlytics и базовую дашбордизацию ошибок импорта/парсинга.
- Зафиксировать регрессионный набор EPUB-файлов (малый/средний/большой, с изображениями и сложным XHTML).
- Ввести автоматические проверки базового сценария чтения на CI.
- Добавить персистентный кеш промежуточного разбора EPUB и метрики времени открытия.
- Оптимизировать memory footprint для книг с большим количеством inline-изображений.
- Расширить покрытие parser edge cases (нестандартные nav/toc, сложные вложенные блоки XHTML).
- Вынести навигационные контракты и инфраструктуру в отдельный модуль (например,
:core:navigation) для уменьшения связности:appи упрощения масштабирования.
- Перейти к более строгим release-gates: обязательные quality checks перед promotion в production.
- Ввести формализованный incident playbook и SLA по срокам реакции/фикса.
- Масштабировать архитектуру добавлением новых корневых модулей по бизнес-доменам (например,
:auth,:notifications,:analytics), без сквозных изменений существующих feature-модулей.