SubTracker — Android приложение для управления семейными подписками с поддержкой синхронизации в реальном времени.
SubTracker позволяет семьям совместно управлять подписками на различные сервисы. Приложение поддерживает:
- ✅ Управление подписками (добавление, редактирование, удаление)
- ✅ Отслеживание платежей и истории
- ✅ Семейный доступ с синхронизацией данных
- ✅ Статистика расходов с графиками
- ✅ Напоминания о предстоящих платежах
- ✅ Вход через Google или по коду семьи
- ✅ Гостевой режим
- ✅ Экспорт данных в CSV
- ✅ Темная и светлая темы
- Android Studio (Hedgehog или новее)
- JDK 17 или выше
- Android SDK (API 23+)
- Firebase проект с настроенными сервисами
git clone <repository-url>
cd subTracker- Создайте проект в Firebase Console
- Добавьте Android приложение с package name:
com.example.subtracker - Скачайте
google-services.jsonи поместите вapp/ - Включите следующие сервисы:
- Authentication (Email/Password, Anonymous, Google)
- Cloud Firestore
- Storage (опционально, для аватаров)
- В Firebase Console → Project Settings → Your apps → Android app
- Добавьте SHA-1 fingerprint:
# Для debug keystore: keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android # Для release keystore (если есть): keytool -list -v -keystore <path-to-keystore> -alias <alias-name>
- Скопируйте SHA-1 и добавьте в Firebase Console
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Семьи
match /families/{familyCode} {
allow read: if request.auth != null;
allow write: if request.auth != null;
}
// Пользователи
match /users/{userId} {
allow read: if request.auth != null;
allow write: if request.auth != null && request.auth.uid == userId;
}
// Подписки
match /subscriptions/{subId} {
allow read: if request.auth != null;
allow write: if request.auth != null;
}
// Платежи
match /payments/{paymentId} {
allow read: if request.auth != null;
allow write: if request.auth != null;
}
}
}- Откройте проект в Android Studio
- Дождитесь синхронизации Gradle
- Подключите устройство или запустите эмулятор (API 23+)
- Нажмите Run (
▶️ ) илиShift+F10
- При первом запуске создайте новую семью или войдите по коду
- Для Google Sign-In убедитесь, что SHA-1 добавлен в Firebase
subTracker/
├── app/
│ ├── src/
│ │ ├── main/
│ │ │ ├── java/com/example/subtracker/
│ │ │ │ ├── app/ # Ядро приложения
│ │ │ │ │ ├── di/ # Dependency Injection
│ │ │ │ │ │ └── AppGraph.kt
│ │ │ │ │ ├── MyApp.kt # Application класс
│ │ │ │ │ └── sync/ # Синхронизация
│ │ │ │ │ ├── PendingActionsSyncWorker.kt
│ │ │ │ │ └── SyncScheduler.kt
│ │ │ │ │
│ │ │ │ ├── core/ # Утилиты и модели
│ │ │ │ │ ├── model/
│ │ │ │ │ │ ├── Periodicity.kt
│ │ │ │ │ │ └── Session.kt
│ │ │ │ │ └── time/
│ │ │ │ │ └── NextPaymentCalculator.kt
│ │ │ │ │
│ │ │ │ ├── data/ # Data Layer
│ │ │ │ │ ├── auth/
│ │ │ │ │ │ └── FirebaseAuthRepository.kt
│ │ │ │ │ ├── local/ # Room Database
│ │ │ │ │ │ ├── AppDatabase.kt
│ │ │ │ │ │ ├── dao/ # Data Access Objects
│ │ │ │ │ │ │ ├── PaymentDao.kt
│ │ │ │ │ │ │ ├── PendingActionDao.kt
│ │ │ │ │ │ │ ├── SubscriptionDao.kt
│ │ │ │ │ │ │ └── UserDao.kt
│ │ │ │ │ │ └── entity/ # Room Entities
│ │ │ │ │ │ ├── PaymentEntity.kt
│ │ │ │ │ │ ├── PendingActionEntity.kt
│ │ │ │ │ │ ├── SubscriptionEntity.kt
│ │ │ │ │ │ └── UserEntity.kt
│ │ │ │ │ ├── mapper/ # Маппинг данных
│ │ │ │ │ │ ├── PaymentDomainMappers.kt
│ │ │ │ │ │ ├── PaymentMappers.kt
│ │ │ │ │ │ ├── SubscriptionDomainMappers.kt
│ │ │ │ │ │ ├── SubscriptionMappers.kt
│ │ │ │ │ │ └── UserMappers.kt
│ │ │ │ │ ├── remote/ # Firebase
│ │ │ │ │ │ └── FirestoreSyncDataSource.kt
│ │ │ │ │ └── repository/ # Реализации репозиториев
│ │ │ │ │ ├── PaymentRepositoryImpl.kt
│ │ │ │ │ └── SubscriptionRepositoryImpl.kt
│ │ │ │ │
│ │ │ │ ├── domain/ # Domain Layer
│ │ │ │ │ ├── auth/
│ │ │ │ │ │ ├── AuthRepository.kt
│ │ │ │ │ │ ├── AuthSession.kt
│ │ │ │ │ │ └── usecase/
│ │ │ │ │ │ ├── CreateFamilyUseCase.kt
│ │ │ │ │ │ ├── JoinFamilyUseCase.kt
│ │ │ │ │ │ ├── LoginGuestUseCase.kt
│ │ │ │ │ │ ├── LogoutUseCase.kt
│ │ │ │ │ │ └── RecoverFamilyCodeUseCase.kt
│ │ │ │ │ ├── model/ # Domain Models
│ │ │ │ │ │ ├── Payment.kt
│ │ │ │ │ │ ├── Session.kt
│ │ │ │ │ │ ├── Subscription.kt
│ │ │ │ │ │ └── User.kt
│ │ │ │ │ ├── repository/ # Repository Interfaces
│ │ │ │ │ │ ├── PaymentRepository.kt
│ │ │ │ │ │ └── SubscriptionRepository.kt
│ │ │ │ │ └── usecase/ # Use Cases
│ │ │ │ │ ├── CreateSubscriptionUseCase.kt
│ │ │ │ │ ├── DeleteSubscriptionUseCase.kt
│ │ │ │ │ ├── ObservePaymentsUseCase.kt
│ │ │ │ │ ├── ObserveSubscriptionsUseCase.kt
│ │ │ │ │ ├── PaySubscriptionUseCase.kt
│ │ │ │ │ └── UpdateSubscriptionUseCase.kt
│ │ │ │ │
│ │ │ │ ├── presentation/ # Presentation Layer
│ │ │ │ │ ├── add/
│ │ │ │ │ │ ├── AddSubscriptionViewModel.kt
│ │ │ │ │ │ └── ServiceSearchManager.kt
│ │ │ │ │ ├── auth/
│ │ │ │ │ │ ├── ForgotFamilyCodeDialogManager.kt
│ │ │ │ │ │ ├── LoginViewModel.kt
│ │ │ │ │ │ ├── LoginViewModelFactory.kt
│ │ │ │ │ │ ├── RegisterViewModel.kt
│ │ │ │ │ │ └── RegisterViewModelFactory.kt
│ │ │ │ │ ├── main/
│ │ │ │ │ │ ├── FamilyInfoDialogManager.kt
│ │ │ │ │ │ ├── FilterDialogManager.kt
│ │ │ │ │ │ ├── MainFrameUiBinder.kt
│ │ │ │ │ │ ├── MainFrameUiState.kt
│ │ │ │ │ │ ├── MainFrameViewModel.kt
│ │ │ │ │ │ ├── MainFrameViewModelFactory.kt
│ │ │ │ │ │ ├── SubscriptionAdapter.kt
│ │ │ │ │ │ ├── SubscriptionCardRenderer.kt
│ │ │ │ │ │ └── SubscriptionDialogManager.kt
│ │ │ │ │ ├── settings/
│ │ │ │ │ │ └── SettingsViewModel.kt
│ │ │ │ │ └── stats/
│ │ │ │ │ ├── PaymentCardRenderer.kt
│ │ │ │ │ ├── StatsUiState.kt
│ │ │ │ │ └── StatsViewModel.kt
│ │ │ │ │
│ │ │ │ ├── ui/ # UI Theme (Compose)
│ │ │ │ │ └── theme/
│ │ │ │ │ ├── Color.kt
│ │ │ │ │ ├── Theme.kt
│ │ │ │ │ └── Type.kt
│ │ │ │ │
│ │ │ │ ├── MainActivity.kt # Экран входа
│ │ │ │ ├── RegisterActivity.kt # Регистрация семьи
│ │ │ │ ├── MainFrameActivity.kt # Главный экран
│ │ │ │ ├── AddSubscriptionActivity.kt # Добавление подписки
│ │ │ │ ├── StatsActivity.kt # Статистика
│ │ │ │ ├── SettingsActivity.kt # Настройки
│ │ │ │ │
│ │ │ │ ├── SessionManager.kt # Управление сессией
│ │ │ │ ├── GuestSession.kt # Гостевой режим
│ │ │ │ ├── ThemeManager.kt # Управление темой
│ │ │ │ ├── SubscriptionReminderManager.kt # Напоминания
│ │ │ │ ├── ServiceItems.kt # Список сервисов
│ │ │ │ └── FirebaseSubscription.kt # Firebase модель
│ │ │ │
│ │ │ ├── res/ # Ресурсы
│ │ │ │ ├── layout/ # XML layouts
│ │ │ │ ├── layout-land/ # Landscape layouts
│ │ │ │ ├── values/ # Strings, colors, themes
│ │ │ │ ├── drawable/ # Icons, images
│ │ │ │ └── xml/ # FileProvider configs
│ │ │ │
│ │ │ └── AndroidManifest.xml
│ │ │
│ │ ├── androidTest/ # Android тесты
│ │ └── test/ # Unit тесты
│ │
│ ├── build.gradle.kts # Конфигурация модуля
│ ├── google-services.json # Firebase конфигурация
│ └── proguard-rules.pro # ProGuard правила
│
├── build.gradle.kts # Корневой build файл
├── settings.gradle.kts # Настройки проекта
├── gradle/
│ └── libs.versions.toml # Версии зависимостей
└── README.md # Этот файл
Проект использует Clean Architecture с разделением на три слоя и паттерн MVVM для Presentation слоя.
┌─────────────────────────────────────────────────────────────┐
│ PRESENTATION LAYER │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Activities │ │ ViewModels │ │ UI State │ │
│ │ │ │ │ │ │ │
│ │ MainActivity │ │ LoginViewModel│ │ MainFrameUi │ │
│ │ MainFrame... │ │ MainFrameVM │ │ State │ │
│ │ Settings... │ │ StatsViewModel│ │ StatsUiState │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ └─────────────────┼──────────────────┘ │
│ │ │
└───────────────────────────┼─────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ DOMAIN LAYER │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Use Cases │ │ Models │ │ Repositories │ │
│ │ │ │ │ │ (Interfaces) │ │
│ │ CreateSub │ │ Subscription │ │ Subscription │ │
│ │ UpdateSub │ │ Payment │ │ Repository │ │
│ │ PaySub │ │ User │ │ Payment │ │
│ │ JoinFamily │ │ Session │ │ Repository │ │
│ └──────┬───────┘ └──────────────┘ └──────┬───────┘ │
│ │ │ │
└─────────┼────────────────────────────────────┼──────────────┘
│ │
│ ▼
┌─────────┼────────────────────────────────────┼──────────────┐
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ DATA LAYER │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │ │
│ │ │ Repositories│ │ Local │ │ Remote │ │ │
│ │ │ (Impl) │ │ (Room) │ │ (Firestore)│ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ Subscription│ │ AppDatabase │ │ Firestore │ │ │
│ │ │ Repository │ │ Subscription│ │ Sync │ │ │
│ │ │ Payment │ │ Entity │ │ DataSource │ │ │
│ │ │ Repository │ │ Payment │ │ │ │ │
│ │ │ │ │ Entity │ │ │ │ │
│ │ │ │ │ UserEntity │ │ │ │ │
│ │ └──────────────┘ └──────────────┘ └────────────┘ │ │
│ └──────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
User Action (UI)
│
▼
ViewModel
│
▼
Use Case
│
▼
Repository (Interface)
│
├──► RepositoryImpl
│ │
│ ├──► Room (Local DB) ──► Offline-first
│ │
│ └──► Firestore (Remote) ──► Sync via PendingActions
│
▼
Domain Model
│
▼
UI State Update
│
▼
UI Re-render
┌─────────────────────────────────────────────────────────┐
│ OFFLINE-FIRST STRATEGY │
└─────────────────────────────────────────────────────────┘
User Action (Create/Update/Delete)
│
▼
┌─────────────────┐
│ Room Database │ ← Сохраняем локально сразу
│ (Immediate) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ PendingActions │ ← Добавляем в очередь синхронизации
│ (Queue) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ WorkManager │ ← Фоновая синхронизация
│ Sync Worker │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Firestore │ ← Отправка на сервер
│ (Remote) │
└─────────────────┘
| Поле | Тип | Описание |
|---|---|---|
id |
String (PK) | Firestore document ID |
familyCode |
String | Код семьи |
ownerUid |
String | UID владельца |
ownerUsername |
String | Имя владельца |
name |
String | Название подписки |
price |
Double | Цена |
periodicity |
String | Периодичность (monthly, yearly, etc.) |
iconResName |
String | Имя ресурса иконки |
nextPaymentDate |
Long | Дата следующего платежа (timestamp) |
updatedAt |
Long | Время последнего обновления |
Индексы: familyCode, ownerUid, nextPaymentDate
| Поле | Тип | Описание |
|---|---|---|
id |
String (PK) | Firestore document ID |
familyCode |
String | Код семьи |
subscriptionName |
String | Название подписки |
amount |
Double | Сумма платежа |
ownerUid |
String | UID владельца |
ownerUsername |
String | Имя владельца |
iconResName |
String | Имя ресурса иконки |
paidAt |
Long | Дата оплаты (timestamp) |
updatedAt |
Long | Время последнего обновления |
Индексы: familyCode, paidAt, ownerUid
| Поле | Тип | Описание |
|---|---|---|
id |
String (PK) | Firestore document ID |
uid |
String | Firebase Auth UID |
username |
String | Имя пользователя |
familyCode |
String | Код семьи |
familyName |
String | Название семьи |
role |
String | Роль (admin, member) |
avatarUrl |
String | Путь к локальному файлу аватара |
updatedAt |
Long | Время последнего обновления |
Индексы: familyCode, uid
| Поле | Тип | Описание |
|---|---|---|
id |
String (PK) | UUID действия |
familyCode |
String | Код семьи |
type |
String | Тип действия (PAY, UPDATE, DELETE) |
subId |
String | ID подписки |
payloadJson |
String | JSON с данными действия |
createdAt |
Long | Время создания |
Индексы: familyCode, createdAt
families/{familyCode}
{
familyName: string,
createdAt: timestamp,
adminUid: string
}users/{userId}
{
uid: string,
username: string,
familyCode: string,
familyName: string,
role: "admin" | "member",
avatarUrl: string,
provider: "google" | "manual"
}subscriptions/{subId}
{
familyCode: string,
ownerUid: string,
ownerUsername: string,
name: string,
price: number,
periodicity: string,
iconResName: string,
nextPaymentDate: timestamp,
createdAt: timestamp,
updatedAt: timestamp
}payments/{paymentId}
{
familyCode: string,
subscriptionName: string,
amount: number,
ownerUid: string,
ownerUsername: string,
iconResName: string,
paidAt: timestamp,
createdAt: timestamp
}┌─────────────┐
│ families │
│ │
│ familyCode │◄─────┐
│ familyName │ │
└─────────────┘ │
│
│ (1:N)
┌─────────────┐ │
│ users │ │
│ │ │
│ uid │──────┘
│ familyCode │
│ username │
│ role │
│ avatarUrl │
└─────────────┘
│
│ (1:N)
│
▼
┌─────────────┐
│subscriptions│
│ │
│ id │
│ familyCode │
│ ownerUid │
│ name │
│ price │
│ periodicity │
│ nextPayment │
└──────┬──────┘
│
│ (1:N)
│
▼
┌─────────────┐
│ payments │
│ │
│ id │
│ familyCode │
│ subName │
│ amount │
│ paidAt │
└─────────────┘
-
Android Jetpack
- Lifecycle (2.8.4)
- Room (2.6.1)
- WorkManager (2.8.1)
- ViewModel & LiveData
-
Firebase
- Authentication
- Cloud Firestore
- Storage (опционально)
-
Google Services
- Play Services Auth (20.7.0) - Google Sign-In
-
UI
- Material Components
- Glide (4.16.0) - загрузка изображений
- MPAndroidChart (v3.1.0) - графики
-
Kotlin
- Coroutines (1.7.3)
- Flow