Skip to main content
The app module is the Android application entry point that ties everything together. It contains the CompositionRoot for dependency injection, root navigation setup, platform-specific implementations, and the main activity.

Module Overview

CompositionRoot

Manual dependency injection container

Navigation

App-level navigation setup

Platform Implementations

Android-specific implementations

Application Setup

App class and MainActivity

Module Configuration

build.gradle.kts

plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.compose.compiler)
    alias(libs.plugins.kotlin.serialization)
}

android {
    namespace = "com.denisbrandi.androidrealca"
    compileSdk = 36

    defaultConfig {
        applicationId = "com.denisbrandi.androidrealca"
        minSdk = 24
        targetSdk = 36
        versionCode = 1
        versionName = "1.0"
    }
    // ...
}

dependencies {
    // Library modules
    implementation(project(":designsystem"))
    implementation(project(":cache"))
    implementation(project(":httpclient"))
    
    // Component modules
    implementation(project(":user-component"))
    implementation(project(":product-component"))
    implementation(project(":wishlist-component"))
    implementation(project(":cart-component"))
    
    // UI modules
    implementation(project(":onboarding-ui"))
    implementation(project(":plp-ui"))
    implementation(project(":wishlist-ui"))
    implementation(project(":cart-ui"))
    implementation(project(":main-ui"))
    
    // Android dependencies
    implementation(libs.androidx.core.ktx)
    implementation(libs.androidx.lifecycle.runtime.ktx)
    implementation(libs.androidx.activity.compose)
    implementation(platform(libs.androidx.compose.bom))
    implementation(libs.compose.navigation)
    // ...
}
The app module depends on all other modules - it’s the only module that brings everything together.

CompositionRoot

The CompositionRoot is a manual dependency injection container that constructs and wires all components in the application.

File Structure

app/src/main/java/com/denisbrandi/androidrealca/di/
├── AndroidCacheProvider.kt
└── CompositionRoot.kt

Full Implementation

From app/src/main/java/com/denisbrandi/androidrealca/di/CompositionRoot.kt:
class CompositionRoot private constructor(
    applicationContext: Context
) {
    private val httpClient by lazy {
        RealHttpClientProvider.getClient()
    }
    
    private val cacheProvider by lazy {
        AndroidCacheProvider(applicationContext)
    }
    
    private val userComponentAssembler by lazy {
        UserComponentAssembler(httpClient, cacheProvider)
    }
    
    private val productComponentAssembler by lazy {
        ProductComponentAssembler(httpClient)
    }
    
    private val wishlistComponentAssembler by lazy {
        WishlistComponentAssembler(cacheProvider, userComponentAssembler.getUser)
    }
    
    private val cartComponentAssembler by lazy {
        CartComponentAssembler(cacheProvider, userComponentAssembler.getUser)
    }
    
    val isUserLoggedIn by lazy {
        userComponentAssembler.isUserLoggedIn
    }
    
    val onboardingUIAssembler by lazy {
        OnboardingUIAssembler(userComponentAssembler.login)
    }
    
    val plpUIAssembler by lazy {
        PLPUIAssembler(
            userComponentAssembler.getUser,
            productComponentAssembler.getProducts,
            wishlistComponentAssembler,
            cartComponentAssembler.addCartItem
        )
    }
    
    val wishlistUIAssembler by lazy {
        WishlistUIAssembler(
            wishlistComponentAssembler, 
            cartComponentAssembler.addCartItem
        )
    }
    
    val cartUIAssembler by lazy {
        CartUIAssembler(cartComponentAssembler)
    }
    
    val mainUIAssembler by lazy {
        MainUIAssembler(
            wishlistComponentAssembler.observeUserWishlistIds, 
            cartComponentAssembler.observeUserCart
        )
    }

    companion object {
        lateinit var INSTANCE: CompositionRoot

        fun compose(applicationContext: Context) {
            INSTANCE = CompositionRoot(applicationContext)
        }
    }
}

val compositionRoot = CompositionRoot.INSTANCE
1

Singleton Instance

The CompositionRoot uses a singleton pattern initialized once in the Application class.
2

Lazy Initialization

All dependencies use by lazy to defer creation until first use, improving app startup time.
3

Dependency Graph Construction

The composition root builds the entire dependency graph:
  1. Creates platform implementations (httpClient, cacheProvider)
  2. Creates component assemblers with their dependencies
  3. Creates UI assemblers with required use cases
4

Public API

Only UI assemblers and essential use cases (like isUserLoggedIn) are exposed publicly.

Dependency Flow

No Dependency Injection FrameworkThis architecture uses manual dependency injection instead of frameworks like Dagger or Hilt. This approach:
  • Provides full control and transparency
  • Reduces build time and complexity
  • Makes the dependency graph explicit and debuggable
  • Eliminates framework-specific annotations and generated code

The app module defines the root navigation graph that connects all screens. From app/src/main/java/com/denisbrandi/androidrealca/navigation/RootNavigation.kt:
@Serializable
object NavSplash

@Serializable
object NavLogin

@Serializable
object NavMain

@Composable
fun RootNavigation() {
    val navController = rememberNavController()
    NavHost(navController, startDestination = NavSplash) {
        composable<NavSplash> {
            val destination: Any = if (compositionRoot.isUserLoggedIn()) {
                NavMain
            } else {
                NavLogin
            }
            navController.navigate(route = destination)
        }
        composable<NavLogin> {
            compositionRoot.onboardingUIAssembler.LoginScreenDestination(
                onLoggedIn = { navController.navigate(route = NavMain) }
            )
        }
        composable<NavMain> {
            compositionRoot.mainUIAssembler.MainScreenDestination(
                RealBottomNavRouter
            )
        }
    }
}
1

