Android-приложение магазина товаров, разработанное на Kotlin с использованием Jetpack Compose.
Проект реализован с разделением на presentation, domain и data слои. Presentation-слой построен по архитектуре TEA с использованием библиотеки KoTEA. Для работы с сетью используется Retrofit, для локального хранения и кэширования данных — Room.
Основной акцент проекта сделан на реактивной работе с данными, offline-сценариях, сохранении состояния приложения и тестировании бизнес-логики и UI.
- просмотр каталога товаров;
- просмотр детальной информации о товаре;
- добавление товаров в корзину;
- отображение количества товаров в корзине;
- очистка корзины;
- загрузка изображений товаров;
- локальное кэширование каталога и детальной информации;
- работа с ранее загруженными данными при отсутствии сети;
- отображение состояния подключения к интернету;
- создание напоминаний о товарах в корзине;
- восстановление активных напоминаний после перезагрузки устройства.
- Kotlin
- Jetpack Compose
- Navigation Compose
- KoTEA
- Kotlin Coroutines и Flow
- Hilt
- Retrofit
- Room
- Kotlinx Serialization
- Glide
- AlarmManager
- BroadcastReceiver
- JUnit
- Kaspresso
- Compose UI Testing
Приложение разделено на три основных слоя:
UI реализован на Jetpack Compose.
Presentation-слой построен по паттерну TEA (The Elm Architecture) с использованием библиотеки KoTEA.
Состояние экрана изменяется через однонаправленный поток данных:
UI Event
↓
ELM Update
↓
Command Use Case
↓
Repository
↓
Room или Retrofit
↓
Flow
↓
новое состояние UI
Такой подход позволяет явно разделять:
- состояние экрана;
- пользовательские события;
- изменение состояния;
- побочные эффекты;
- работу с domain- и data-слоями.
Domain-слой содержит:
- модели предметной области;
- интерфейсы репозиториев;
- use case'ы.
Presentation-слой не работает непосредственно с Room или Retrofit — доступ к данным осуществляется через domain-контракты.
Data-слой отвечает за:
- получение данных из сети через Retrofit;
- работу с локальной базой Room;
- преобразование DTO и Entity в domain-модели;
- реализацию репозиториев;
- кэширование данных.
Для получения данных используется Retrofit и публичный DummyJSON API.
Реализованы запросы:
- списка товаров с пагинацией;
- детальной информации о конкретном товаре.
Полученные сетевые DTO преобразуются в domain-модели и сохраняются в Room.
Room используется как локальное хранилище данных и основной источник данных для UI.
Для каталога используется схема:
Retrofit
↓
Repository
↓
Room
↓
Flow
↓
UI
После загрузки данных из сети они сохраняются в Room. UI получает изменения через Flow, поэтому обновляется реактивно при изменении базы.
Для детальной информации о товаре реализована проверка актуальности кэша.
Если сохранённые данные ещё актуальны, приложение использует их без дополнительного сетевого запроса.
Если кэш устарел:
- выполняется запрос к API;
- новые данные сохраняются в Room;
- UI автоматически получает обновлённые данные.
Если сеть недоступна, но в базе остались ранее загруженные данные, приложение продолжает отображать их с признаком устаревшего кэша.
Таким образом приложение не становится полностью непригодным при временном отсутствии сети.
Состояние корзины также хранится в Room.
Для товаров сохраняются, в частности:
- идентификатор;
- название;
- цена;
- бренд;
- количество;
- состояние напоминания.
Изменения корзины поступают в presentation-слой реактивно через Flow.
Для товаров в корзине можно включать напоминания.
Планирование реализовано через AlarmManager.
Для каждого товара создаётся отдельный PendingIntent, после срабатывания которого BroadcastReceiver показывает соответствующее уведомление.
Информация о включённых напоминаниях сохраняется в Room.
AlarmManager не сохраняет запланированные события после перезагрузки устройства, поэтому в приложении реализован BootCompletedReceiver.
После получения:
Intent.ACTION_BOOT_COMPLETED
receiver:
- получает из Room товары с активными напоминаниями;
- повторно планирует напоминание для каждого товара.
Таким образом активные пользовательские напоминания восстанавливаются после перезагрузки устройства.
Приложение отслеживает изменение подключения к интернету.
Для этого используется BroadcastReceiver, реагирующий на изменение connectivity state.
Текущее состояние сети передаётся в UI, где пользователь видит индикатор offline-режима.
Offline-состояние также учитывается при работе с локальным кэшем.
Для внедрения зависимостей используется Hilt.
Через DI предоставляются:
- Retrofit API;
- Room database и DAO;
- реализации репозиториев;
- use case'ы;
- компоненты работы с напоминаниями;
- вспомогательные зависимости.
Это позволяет отделить создание объектов от их использования и упрощает тестирование компонентов приложения.
Изображения товаров загружаются с помощью Glide и интеграции с Jetpack Compose.
Предусмотрено отдельное состояние для случаев, когда URL изображения отсутствует.
Проект содержит unit- и UI-тесты.
Unit-тестами покрывается логика, не требующая запуска Android UI, включая:
- repository-логику;
- преобразование моделей;
- ELM Update;
- обработку состояний и событий.
Для тестирования coroutine-кода используется kotlinx-coroutines-test.
UI-тесты написаны с использованием:
- Kaspresso;
- Kakao Compose;
- Compose UI Test;
- JUnit4.
Среди проверяемых сценариев:
- отображение кнопки очистки для непустой корзины;
- отсутствие кнопки очистки для пустой корзины;
- корректное отображение количества товара;
- отображение изображения товара при наличии URL;
- отображение placeholder при отсутствии изображения.
com.example.productsStore
│
├── data
│ ├── local
│ │ ├── dao
│ │ ├── entity
│ │ └── mapper
│ ├── remote
│ │ ├── api
│ │ ├── dto
│ │ └── mapper
│ └── repositoryImpl
│
├── domain
│ ├── model
│ ├── repository
│ └── useCase
│
├── presentation
│ ├── elm
│ │ ├── cart
│ │ ├── productDetails
│ │ └── productsList
│ └── ui
│
├── di
├── network
└── reminder
В проекте на практике реализованы:
- однонаправленный поток данных посредством KoTEA;
- чистая архитектура: разделение presentation-domain-data;
- dependency injection через Hilt;
- реактивная работа с Room через Flow;
- Room как локальный источник данных для UI;
- синхронизация сетевых и локальных данных;
- проверка актуальности кэша;
- fallback на устаревший кэш при недоступной сети;
- обработка online и offline состояния;
- фоновые напоминания через AlarmManager;
- восстановление напоминаний после перезагрузки Android;
- unit-тестирование бизнес-логики;
- UI-тестирование Jetpack Compose через Kaspresso.