# Startup Flow On Android And iOS

> Understand which points the app passes through from tapping the icon to the first screen, on both Android and iOS.

- URL: https://dantech.academy/en/courses/kotlin-accelerator/engineer-builder/01-production-app-map/lesson-02-trace-app-startup
- Course: [Mobile Action Pro](https://dantech.academy/en/courses/kotlin-accelerator.md)
- Module: Production App Map Architecture
- Duration: 6 min
- Difficulty: beginner
- Access: Free, full lesson
- Tiếng Việt: https://dantech.academy/courses/kotlin-accelerator/engineer-builder/01-production-app-map/lesson-02-trace-app-startup.md

## Objectives

- Trace the two entry points, App.onCreate() and iOSApp.init(), to where they converge in startSharedApp
- Explain the order in which RevenueCat, the shared Koin modules and each platform's modules are loaded
- Pick the right file to open for an early crash, an empty config or a ViewModel injection error

---

## Lesson Goal

You will understand the app's startup flow on both platforms: from the moment the user taps the icon, through the platform creating the process, the app setting up dependencies, Compose being built and navigation being wired, up to the first real screen.

After this lesson, when the app shows a blank screen, crashes before any UI appears, or can't inject a ViewModel, you will know which file to open first, on Android and on iOS.

## Why Learn The Startup Flow

Many annoying bugs happen right as the app opens: a dependency isn't ready, the UI doesn't render, the app crashes before there is any UI. Without a map of startup, you end up debugging in the wrong place.

This app has two entry points, one per platform, but they meet very early:

```text
Android                                  iOS
App.onCreate()                           iOSApp.init()
                                         -> SharedKoinKt.doInitApp()
   \                                       /
    -> startSharedApp(config, platformModules)
       -> Koin graph: shared modules + platform modules

LauncherActivity.setContent { }          ContentView -> MainViewController()
   \                                       /
    -> AppShell()
       -> AppTheme + AppNavHost
       -> Splash -> Menu
```

The platform-specific part is very thin. From `startSharedApp` and `AppShell` onward, both platforms run the same Kotlin code in `:shared-app`.

## Part 1: The Android Entry Point

Open `app/src/main/AndroidManifest.xml`:

| Manifest item                                | What it means                                             |
| -------------------------------------------- | --------------------------------------------------------- |
| `android:name=".App"`                        | Android creates this `Application` before the first activity |
| `.LauncherActivity` with `MAIN` and `LAUNCHER` | The activity opened from the app icon                   |

Then open `app/src/main/java/com/creative/kta/App.kt`. `App.onCreate()` does three things:

1. Turns on Timber logging in debug builds.
2. Calls `androidBuildConfig()` to collect the values from `BuildConfig` (Supabase URL, keys, flags, version) into an `AppBuildConfig`.
3. Calls `startSharedApp(...)` with three Android-specific arguments:
   - `platformModules = androidStartupModules`: Koin modules only Android has, such as the WorkManager workers and the notification scheduler (`app/.../di/AppModule.kt`).
   - `configure`: `androidLogger`, `androidContext(this@App)` and `workManagerFactory()`, which run before the modules are loaded.
   - `afterStart`: registers activity lifecycle callbacks, creates notification channels, schedules the charging sync.

## Part 2: The iOS Entry Point

Open `iosApp/iosApp/iOSApp.swift`. The `init()` of the `iOSApp` struct calls:

```swift
SharedKoinKt.doInitApp()
```

`doInitApp` lives in `shared-app/src/iosMain/kotlin/com/creative/kta/shared/SharedKoin.kt`. The file is named `SharedKoin.kt`, so Swift sees it as the class `SharedKoinKt`. The function is called `doInitApp` instead of `initApp` because Swift imports a Kotlin function whose name starts with `init` as an initializer, and the call site becomes very confusing.

`doInitApp` does the same job as `App.onCreate()`:

1. Calls `iosAppBuildConfig()` to read the values from `Info.plist` (which Xcode fills from `Config.xcconfig`) into the same `AppBuildConfig` data class.
2. Calls `startSharedApp` with `platformModules = listOf(iosPlatformModule)`.
3. `afterStart`: refreshes the notification permission state and calls `checkGoogleSignInConfiguration()`.

`AppBuildConfig` is where the two platforms meet. Each platform reads from its own source (`BuildConfig` or `Info.plist`), and the shared code only sees one data class.

## Part 3: `startSharedApp`, Where They Converge

Open `shared-app/src/commonMain/kotlin/com/creative/kta/shared/StartSharedApp.kt`. `startSharedApp` runs in this order:

```kotlin
require(platformModules.isNotEmpty())
installQzdsImageLoader()
configurePurchases(config)
startKoin {
    configure()
    modules(appKoinModules(config) + platformModules)
}
afterStart()
```

Three details worth learning:

- `configurePurchases` runs before `startKoin`. `SubscriptionManagerImpl` reads `Purchases.isConfigured` when the graph is created. Configure it afterwards and the paywall spins on loading forever. When the premium gate is off, RevenueCat isn't configured at all: this is a build-time kill switch.
- `appKoinModules(config)` (in `AppKoinModules.kt`) is the list of shared modules: `coreModule`, `dataModule`, `dataPlatformModule`, `domainModule`, `sharedAppModule`, `appConfigModule(config)`, plus the features' Koin modules from `FeatureRegistry.koinModules(appFeatureInitializers)`. The `appFeatureInitializers` list is written by hand in `AppFeatures.kt`.
- `require(platformModules.isNotEmpty())` fails early if a platform forgets to pass its modules.

`dataPlatformModule` is an `expect val`. Android and iOS each provide their own `actual` (`DataModule.android.kt`, `DataModule.ios.kt`), binding the same contracts, such as `AudioManager` or `HttpClientEngine`, to that platform's implementation.

This is a pattern you will see in many KMP projects: one shared startup function, and each platform passes in only what it knows.

## Part 4: From Activity And ViewController To `AppShell`

On Android, in `LauncherActivity.onCreate`:

```kotlin
installSplashScreen()
enableEdgeToEdge()
setContent {
    AppShell()
}
```

On iOS, in `ContentView.swift`, the `ComposeView` struct returns `MainViewControllerKt.MainViewController()`. The `MainViewController` function (`shared-app/src/iosMain/.../MainViewController.kt`) is:

```kotlin
fun MainViewController(): UIViewController = ComposeUIViewController {
    AppShell()
}
```

Both draw the same composable. `AppShell` (`shared-app/src/commonMain/.../AppShell.kt`) wraps `AppNavHost` in `AppTheme`:

```kotlin
@Composable
fun AppShell(modifier: Modifier = Modifier) {
    AppTheme {
        AppNavHost(
            navController = rememberNavController(),
            modifier = modifier.fillMaxSize(),
        )
    }
}
```

There is only one copy of the theme and the nav graph. If Android wrapped an extra theme of its own inside `setContent`, the two platforms would drift apart in color with nothing reporting an error. So the activity stays very thin: it only handles what belongs to an Activity, such as the system splash screen, edge-to-edge, background music and the notification schedule on `onResume`/`onPause`.

```mermaid
%%{init: {"theme": "base", "themeVariables": {"background": "transparent", "primaryColor": "#1e293b", "primaryTextColor": "#e2e8f0", "primaryBorderColor": "#475569", "secondaryColor": "#1e293b", "tertiaryColor": "#0f172a", "lineColor": "#64748b", "textColor": "#e2e8f0", "clusterBkg": "#0f172a", "clusterBorder": "#475569", "titleColor": "#cbd5e1", "edgeLabelBackground": "#334155", "fontSize": "15px"}, "flowchart": {"wrappingWidth": 420}}}%%
flowchart TB
    classDef ui fill:#1e293b,stroke:#3b82f6,stroke-width:2px,color:#e2e8f0
    classDef vm fill:#1e293b,stroke:#10b981,stroke-width:2px,color:#e2e8f0
    classDef dom fill:#1e293b,stroke:#f59e0b,stroke-width:2px,color:#e2e8f0
    classDef data fill:#1e293b,stroke:#8b5cf6,stroke-width:2px,color:#e2e8f0
    classDef root fill:#1e293b,stroke:#94a3b8,stroke-width:2px,color:#e2e8f0
    classDef bad fill:#1e293b,stroke:#ef4444,stroke-width:2px,color:#e2e8f0
    classDef good fill:#1e293b,stroke:#10b981,stroke-width:2px,color:#e2e8f0
    classDef note fill:#0f172a,stroke:#475569,stroke-dasharray:4 3,color:#cbd5e1
    accTitle: Startup flow on Android and iOS
    accDescr: Android starts in App.onCreate, iOS starts in the iOSApp init, which calls SharedKoinKt.doInitApp. Both read their config into an AppBuildConfig, then call startSharedApp with their own platform modules. startSharedApp installs the image loader, configures RevenueCat, calls startKoin with appKoinModules plus the platform modules, then runs afterStart. After that, LauncherActivity and MainViewController both draw AppShell.

    subgraph AND["Android"]
        direction TB
        A1["App.onCreate()"]:::root --> A2["androidBuildConfig()<br/>reads BuildConfig"]:::root
        A2 --> A3["platformModules = androidStartupModules"]:::root
    end
    subgraph IOS["iOS"]
        direction TB
        I1["iOSApp init()<br/>SharedKoinKt.doInitApp()"]:::root --> I2["iosAppBuildConfig()<br/>reads Info.plist"]:::root
        I2 --> I3["platformModules = listOf(iosPlatformModule)"]:::root
    end
    subgraph START["startSharedApp(...)"]
        direction TB
        S1["installQzdsImageLoader()"]:::vm --> S2["configurePurchases(config)<br/>before startKoin"]:::vm
        S2 --> S3["startKoin { configure(); modules(appKoinModules(config) + platformModules) }"]:::vm
        S3 --> S4["afterStart()"]:::vm
    end
    A3 --> START
    I3 --> START
    START --> UIA["Android: LauncherActivity<br/>setContent { AppShell() }"]:::ui
    START --> UII["iOS: MainViewController()<br/>ComposeUIViewController { AppShell() }"]:::ui
    UIA --> SHELL["AppShell: AppTheme + AppNavHost"]:::ui
    UII --> SHELL
```

## Part 5: Getting To The First Screen

Open:

```text
shared-app/src/commonMain/kotlin/com/creative/kta/shared/navigation/AppNavHost.kt
shared-app/src/commonMain/kotlin/com/creative/kta/shared/navigation/SplashNavigation.kt
shared-app/src/commonMain/kotlin/com/creative/kta/shared/splash/SplashRoute.kt
shared-app/src/commonMain/kotlin/com/creative/kta/shared/splash/SplashViewModel.kt
```

`AppNavHost` declares `startDestination = SplashRouteData`. Splash decides on its own when to leave:

```text
SplashRoute gets SplashViewModel through koinViewModel()
-> SplashViewModel initializes audio, waits a random delay, emits NavigateToMenu
-> AppNavHost navigates to MenuRouteData, popUpTo<SplashRouteData> { inclusive = true }
-> Splash is removed from the back stack
```

```text
Splash belongs to :shared-app.
Menu belongs to :feature:menu.
AppNavHost connects the two.
```

`AppNavHost` knows the menu screen exists through the `FeatureNavigationProvider`s listed in `appFeatureNavigationProviders` in `AppFeatures.kt`.

## Part 6: When A Missing Binding Shows Up

Koin creates `single` and `viewModel` definitions lazily, the first time someone needs them. If `dataModule` is missing a binding, the app still starts normally. The error only blows up when you open the screen that needs that binding, and maybe only on one platform.

The repo guards against this with `AppGraphTest` (`shared-app/src/commonTest/`). The test builds the whole graph and tries to create every registered binding, along with all of their constructor parameters. Because it lives in `commonTest`, it runs on both the JVM and the iOS Simulator, so it catches a binding registered on only one side. When you change a Koin module, run:

```bash
./gradlew :shared-app:testAndroidHostTest
```

## Quick Debug Table

| Problem                                     | Where to check first                                                      |
| ------------------------------------------- | ------------------------------------------------------------------------- |
| Android crashes before any UI               | `App.kt`, then `startSharedApp`                                           |
| iOS crashes right at launch                 | `SharedKoin.kt` (`doInitApp`), then `startSharedApp`                      |
| Config values are empty on one platform     | `androidBuildConfig()` in `App.kt`, or `iosAppBuildConfig()` and `Config.xcconfig` |
| A ViewModel can't be injected               | The module that provides that ViewModel, `appKoinModules`, then run `AppGraphTest` |
| The app opens but a screen is missing       | `AppNavHost.kt` and `AppFeatures.kt`                                      |
| Splash never goes away                      | `SplashRoute.kt` and the navigation event in `SplashViewModel`            |
| Odd notifications or music while the app is in the background (Android) | `LauncherActivity.onPause()` and `onResume()`  |

## Checkpoint

Read `App.kt`, `SharedKoin.kt` and `StartSharedApp.kt` from top to bottom once.

You are done with this lesson when you can answer:

- Which startup code is Android-only, which is iOS-only, and in which function do they meet?
- Why must RevenueCat be configured before `startKoin`?
- If `dataModule` is missing a binding, does the error appear at app startup or when a screen opens? Which test catches it earlier?
- Why doesn't `LauncherActivity` wrap an extra theme around `AppShell()`?
- Which file decides the first screen the user sees?

---

- Previous lesson: [Read The Module Graph](https://dantech.academy/en/courses/kotlin-accelerator/engineer-builder/01-production-app-map/lesson-01-read-module-graph.md)
- Next lesson: [Feature Registration And Navigation Boundaries](https://dantech.academy/en/courses/kotlin-accelerator/engineer-builder/01-production-app-map/lesson-03-understand-feature-discovery.md)
