Exploiting ContentProvider to initalize libraries

Update: 17/06/2020
A month after this blog post, the Jetpack team launched the App Startup library to replace usage of ContentProviders for running init logic at app startup. Read more about it here

Library developers on Android often require applications to initialize their code in the Application class. One of my libraries, WhatTheStack, requires explicit initialization on startup as well:

1
2
3
4
5
6
7
8
class MainApplication : Application() {
  override fun onCreate() {
    super.onCreate()
    if (BuildConfig.DEBUG) {
      WhatTheStack(this).init()
    }
  }
}

While this approach allows fine grained control over what gets initialized, it has a few drawbacks.

Bloat

As an application grows, so does the list of things that need to be initialized with it. It is not uncommon to find code like this in Android projects:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class MainApplication : Application() {
  override fun onCreate(){
    super.onCreate()
    Fabric.with(this, new Crashlytics())
    JodaTimeAndroid.init(this)
    Realm.init(this)
    if (BuildConfig.DEBUG) {
      Stetho.initializeWithDefaults(this)
      WhatTheStack(this).init()
    }
  }
}

This code snippet is noisy and obscure. It gets progressively difficult to scan it as the list grows. It also makes it easy to initialize something that shouldn’t be.


Debug-only dependencies

Some libraries are meant to be included in debug builds only.

Including them in a disabled form in release builds would only increase the application size, or create extra work for the code stripper. For such libraries it is semantically more correct to add them with debugImplementation instead of implementation. However, there is a catch in doing so:

Referencing debugImplementation libraries in your application code will result in compilation errors when creating release builds.

This problem has led to the creation of no-op variants of popular libraries1. The build setup with no-op variants requires a different releaseImplementation line:

1
2
debugImplementation "com.facebook.stetho:stetho:1.5.1"
releaseImplementation "net.igenius:stetho-no-op:1.1"

While this is not ugly, it certainly is inconvenient. Why do I have to include code that is meant to do nothing just to fix compilation errors?


ContentProvider to the rescue

A ContentProvider is a core Android component used for making an app’s data available to other apps. We shall exploit one of its nifty characteristics:

An app’s ContentProviders are initialized automatically by the system on launch.

To see how this characteristic can be leveraged for auto initialization of our library code, let’s look at WhatTheStack’s implementation2.

1
2
3
4
5
6
<!-- AndroidManifest file of the library -->
<provider
  android:name=".WhatTheStackInitProvider"
  android:authorities="${applicationId}.WhatTheStackInitProvider"
  android:exported="false"
  android:enabled="true"/>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
class WhatTheStackInitProvider : ContentProvider() {

  override fun onCreate(): Boolean {
    val applicationContext = context ?: return false
    WhatTheStackInitializer.init(applicationContext)
    return true
  }

  override fun attachInfo(context: Context?, info: ProviderInfo?) {
    super.attachInfo(context, info)
    checkProperAuthority(info)
  }

  private fun checkProperAuthority(info: ProviderInfo?) {
    val contentProviderName = WhatTheStackInitProvider::class.java.name
    require(info?.authority != contentProviderName) {
      "Please provide an applicationId inside your application's build.gradle file"
    }
  }

  // Other overrides
}

There is a lot to unpack here, so let’s go through it step by step.

  • First we need to delcare a <provider> element in the library’s AndroidManifest file. The name property points to the class which is used as the ContentProvider. authorities defines a unique identifier for it. exported is set to false as we don’t want other applications to use this content provider, and enabled property is set to true to inform the OS that it can be used.

  • Second, we need to define a class extending ContentProvider. Upon creation, an instance of this class receives the onCreate() callback. This method is supposed to return true if the initialization of the content provider is successful, and false otherwise. The library is initialized here.

Caution
This callback is run on the main thread of the consuming application. DO NOT perform long running operations here.
  • Next, it receives the attachInfo() callback. This contains information about the ContentProvider itself, and here we use it to validate the authority property3.

… and Voila!

With this setup in place, our library is initialized automatically, and our consumers do not need to handle it in their application class. Not only that, if our library is meant to be used in debug builds only then the dependency on it can be changed to debugImplementation without requiring a no-op variant! 🎉


Disabling automatic initialization

The content provider can be prevented from running at application start by adding the following block in the application’s manifest:

1
2
3
4
<provider
  android:name="com.haroldadmin.whatthestack.WhatTheStackInitProvider"
  android:authorities="${applicationId}.WhatTheStackInitProvider"
  tools:node="remove" />

Exercise caution

A lot of libraries use this approach to hide away init logic, such as Firebase, LeakCanary, and even WorkManager

While this is a neat trick, not everyone should rush to update their libraries with automatic initialization. Explicitness and verbosity have their value, and hiding away complexity behind a magic curtain is not always a good idea.

If you are using this trick, make sure your consumers know about it, and give them the option to disable it as well.

Question, comments or feedback? Feel free to reach out to me on Twitter @haroldadmin


  1. The example used is of Stetho-no-op ↩︎

  2. The PR containing these changes can be found here ↩︎

  3. This field is created using the authorities property in the AndroidManifest file. We declare it to be unique by placing the ${applicationId} suffix on it. In some applications, this ID property is not configured correctly, in which case the authorities property uses the library’s package name as the fallback. If there are multiple such applications on a user’s device, they will all have this content provider with the same value on the authority property, which means it is no longer a unique identifier. Therefore it is necessary to perform this validation. ↩︎