DEV Community

Cover image for What Really Happens When You Open a Kotlin Android App? A Deep Dive Into the App Lifecycle
Synfinity Dynamics Pvt Ltd
Synfinity Dynamics Pvt Ltd

Posted on

What Really Happens When You Open a Kotlin Android App? A Deep Dive Into the App Lifecycle

Most developers assume something like this:

"When I tap an app icon, Android simply opens my Activity."

It feels true because that's the part we actually write code for. But by the time your Activity shows up on screen, a surprising amount of machinery has already run none of which lives in your MainActivity.kt.

Here's the real sequence, at a glance:

User Taps App Icon
        ↓
Android System Receives Request
        ↓
App Process Created
        ↓
Kotlin Runtime Initialized
        ↓
Application Created
        ↓
Activity Started
        ↓
UI Rendered
        ↓
First Frame Appears
Enter fullscreen mode Exit fullscreen mode

Understanding this chain isn't just trivia. It directly affects:

  • Faster app startup - you can't optimize what you don't understand
  • Better architecture decisions - knowing where DI, database setup, and networking actually fit
  • Avoiding unnecessary initialization - cutting dead weight from Application.onCreate()
  • Improving user experience - fewer frozen frames, faster time-to-interactive

Let's walk through the whole pipeline, step by step.


What Happens When You Tap the App Icon?

The first interaction isn't between the user and your app it's between the user and the Android OS.

Launcher
   ↓
Intent
   ↓
Activity Manager
   ↓
Application Process
Enter fullscreen mode Exit fullscreen mode

When you tap an icon:

  1. The Launcher (itself just another app) fires an Intent targeting your app's main Activity.
  2. ActivityManagerService (AMS) receives that intent and checks whether your app's process is already running.
  3. PackageManager resolves the intent against your app's manifest to figure out exactly which component should handle it.
  4. If no process exists, AMS requests a new process be created for your app.

The key takeaway: the Android OS not your application is driving this. Your code hasn't run a single line yet.


Android Process Creation: The Role of Zygote

Spinning up a brand-new Linux process from scratch for every app launch would be painfully slow. Android sidesteps this with Zygote.

Android System
     ↓
Zygote Process
     ↓
Fork New App Process
     ↓
Application Starts
Enter fullscreen mode Exit fullscreen mode

Zygote is a special process that starts at device boot. It preloads:

  • Core Java/Kotlin classes
  • Common framework resources
  • The Android runtime itself

When your app needs a new process, Android doesn't build one from zero it forks a copy of Zygote. Since Zygote already has the heavy lifting done (classes loaded, runtime warmed up), the fork is fast and the new process inherits all of that preloaded state via copy-on-write memory.

This is one of the biggest reasons Android apps don't take several seconds just to get a process off the ground.


Inside the Android Runtime (ART)

Once the process exists, it needs somewhere to actually execute your Kotlin.

Kotlin Code
     ↓
Kotlin Compiler
     ↓
JVM Bytecode
     ↓
DEX Conversion
     ↓
ART Runtime
     ↓
Device Execution
Enter fullscreen mode Exit fullscreen mode

Your Kotlin source is compiled into JVM bytecode, just like Java. That bytecode is then converted into DEX (Dalvik Executable) format, which the Android Runtime (ART) understands.

ART handles a few important jobs:

  • AOT (Ahead-Of-Time) compilation - parts of your app are compiled to native machine code at install time, so they don't need to be interpreted at runtime
  • JIT (Just-In-Time) compilation - hot code paths get compiled on the fly during execution for further speedups
  • Garbage Collection - automatic memory management, reclaiming objects your app no longer references

This hybrid AOT+JIT approach is part of why modern Android apps perform close to native despite running on a managed runtime.


The Main Thread: Where Your App Begins Execution

Every Android app starts with a single thread the main thread, also called the UI thread.

Main Thread
     ↓
Looper
     ↓
Message Queue
     ↓
Tasks
     ↓