Type-Safe Navigation

Uses Kotlin serialization for type-safe navigation with @Serializable route objects.
2

Splash Logic

The splash screen checks authentication state and navigates to either login or main screen.
3

Screen Composition

Screens are created via UI assemblers accessed through the compositionRoot.

Bottom Navigation Router

private object RealBottomNavRouter : BottomNavRouter {
    @Composable
    override fun OpenPLPScreen() {
        compositionRoot.plpUIAssembler.PLPScreenDestination()
    }

    @Composable
    override fun OpenWishlistScreen() {
        compositionRoot.wishlistUIAssembler.WishlistScreenDestination()
    }

    @Composable
    override fun OpenCartScreen() {
        compositionRoot.cartUIAssembler.CartScreenDestination()
    }
}
The BottomNavRouter implementation is defined in the app module, allowing it to access all UI assemblers while keeping the main-ui module decoupled from other UI modules.

Platform Implementations

The app module provides Android-specific implementations of cross-platform interfaces.

AndroidCacheProvider

From app/src/main/java/com/denisbrandi/androidrealca/di/AndroidCacheProvider.kt:
class AndroidCacheProvider(
    private val applicationContext: Context
) : CacheProvider {

    private val settings by lazy {
        val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(
            applicationContext
        )
        SharedPreferencesSettings(sharedPrefs)
    }

    override fun <T : Any> getCachedObject(
        fileName: String,
        serializer: KSerializer<T>,
        defaultValue: T
    ): CachedObject<T> {
        return RealCachedObject(fileName, settings, serializer, defaultValue)
    }

    override fun <T : Any> getFlowCachedObject(
        fileName: String,
        serializer: KSerializer<T>,
        defaultValue: T
    ): FlowCachedObject<T> {
        return RealFlowCachedObject(getCachedObject(fileName, serializer, defaultValue))
    }
}
The AndroidCacheProvider adapts the multiplatform cache module to use Android’s SharedPreferences via the Multiplatform Settings library.

Application Setup

App Class

From app/src/main/java/com/denisbrandi/androidrealca/App.kt:
class App : Application() {
    override fun onCreate() {
        super.onCreate()
        CompositionRoot.compose(applicationContext)
    }
}
The App class initializes the CompositionRoot during application startup, ensuring dependencies are ready before any activity launches.

MainActivity

From app/src/main/java/com/denisbrandi/androidrealca/MainActivity.kt:
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            AppTheme {
                RootNavigation()
            }
        }
    }
}
1

Edge-to-Edge Display

Enables edge-to-edge display for modern Android UI.
2

Theme Application

Wraps the app in AppTheme from the designsystem module.
3

Navigation Setup

Launches the root navigation graph.

Module Dependency Graph

The app module sits at the top of the dependency hierarchy:

Testing the App Module

While the app module primarily contains wiring code, you can test:
Test that the composition root correctly wires dependencies:
@Test
fun `composition root provides all UI assemblers`() {
    val context = ApplicationProvider.getApplicationContext<Context>()
    CompositionRoot.compose(context)
    
    assertNotNull(compositionRoot.onboardingUIAssembler)
    assertNotNull(compositionRoot.plpUIAssembler)
    assertNotNull(compositionRoot.wishlistUIAssembler)
    assertNotNull(compositionRoot.cartUIAssembler)
    assertNotNull(compositionRoot.mainUIAssembler)
}

Best Practices

Single Composition RootNever create multiple composition roots or partial dependency graphs. All dependency construction happens in one place.
Lazy InitializationUse by lazy for all dependencies to defer creation until needed. This improves app startup performance.
Private by DefaultKeep component assemblers and infrastructure private. Only expose UI assemblers and essential use cases.
Application ContextAlways use applicationContext instead of activity context in the composition root to avoid memory leaks.

Composition Root Guidelines

1

Initialize in Application.onCreate()

Compose the dependency graph in Application.onCreate() before any activities start.
2

Use Constructor Injection

Pass dependencies through constructors at every level - never use service locator pattern.
3

Wire from Bottom Up

Create library modules first, then components, then UI assemblers.
4

Keep It Simple

Don’t add abstractions or indirection. The composition root should be straightforward and explicit.
1

Use Type-Safe Routes

Define route objects with @Serializable for compile-time safety.
2

Handle Back Stack Carefully

Clear the back stack when navigating between major flows (login → main).
3

Pass Callbacks, Not NavController

UI modules should receive navigation callbacks, not direct access to NavController.
4

Centralize Deep Linking

Handle all deep links in the root navigation graph for consistency.

Comparison with DI Frameworks

Manual dependency injection vs. frameworks like Dagger/Hilt:
AspectManual DIDagger/Hilt
Setup ComplexitySimple, explicit codeRequires annotations, modules
Build TimeFastSlower due to code generation
DebuggabilityEasy - just follow the codeHarder - generated code
Learning CurveMinimalSteep
Type SafetyFull compile-time checkingFull compile-time checking
FlexibilityComplete controlFramework constraints
TestingEasy - construct test graphsRequires test modules
For small to medium apps (< 50 screens), manual dependency injection provides the best developer experience. Only consider frameworks when the composition root becomes unmanageable.

Summary

The app module is where Real Clean Architecture comes together: Single entry point for the entire application ✅ Manual dependency injection via CompositionRootType-safe navigation with Jetpack Compose Navigation ✅ Platform-specific implementations of cross-platform interfaces ✅ Clear separation between infrastructure (app) and features (UI modules) ✅ Explicit dependency graph that’s easy to understand and debug By keeping the app module focused on wiring and navigation, the architecture remains clean, testable, and maintainable.

Build docs developers (and LLMs) love