androidengineers.Book a session

Modern Android Architecture Components

App Startup & Lazy Init

article15 minMedium

Why Startup Time Matters

Cold start time is a first impression. Google Play's performance dashboard surfaces startup metrics; users abandon apps that take more than 2-3 seconds to show content. The primary enemies of fast startup are:

  1. Doing too much in Application.onCreate() — content providers, analytics, logging, and crash reporting all initializing synchronously
  2. Blocking the main thread during initialization
  3. Initializing libraries eagerly when they're only needed later

The Problem: ContentProvider Chains

Many libraries abuse ContentProvider to auto-initialize without any setup code. A library registers a ContentProvider that runs in onCreate() before your Application.onCreate(). With 10 libraries doing this, you get 10 blocking init calls before your first line of code.

<!-- What WorkManager, Firebase, etc. used to do internally -->
<provider
    android:name="androidx.work.impl.WorkManagerInitializer"
    android:authorities="${applicationId}.workmanager-init"
    android:exported="false"
    android:multiprocess="true" />

App Startup Library

The Jetpack App Startup library provides a single ContentProvider (InitializationProvider) that runs all initializers in one pass. Critically, it supports dependency ordering and allows deferring initialization to explicit call time.

// build.gradle.kts
implementation("androidx.startup:startup-runtime:1.1.1")

Implementing an Initializer

// TimberInitializer.kt
class TimberInitializer : Initializer<Unit> {

    override fun create(context: Context): Unit {
        if (BuildConfig.DEBUG) {
            Timber.plant(Timber.DebugTree())
        } else {
            Timber.plant(CrashReportingTree())
        }
        return Unit
    }

    // No dependencies
    override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()
}

// AnalyticsInitializer depends on Timber being ready first
class AnalyticsInitializer : Initializer<AnalyticsTracker> {

    override fun create(context: Context): AnalyticsTracker {
        return AnalyticsTracker.init(context) // returns singleton
    }

    override fun dependencies(): List<Class<out Initializer<*>>> =
        listOf(TimberInitializer::class.java)  // runs after Timber
}

// WorkManagerInitializer (disable WorkManager's own ContentProvider)
class WorkManagerInitializer : Initializer<WorkManager> {

    override fun create(context: Context): WorkManager {
        val config = Configuration.Builder()
            .setMinimumLoggingLevel(Log.INFO)
            .build()
        WorkManager.initialize(context, config)
        return WorkManager.getInstance(context)
    }

    override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()
}

Auto-init Registration in Manifest

<!-- AndroidManifest.xml -->
<provider
    android:name="androidx.startup.InitializationProvider"
    android:authorities="${applicationId}.androidx-startup"
    android:exported="false"
    tools:node="merge">

    <!-- Auto-init: runs during ContentProvider creation -->
    <meta-data
        android:name="com.example.TimberInitializer"
        android:value="androidx.startup" />
    <meta-data
        android:name="com.example.AnalyticsInitializer"
        android:value="androidx.startup" />

    <!-- Disable WorkManager's own ContentProvider -->
    <meta-data
        android:name="androidx.work.WorkManagerInitializer"
        android:value="androidx.startup"
        tools:node="remove" />
</provider>

Manual Init (Lazy, Deferred)

For non-critical initializers, remove from manifest and call explicitly when needed:

<!-- Mark as manual-only: remove from auto-init -->
<meta-data
    android:name="com.example.WorkManagerInitializer"
    android:value="androidx.startup"
    tools:node="remove" />
// Initialize later (e.g., after first screen is displayed)
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Show first screen immediately...

        // Defer non-critical initialization
        lifecycleScope.launch {
            delay(500) // or after onWindowFocusChanged
            AppInitializer.getInstance(this@MainActivity)
                .initializeComponent(WorkManagerInitializer::class.java)
        }
    }
}

Kotlin lazy Delegate Patterns

Kotlin's lazy {} is the primary tool for deferring object construction to first use.

// Thread-safe by default (LazyThreadSafetyMode.SYNCHRONIZED)
private val database: AppDatabase by lazy {
    Room.databaseBuilder(context, AppDatabase::class.java, "app_db").build()
}

// Main-thread-only singleton (no synchronization overhead)
private val adapter: ArticleAdapter by lazy(LazyThreadSafetyMode.NONE) {
    ArticleAdapter()
}