UI Updates
Enter fullscreen mode Exit fullscreen mode

The main thread runs a Looper, which continuously pulls messages off a MessageQueue and dispatches them touch events, lifecycle callbacks, view invalidations, all of it.

This is exactly why blocking the main thread is dangerous. If you run something heavy here, that queue backs up, and the result is:

  • A frozen UI
  • Slower app startup
  • An ANR (Application Not Responding) dialog if it goes on too long

Bad:

override fun onCreate() {
    loadLargeDatabase() // blocks the main thread
}
Enter fullscreen mode Exit fullscreen mode

Better:

viewModelScope.launch {
    loadData() // runs off the main thread, updates UI when ready
}
Enter fullscreen mode Exit fullscreen mode

Application Class Initialization

Before any Activity exists, Android creates your Application object this is typically the very first piece of your Kotlin code to run.

Process Created
     ↓
Application Object Created
     ↓
Application.onCreate()
     ↓
Activity Creation
Enter fullscreen mode Exit fullscreen mode

This is the natural place for global, app-wide setup:

class MyApplication : Application() {

    override fun onCreate() {
        super.onCreate()

        initializeDependencies()
    }
}
Enter fullscreen mode Exit fullscreen mode

Common things people put here:

  • Dependency injection container setup (Hilt, Koin, etc.)
  • Analytics/crash reporting SDKs
  • Database instance creation

The catch: everything in Application.onCreate() runs before your first screen can appear. Overload it, and you've slowed down every single app launch cold, warm, and hot.


Activity Launch Process Internally

With Application.onCreate() done, Android moves on to actually creating your Activity.

ActivityManager
      ↓
ActivityThread
      ↓
Instrumentation
      ↓
Activity Object
      ↓
onCreate()
Enter fullscreen mode Exit fullscreen mode

Internally:

  • ActivityManager tells the app process it's time to launch a specific Activity
  • ActivityThread (running on the main thread) manages the actual creation
  • Instrumentation handles instantiating the Activity class and attaching its Context
  • Only after the Activity object exists and is wired up does your overridden onCreate() finally run

Then the familiar lifecycle kicks off:

onCreate()
   ↓
onStart()
   ↓
onResume()
Enter fullscreen mode Exit fullscreen mode

Understanding Activity Lifecycle During Startup

Activity Created
       ↓
   onCreate()
       ↓
   onStart()
       ↓
   onResume()
       ↓
User Interaction
Enter fullscreen mode Exit fullscreen mode

onCreate()
Used for one-time setup: inflating layouts or calling setContent {}, initializing dependencies, restoring saved state.

onStart()
Called when the Activity becomes visible to the user, but before it's interactive.

onResume()
Called when the Activity is in the foreground and the user can actually interact with it. This is the point where your app is "fully alive."


How Jetpack Compose Changes the Startup Flow

The rise of Compose changes what happens after onCreate() fires.

Traditional View system:

Activity
   ↓
XML Layout
   ↓
View Inflation
   ↓
Rendering
Enter fullscreen mode Exit fullscreen mode

Jetpack Compose:

Activity
   ↓
setContent()
   ↓
Composable Functions
   ↓
Composition
   ↓
UI Rendering
Enter fullscreen mode Exit fullscreen mode

Instead of inflating an XML tree, Compose builds the UI by running composable functions during a phase called Composition, producing a tree of UI nodes directly. When state changes, only the affected composables re-run during Recomposition there's no full layout re-inflation.

This is a real shift in mental model: instead of thinking "inflate this layout once, then mutate views," you think in terms of state driving UI, continuously.


Dependency Injection During App Startup

Frameworks like Hilt, Dagger, and Koin hook directly into this startup sequence.

Application Start
       ↓
DI Container Created
       ↓
Dependencies Provided
       ↓
Activity Uses Objects
Enter fullscreen mode Exit fullscreen mode

Benefits:

  • Cleaner separation of concerns
  • Easier testing (swap real dependencies for fakes)
  • Centralized dependency management

