Skip to content
All posts

androidx.startup in 2026: ordering library initialization without a stack of ContentProviders

A practical guide to Android's App Startup library — consolidating initializers into one ContentProvider, declaring dependencies, and the lazy-init cases it can't replace.

MFKAPPS 4 min read

Every library that needs to run code before your first Activity does it the same way: a ContentProvider with no query logic, whose only job is to fire during app startup. WorkManager does it. Firebase does it. Any analytics or crash-reporting SDK you add does it too. Each one is invisible in your own code, and each one is doing full component initialization — binder registration, a new ContentResolver call — before your app has drawn a single pixel. androidx.startup exists to collapse all of that into one ContentProvider, with an explicit dependency graph instead of an accidental one.

I ran into this while trimming cold-start time on Mintly, where WorkManager, a small logging shim, and my own preload step were each registering their own provider. Here’s what App Startup actually buys you, and where manual lazy init still beats it.

The problem it’s solving

ContentProvider.onCreate() runs on the main thread, before Application.onCreate() returns, for every provider declared in the manifest — in an order Android does not guarantee. That’s fine when there’s one. It stops being fine once three or four libraries each ship their own, because:

  • Each provider is a full component the system has to instantiate and register, with its own fixed overhead.
  • You have no control over ordering. If your logging shim needs WorkManager’s Configuration to already exist, you’re relying on manifest-merge order, which is not a contract.
  • You can’t easily disable one for a build variant — a debug build that doesn’t need crash reporting still pays for that provider’s onCreate().

Multiply the fixed cost of provider instantiation by every SDK in your build.gradle, and it adds up on exactly the metric — cold start — you’re usually trying to protect.

What App Startup replaces it with

androidx.startup defines one InitializationProvider, and your library or app registers Initializer<T> implementations with it instead of shipping a provider of their own. WorkManager itself moved to this model years ago — its WorkManagerInitializer runs through the same InitializationProvider your own initializers do, not a separate one.

An initializer for a small logging setup looks like this:

class LoggingInitializer : Initializer<Unit> {
    override fun create(context: Context) {
        Log.setLogLevel(if (BuildConfig.DEBUG) Log.VERBOSE else Log.WARN)
    }

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

Declared once in the manifest, under the single InitializationProvider merge point:

<provider
    android:name="androidx.startup.InitializationProvider"
    android:authorities="${applicationId}.androidx-startup"
    android:exported="false"
    tools:node="merge">
    <meta-data
        android:name="com.mfkapps.mintly.LoggingInitializer"
        android:value="androidx.startup" />
</provider>

Every library using App Startup merges its <meta-data> entries into this same provider block via manifest merging. You end up with one ContentProvider instance doing the work that used to take one per library.

Declaring real dependencies

The part manifest-order luck used to paper over is dependencies(). If a session-tracking initializer needs WorkManager configured first, say so explicitly:

class SessionTrackingInitializer : Initializer<Unit> {
    override fun create(context: Context) {
        WorkManager.getInstance(context).enqueue(sessionHeartbeatRequest())
    }

    override fun dependencies(): List<Class<out Initializer<*>>> =
        listOf(WorkManagerInitializer::class.java)
}

App Startup builds a dependency graph from these declarations and topologically sorts it, so WorkManagerInitializer.create() always finishes before SessionTrackingInitializer.create() runs — regardless of merge order, regardless of which Gradle module declared which dependency first. That guarantee is the actual feature; consolidating providers is just the mechanism that makes it possible.

Turning one off without deleting the library

The other common need is disabling a specific initializer for a build variant — a debug build that shouldn’t start a production analytics pipeline, for instance. You don’t fork the dependency; you remove its <meta-data> entry in that variant’s manifest:

<!-- src/debug/AndroidManifest.xml -->
<provider
    android:name="androidx.startup.InitializationProvider"
    android:authorities="${applicationId}.androidx-startup"
    tools:node="merge">
    <meta-data
        android:name="com.mfkapps.mintly.analytics.AnalyticsInitializer"
        tools:node="remove" />
</provider>

The library’s code stays untouched; only the trigger that runs it at startup is gone for that variant.

Where it doesn’t help

App Startup only removes the overhead of having a provider. It does nothing about the overhead of what the initializer’s create() method actually does — and create() still runs synchronously on the main thread by default, same as the old ContentProvider.onCreate() did. If an SDK’s initialization genuinely doesn’t need to finish before your first frame, App Startup isn’t the fix; deferring that work to a background coroutine launched from Application.onCreate(), or lazily on first use, is. I moved Mintly’s session-tracking initializer to a WorkManager one-time request that fires after the first frame is drawn, rather than an Initializer at all — it needed to happen soon, not before anything else could.

The rule I ended up with: use androidx.startup for anything a library must configure before other code can safely touch it — WorkManager’s own case, or a dependency injection container other initializers assume exists. Use deferred, lazy, or background init for anything that’s merely convenient to start early. Consolidating providers and ordering the ones that must run early is real value; treating App Startup as a place to put every SDK’s boilerplate just moves the same main-thread cost into a different single provider instead of several.