// Custom initialization mode
class HeavyFeatureManager private constructor() {

    companion object {
        // Double-checked locking equivalent
        val instance: HeavyFeatureManager by lazy { HeavyFeatureManager() }
    }

    private val expensiveResource by lazy {
        // Only initialized when first accessed — not at construction time
        DatabaseConnectionPool(size = 10)
    }
}

Application-Level Lazy Singletons

class MyApplication : Application() {

    // Initialized only when first accessed — not blocking onCreate()
    val imageLoader: ImageLoader by lazy {
        ImageLoader.Builder(this)
            .memoryCache { MemoryCache.Builder(this).maxSizePercent(0.25).build() }
            .diskCache { DiskCache.Builder().maxSizeBytes(50 * 1024 * 1024).build() }
            .build()
    }

    val analyticsTracker: AnalyticsTracker by lazy {
        AnalyticsTracker(apiKey = BuildConfig.ANALYTICS_KEY)
    }

    override fun onCreate() {
        super.onCreate()
        // onCreate() is lean — only critical init here
        // TimberInitializer handles Timber via App Startup
    }
}

Baseline Profiles

Baseline Profiles tell the ART compiler which code paths are hot during startup, enabling ahead-of-time compilation instead of interpretation. This reduces cold start time by 20-40%.

// build.gradle.kts
plugins {
    id("androidx.baselineprofile")
}

dependencies {
    baselineProfile(project(":baselineprofile"))
}
// baselineprofile/src/main/kotlin/com/example/BaselineProfileGenerator.kt
@RunWith(AndroidJUnit4::class)
@LargeTest
class BaselineProfileGenerator {

    @get:Rule
    val rule = BaselineProfileRule()

    @Test
    fun generate() {
        rule.collect(packageName = "com.example.app") {
            // Simulate the startup journey
            pressHome()
            startActivityAndWait()

            // Navigate the critical paths you want compiled AOT
            device.findObject(By.text("Explore")).click()
            device.waitForIdle()
            device.findObject(By.text("Articles")).click()
            device.waitForIdle()
        }
    }
}

The generated baseline-prof.txt is packaged into the APK and used by Play's cloud compilation service.


Startup Tracing

Use androidx.tracing to add custom trace sections visible in Perfetto/Android Studio Profiler:

implementation("androidx.tracing:tracing-ktx:1.1.0")

class UserRepository {
    suspend fun loadUsers(): List<User> = withContext(Dispatchers.IO) {
        Trace.beginSection("UserRepository.loadUsers")
        try {
            api.getUsers()
        } finally {
            Trace.endSection()
        }
    }
}

// Kotlin extension
suspend fun <T> tracedSection(name: String, block: suspend () -> T): T =
    trace(name) { block() }

Startup Checklist

CheckAction
App Startup library addedReplace ContentProvider soup with InitializationProvider
Application.onCreate() leanMove all library init to Initializer<T> classes
Non-critical inits deferredUse manual init or lazy {}
Baseline Profile generatedAdd baselineprofile module; run generator against release
Strict mode enabled in debugCatches disk/network reads on main thread
Tracing added to hot pathsMeasure in Android Studio Profiler
// Enable StrictMode in debug to catch main-thread violations early
class MyApplication : Application() {
    override fun onCreate() {
        if (BuildConfig.DEBUG) {
            StrictMode.setThreadPolicy(
                StrictMode.ThreadPolicy.Builder()
                    .detectDiskReads()
                    .detectDiskWrites()
                    .detectNetwork()
                    .penaltyLog()
                    .build()
            )
        }
        super.onCreate()
    }
}

Key Takeaways

ConceptSummary
ContentProvider initLibraries abuse it for auto-init; App Startup consolidates to one
Initializer<T>Declare dependencies; run in topological order; supports manual mode
Auto vs manual initAuto: critical path; manual: deferred to when feature is first needed
lazy {}Kotlin delegate; thread-safe by default; use NONE on main thread only
Baseline ProfilesART AOT compilation of hot paths; 20-40% cold start improvement
StrictModeDebug-only; catches disk/network IO on main thread before shipping

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
App Startup & Lazy Init | Android System Design | Android Engineers