Application entry point with dependency injection and navigation
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.
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:
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.
While the app module primarily contains wiring code, you can test:
Integration Tests
Test that the composition root correctly wires dependencies:
@Testfun `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)}
Navigation Tests
Test navigation flows:
@Testfun `splash navigates to login when not logged in`() { // Set up test composition root with stubbed isUserLoggedIn // Verify navigation to login screen}@Testfun `splash navigates to main when logged in`() { // Set up test composition root with logged-in user // Verify navigation to main screen}
Manual dependency injection vs. frameworks like Dagger/Hilt:
Aspect
Manual DI
Dagger/Hilt
Setup Complexity
Simple, explicit code
Requires annotations, modules
Build Time
Fast
Slower due to code generation
Debuggability
Easy - just follow the code
Harder - generated code
Learning Curve
Minimal
Steep
Type Safety
Full compile-time checking
Full compile-time checking
Flexibility
Complete control
Framework constraints
Testing
Easy - construct test graphs
Requires 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.
The app module is where Real Clean Architecture comes together:✅ Single entry point for the entire application
✅ Manual dependency injection via CompositionRoot
✅ Type-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 debugBy keeping the app module focused on wiring and navigation, the architecture remains clean, testable, and maintainable.