The trade-off: DI containers often build a lot of objects eagerly at startup. If your dependency graph is large or does heavy work in constructors, you'll feel it directly in your cold start time.


Database and Network Initialization

A very common startup mistake is doing all your "setup" work synchronously before showing anything.

Bad startup:

Open App
   ↓
Initialize Database
   ↓
Connect Network
   ↓
Load Configuration
   ↓
Show UI
Enter fullscreen mode Exit fullscreen mode

Better startup:

Open App
   ↓
Show UI Quickly
   ↓
Load Required Data
   ↓
Update Screen
Enter fullscreen mode Exit fullscreen mode

Prefer:

  • Lazy initialization - only set things up when they're actually needed
  • Background loading - fetch data off the main thread while showing a loading state
  • Caching - avoid redundant network/database work on every launch

Cold Start vs Warm Start vs Hot Start

Not all launches are equal. Android distinguishes three startup types:

Cold Start
The app process doesn't exist at all.

New Process
     ↓
Full Initialization
Enter fullscreen mode Exit fullscreen mode

This is the slowest path everything from process creation to first frame happens fresh.

Warm Start
The process is still alive, but the Activity needs to be recreated (e.g., the user navigated away and the system reclaimed some resources, or the Activity was destroyed due to a configuration change).

Hot Start
The app and its Activity are both already in memory the system just needs to bring it back to the foreground.

This is fastest.

Because cold start is the worst-case (and often the first impression a user gets), it's the scenario developers spend the most effort optimizing.


Common Reasons Kotlin Apps Start Slowly

Heavy Application Initialization
Large SDK setup, database migrations, and analytics initialization all running synchronously in Application.onCreate().

Blocking the Main Thread
Network calls, file I/O, or heavy computation executed directly instead of being dispatched to a background dispatcher.

Too Many Dependencies
A sprawling dependency graph means longer initialization and higher memory usage before anything is even shown.

Poor Image and Resource Loading
Loading large images or resources eagerly instead of lazily, with no caching strategy.


How Developers Optimize Android App Startup Time

A practical checklist:

  • Keep Application.onCreate() as lightweight as possible
  • Delay unnecessary initialization until it's actually needed
  • Use lazy loading (by lazy, deferred initialization)
  • Move heavy work off the main thread with coroutines
  • Audit and trim your dependency graph
  • Use the Android App Startup library to sequence and optimize initializer order
  • Actually measure startup instead of guessing

Useful tools:

  • Android Studio Profiler
  • Macrobenchmark library
  • Firebase Performance Monitoring

Measuring App Startup Performance

You can't optimize what you don't measure. Two key metrics:

Time To Initial Display (TTID)
How long until the very first screen/frame appears.

Time To Full Display (TTFD)
How long until the UI is completely populated and ready for real use — not just the first frame, but the fully loaded content.

App Launch
     ↓
First Frame
     ↓
Complete Content
Enter fullscreen mode Exit fullscreen mode

Both matter: a fast first frame with a spinner isn't the same as a genuinely usable screen.


The Complete Kotlin Android Startup Flow

Putting it all together:

User Opens App
        ↓
Launcher Sends Intent
        ↓
Activity Manager Starts App
        ↓
Zygote Creates Process
        ↓
ART Initializes Runtime
        ↓
Main Thread Starts
        ↓
Application.onCreate()
        ↓
Activity Created
        ↓
onCreate()
        ↓
Compose/View Rendering
        ↓
First Frame Displayed
Enter fullscreen mode Exit fullscreen mode

Final Thoughts: Understanding the System Behind Your Code

Writing Kotlin is only one part of Android development. A huge amount happens before your code even gets a chance to run process creation, runtime initialization, thread setup, and lifecycle orchestration, all handled by the OS.

The best Android developers don't just know the APIs. They understand the system that's executing those APIs and that understanding is what turns "my app feels slow" into "here's exactly why, and here's the fix."


📚 Related Reading

Top comments (0)