This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Android Modular Template is a production-ready template project for building Android applications with Jetpack Compose and Clean Architecture. The project demonstrates modern best practices through multi-module architecture, type-safe navigation, secure authentication, and comprehensive Firebase integration.
Note: The recording, profile, and settings features are demo implementations showcasing architectural patterns. They serve as reference examples and can be replaced with your own features.
IMPORTANT: Before building, you MUST have google-services.json configured.
-
Firebase Configuration (Required for any build):
- Create Firebase project at Firebase Console
- Add Android app with package:
com.example.myapp(or your chosen package name) - Download
google-services.jsonand place inapp/directory - See GETTING_STARTED.md for detailed instructions
- Note: Update package name after rebranding the project
-
Verify Setup:
./gradlew :app:assembleDevDebug
Add to GitHub repository secrets (Settings → Secrets and variables → Actions):
GOOGLE_SERVICES_JSON- Base64-encodedgoogle-services.jsonfilecat app/google-services.json | base64 | pbcopy
Only needed when ready to publish to Play Store:
- Generate keystore: See
GETTING_STARTED.md - Create
keystore.propertiesfile (gitignored) - Uncomment signing config in
app/build.gradle.kts
For detailed setup instructions, see GETTING_STARTED.md.
./gradlew build./gradlew :app:assembleDebug
./gradlew :core:ui:assemble./gradlew :app:assembleDevDebug # Dev environment, debug build
./gradlew :app:assembleDevRelease # Dev environment, release build
./gradlew :app:assembleProdDebug # Production environment, debug build
./gradlew :app:assembleProdRelease # Production environment, release build./gradlew assembleProdRelease # Production release APK
./gradlew bundleProdRelease # Production release AAB (for Play Store)./gradlew clean build./gradlew :build-logic:assemble./gradlew test # All unit tests
./gradlew :app:test # App module tests
./gradlew :app:testDevDebug # Run tests for specific variant
./gradlew connectedAndroidTest # Instrumented tests (requires device/emulator)./gradlew detekt # Static analysis
./gradlew lint # Android lint (requires google-services.json)
./gradlew lintDevDebug # Lint specific variant./scripts/bump_version.sh patch # 1.0.0 → 1.0.1 (bug fixes)
./scripts/bump_version.sh minor # 1.0.0 → 1.1.0 (new features)
./scripts/bump_version.sh major # 1.0.0 → 2.0.0 (breaking changes)Version info stored in version.properties and automatically applied to builds.
./gradlew createFeature -PfeatureName=myfeatureThis automatically creates:
:feature:myfeature- Feature implementation module:feature:myfeature:api- Navigation route definitions- Build files, manifests, and NavKey boilerplate
- Updates
settings.gradle.kts
./install-hooks.shThis sets up pre-commit hooks that run Detekt and tests automatically.
The project follows a modular architecture with clear separation of concerns:
:app - Main application module, wires features together with Navigation3
:core:ui - Shared UI components and design system (theme, colors, typography)
:core:common - Infrastructure (dispatchers, coroutine scopes, DI qualifiers)
:core:navigation - Navigation3 setup, Navigator wrapper, NavKey definitions
:core:network - Network configuration (Retrofit, OkHttp, auth interceptors)
:core:data - Data layer (repositories, data sources, Room)
:core:domain - Business logic layer (models, use cases, domain entities)
:core:datastore:preferences - Encrypted token storage using Google Tink + DataStore
:core:datastore:proto - Proto DataStore definitions
:core:analytics - Firebase Crashlytics, Analytics, Performance monitoring
:core:notifications - Push notifications (FCM), notification channels
:core:remoteconfig - Firebase Remote Config, feature flags, A/B testing
:feature:recording - Demo: Recording feature (camera, permissions example)
:feature:profile - Demo: Profile feature (CRUD operations example)
:feature:profile:api - Profile routes for cross-feature navigation (no UI)
:feature:settings - Demo: Settings feature (preferences example)
Dependency Flow:
:app→ feature modules,:core:ui,:core:navigation,:core:network,:core:data,:core:analytics,:core:notifications,:core:remoteconfig:feature:*→:core:ui,:core:domain,:core:data,:core:navigation:feature:*:api→:core:navigationonly (sealed route interfaces, no implementation):core:data→:core:network,:core:domain,:core:datastore:preferences,:core:common:core:network→:core:datastore:preferences(for token storage):core:domain→:core:common(for dispatcher qualifiers in use cases):core:datastore:preferences→:core:common(for application scopes):core:analytics→ standalone (Firebase Crashlytics, Analytics, Performance):core:notifications→:core:analytics(for FCM token tracking):core:remoteconfig→ standalone (Firebase Remote Config):core:navigation→ standalone (Navigation3 wrappers):core:ui→ standalone (only UI/theme):core:common→ standalone (infrastructure only - dispatchers, scopes)
The project uses Navigation3 with a modular, type-safe navigation pattern:
- Route Definitions: Each feature defines routes in a sealed interface implementing
NavKey(e.g.,ProfileRoute,RecordingRoute) - API Modules: For cross-feature navigation, create
:feature:name:apimodules containing only route definitions (no UI code) - Hilt Integration: Features register navigation entries using
@IntoSetwithEntryProviderInstaller - Navigator Wrapper:
:core:navigationprovides aNavigatorclass wrapping Navigation3'sNavigationController
Example route definition (in :feature:profile:api):
@Serializable
sealed interface ProfileRoute : NavKey {
@Serializable
data class Profile(val userId: String, val name: String) : ProfileRoute
}
fun Navigator.navigateToProfile(userId: String, name: String) {
navigateTo(ProfileRoute.Profile(userId, name))
}Cross-feature navigation: Features depend on other features' :api modules to navigate without coupling to implementations. For example, :feature:recording can navigate to profile by depending on :feature:profile:api and calling navigator.navigateToProfile().
The project uses a clean network architecture with automatic JWT token management to avoid circular dependencies:
Key Components:
:core:network- Provides Retrofit, OkHttpClient, and auth interceptors:core:data- Implements API services and token refresh logic:core:datastore:preferences- Securely stores JWT tokens with AES-256-GCM encryption
How Token Refresh Works (No Circular Dependencies):
AuthInterceptor(in:core:network) automatically adds access tokens to requestsTokenAuthenticator(in:core:network) intercepts 401 responses and refreshes tokens- The authenticator depends on a
TokenRefreshCallbackinterface (defined in:core:network) :core:dataprovides the implementation ofTokenRefreshCallbackusingAuthApiService- This follows the Dependency Inversion Principle - network layer depends on abstraction, data layer provides implementation
Dependency Flow for Network:
:core:network (defines TokenRefreshCallback interface)
↓
:core:data (implements TokenRefreshCallback using AuthApiService)
No circular dependency! ✅
Configuration:
- API base URL is configured in
:appmodule viaBuildConfig.API_BASE_URL - The
:appmodule provides@ApiBaseUrlvia Hilt for injection into Retrofit - Network configuration (timeouts, logging) is centralized in
:core:network
The project includes comprehensive production monitoring and debugging tools:
:core:analytics Module - Crash reporting, analytics, and performance monitoring:
- Firebase Crashlytics: Automatic crash reporting with CrashlyticsTree for production logging
- Firebase Analytics: Event tracking, screen views, user properties
- Firebase Performance: Automatic performance monitoring
Interface-based design (AnalyticsTracker):
interface AnalyticsTracker {
fun logEvent(eventName: String, params: Map<String, Any>? = null)
fun logScreenView(screenName: String, screenClass: String? = null)
fun setUserId(userId: String?)
fun setUserProperty(name: String, value: String?)
fun logException(throwable: Throwable, message: String? = null)
fun setCustomKey(key: String, value: Any)
fun setAnalyticsCollectionEnabled(enabled: Boolean)
}:core:notifications Module - Push notifications and FCM:
- Firebase Cloud Messaging (FCM) integration
- Notification channels (Android O+)
- FCM token management and topic subscriptions
- Android 13+ notification permission handling
Interface-based design (example NotificationManager interface):
interface NotificationManager {
fun createNotificationChannels()
fun showNotification(channelId: String, notificationId: Int, title: String, message: String, autoCancel: Boolean = true)
suspend fun getFcmToken(): String?
suspend fun subscribeToTopic(topic: String): Result<Unit>
suspend fun unsubscribeFromTopic(topic: String): Result<Unit>
fun hasNotificationPermission(context: Context): Boolean
fun cancelNotification(notificationId: Int)
fun cancelAllNotifications()
}:core:remoteconfig Module - Feature flags and A/B testing:
- Firebase Remote Config integration
- Runtime feature flag management
- Default values for offline support
- 1-hour fetch interval (configurable)
Interface-based design (FeatureFlagManager):
interface FeatureFlagManager {
suspend fun fetchAndActivate(): Result<Boolean>
fun getBoolean(key: String, defaultValue: Boolean = false): Boolean
fun getString(key: String, defaultValue: String = ""): String
fun getLong(key: String, defaultValue: Long = 0L): Long
fun getDouble(key: String, defaultValue: Double = 0.0): Double
}The app uses product flavors for environment separation:
productFlavors {
create("dev") {
dimension = "environment"
applicationIdSuffix = ".dev"
versionNameSuffix = "-dev"
buildConfigField("String", "API_BASE_URL", "\"https://dev-api.example.com/\"")
buildConfigField("String", "ENVIRONMENT", "\"development\"")
}
create("prod") {
dimension = "environment"
buildConfigField("String", "API_BASE_URL", "\"https://api.example.com/\"")
buildConfigField("String", "ENVIRONMENT", "\"production\"")
}
}Available build variants:
devDebug- Development with debug toolsdevRelease- Development with release optimizationsprodDebug- Production with debug tools (for testing)prodRelease- Production release build
LeakCanary (debug builds only):
- Automatic memory leak detection
- Shows leak notifications with detailed traces
- Zero configuration required
Chucker (debug builds only):
- Network traffic inspector
- Shows all HTTP requests/responses in notification
- Searchable request history
- Auto-disabled in release builds (no-op dependency)
Development-Safe Crash Handler:
private fun setupGlobalExceptionHandler() {
val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
analytics.logException(throwable, "Uncaught exception: ${throwable.message}")
analytics.setCustomKey("crash_thread", thread.name)
if (BuildConfig.DEBUG) {
// In debug, let app crash visibly for developer awareness
defaultHandler?.uncaughtException(thread, throwable)
} else {
// In production, report to Crashlytics
defaultHandler?.uncaughtException(thread, throwable)
}
}
}Conditional Timber Logging:
- Debug builds:
Timber.DebugTree()- verbose console logging - Release builds:
CrashlyticsTree()- logs to Crashlytics only
Conditional HTTP Logging:
- Debug builds:
HttpLoggingInterceptor.Level.BODY- full request/response logging - Release builds:
HttpLoggingInterceptor.Level.NONE- no logging for security
The :app module provides @Named("isDebug") Boolean to :core:network for conditional logging configuration.
Backup Rules (app/src/main/res/xml/backup_rules.xml):
- Includes databases and files
- Excludes encrypted DataStore (token storage)
- Excludes device-specific preferences
- Excludes cache directories
Data Extraction Rules (app/src/main/res/xml/data_extraction_rules.xml):
- Cloud backup rules (Android 12+)
- Device transfer rules
- Same security exclusions as backup rules
Custom URL Schemes:
<data android:scheme="myapp" android:host="content" />Example: myapp://content/123
App Links (verified HTTPS):
<data android:scheme="https" android:host="example.com" android:pathPrefix="/content" />Example: https://example.com/content/123
Required: Add google-services.json from Firebase Console to app/ directory.
See GETTING_STARTED.md for detailed setup instructions:
- Create Firebase project
- Add Android app with your package name (e.g.,
com.example.myapp) - Download
google-services.json - Enable Firebase services (Crashlytics, Analytics, FCM, Remote Config)
- Update package name after rebranding
Build without Firebase will fail - google-services.json is required.
Token Storage (TinkAuthStorage in :core:datastore:preferences):
- Access tokens and refresh tokens encrypted using Google Tink (production-grade crypto library)
- Encryption: AES-256-GCM-HKDF via Tink AEAD primitive with hardware-backed keys (Android Keystore)
- Storage backend: DataStore Preferences (encrypted values stored as Base64)
- In-memory cache:
AtomicReferencefor thread-safe synchronous access (OkHttp interceptors) - Performance: Zero
runBlocking- synchronous getters read from cache, async setters update DataStore - Memory management: Cache cleared when app backgrounds (
onTrimMemory()) to reduce memory dump risk - AEAD provides authenticated encryption preventing tampering
- Replaces deprecated
EncryptedSharedPreferences(deprecated April 2024) - Fail-fast on encryption errors (throws
SecurityException- no silent fallback to unencrypted storage)
ProGuard/R8:
- Release builds use R8 with comprehensive keep rules
- Configuration in
app/proguard-rules.pro - Includes rules for:
- Firebase (Crashlytics, Analytics, Messaging, Remote Config, Performance)
- Kotlinx Serialization
- Retrofit and OkHttp
- Room Database
- Hilt Dependency Injection
- LeakCanary and Chucker (debug tools)
Critical: This project uses Gradle convention plugins located in build-logic/ to eliminate boilerplate. All modules use these plugins instead of directly configuring Android/Kotlin settings.
Available Plugins:
convention.android.application- For:appmodule (includes SDK config, test dependencies, Kotlin setup)convention.android.library- For library modules (same as above, but for libraries)convention.android.feature- For feature modules (applies library + compose + hilt + core dependencies automatically)convention.android.compose- Adds Jetpack Compose support (must be applied after application/library plugin)convention.android.hilt- Adds Hilt dependency injection (KSP + dependencies)convention.android.room- Adds Room database support (KSP + dependencies)convention.android.network- Adds networking dependencies (Retrofit, OkHttp, Kotlinx Serialization)
Note: Detekt is applied globally to all subprojects in the root build.gradle.kts - no need to apply it per-module.
Configuration Centralization:
Build Configuration (build-logic/src/main/kotlin/AndroidConfig.kt):
COMPILE_SDK = 36MIN_SDK = 30TARGET_SDK = 36JVM_TARGET = "11"
Branding Configuration (template.properties):
- Package names (
package.base,package.app) - Project names (
project.name,project.name.lowercase) - App display name (
app.display.name) - Plugin ID prefix (
plugin.id.prefix)
All modules access these via the ProjectPropertiesConventionPlugin, which reads from template.properties.
To change SDK versions: Edit AndroidConfig.kt - changes apply to all modules automatically.
To rebrand the project: Run ./rebrand.sh or edit template.properties and rebuild.
The project uses a centralized infrastructure module (:core:common) for coroutine dispatchers and scopes:
// Instead of old approach with separate annotations:
@IoDispatcher, @DefaultDispatcher, @MainDispatcher
// Use type-safe enum-based approach:
@Dispatcher(AppDispatchers.IO)
@Dispatcher(AppDispatchers.Default)
@Dispatcher(AppDispatchers.Main)
@Dispatcher(AppDispatchers.Unconfined)Example usage in use cases:
class LoginUseCase @Inject constructor(
private val authRepository: AuthRepository,
@Dispatcher(AppDispatchers.IO) private val ioDispatcher: CoroutineDispatcher
) {
suspend operator fun invoke(email: String, password: String) =
withContext(ioDispatcher) {
// Login logic
}
}For long-running operations that survive component cancellations:
@ApplicationScope // Uses Dispatchers.Default
@ApplicationScopeIO // Uses Dispatchers.IO (for DataStore, network, etc.)Example usage in TinkAuthStorage:
@Singleton
class TinkAuthStorage @Inject constructor(
@ApplicationContext private val context: Context,
private val dataStore: DataStore<Preferences>,
@ApplicationScopeIO private val scope: CoroutineScope // For DataStore writes
) {
init {
scope.launch {
// Populate cache from DataStore (persists across app lifecycle)
}
}
}Benefits:
- Compile-time safety (typo in dispatcher enum = compile error)
- Self-documenting (
AppDispatchers.IOis clearer than@IoDispatcher) - Centralized in
:core:common- no layering violations - Follows Now in Android best practices
Static code analysis runs automatically on all modules:
./gradlew detektConfiguration: config/detekt/detekt.yml
Install git hooks to run checks before commits:
./install-hooks.shHooks run:
- Detekt static analysis
- Unit tests
To bypass (not recommended): git commit --no-verify
CI Workflow (.github/workflows/ci.yml) - Runs on every push/PR:
- Build all modules
- Run unit tests
- Run Detekt static analysis
- Run Android Lint
- Assemble dev and prod release APKs
Build Release APKs Workflow (.github/workflows/build-release.yml) - Manual or on version tags:
- Builds unsigned release APKs (no keystore required)
- Supports flavor selection: dev, prod, or both
- Uploads APK artifacts for download
- Auto-creates GitHub releases on version tags (v*..)
- Perfect for testing before Play Store deployment
Usage:
# Trigger via GitHub Actions UI:
# 1. Go to Actions tab → "Build Release APKs"
# 2. Click "Run workflow"
# 3. Select flavor (dev/prod/both)
# 4. Download from artifacts
# Or tag a version:
git tag v1.0.0
git push origin v1.0.0
# Auto-creates release with APKs attachedDeploy Workflow (disabled by default):
- See
GETTING_STARTED.mdfor enabling Play Store deployment - Requires keystore setup and Play Store service account
Located in docs/architecture/:
- ADR-001: Multi-Module Architecture
- ADR-002: Navigation3 Adoption
- ADR-003: Token Refresh Strategy
- ADR-004: Convention Plugins System
- ADR-005: Encrypted Token Storage (migrated to Tink 2025-10-19)
- ADR-006: Token Expiration Strategy (proactive refresh with 5-minute buffer)
These documents explain why architectural decisions were made.
Located in docs/api/:
- Authentication endpoints (login, register, refresh)
- User profile management
- Recording session endpoints
Production readiness documentation:
GETTING_STARTED.md- Complete setup guide from local dev to Play Store deployment
Current Status: 100% Development Ready 🎉
What's working now:
- ✅ Local builds (requires
google-services.jsonsetup) - ✅ CI/CD pipelines (requires GitHub secret:
GOOGLE_SERVICES_JSON) - ✅ Build unsigned release APKs via GitHub Actions
- ✅ Firebase integration (Analytics, Crashlytics, FCM, Remote Config)
- ✅ All production features implemented
What's pending (only for Play Store deployment):
- ⏳ Keystore generation (when ready to publish)
- ⏳ Play Store service account (when ready to publish)
- ⏳ Deploy workflow (currently disabled, enable when ready)
Gradle performance settings in gradle.properties:
org.gradle.parallel=true- Parallel module buildsorg.gradle.caching=true- Build cache enabledorg.gradle.configureondemand=true- Configure only needed moduleskotlin.incremental=true- Incremental Kotlin compilation
Release builds use R8 (ProGuard) for:
- Code shrinking
- Code obfuscation
- Resource shrinking
Enable in app/build.gradle.kts:
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(...)
}
}Signing configuration in app/build.gradle.kts (commented by default).
Create keystore.properties (gitignored):
storeFile=../your-keystore.jks
storePassword=YOUR_PASSWORD
keyAlias=YOUR_ALIAS
keyPassword=YOUR_PASSWORD