May 14, 2020
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:
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:
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.
It appears that{" "} @amazonmusic {" "} is indeed shipping with LeakCanary in production.
— Py ⚔ (@Piwai) April 2, 2020
I downloaded Amazon Music version 16.9.1 from Play Store and decompiled with ShowJava and did find LeakCanary v1.6 in it. The quoted tweet shows 2.2 though?{" "} https://t.co/hsGNUHnOE9{" "} pic.twitter.com/Qg8DhHPzEd
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:
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.
<!-- AndroidManifest file of the library -->
<provider
android:name=".WhatTheStackInitProvider"
android:authorities="${applicationId}.WhatTheStackInitProvider"
android:exported="false"
android:enabled="true"/>
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’sAndroidManifest
file. Thename
property points to the class which is used as theContentProvider
.authorities
defines a unique identifier for it.exported
is set tofalse
as we don’t want other applications to use this content provider, andenabled
property is set totrue
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 theonCreate()
callback. This method is supposed to returntrue
if the initialization of the content provider is successful, andfalse
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 theContentProvider
itself, and here we use it to validate theauthority
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:
<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
Footnotes
-
The example used is of Stetho-no-op ↩
-
This field is created using the
authorities
property in theAndroidManifest
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 theauthorities
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. ↩