When “Compat” libraries won’t save you

Danny Preussler
ProAndroidDev
Published in
5 min readJan 4, 2021

--

And why you should avoid using the “NewApi” suppression!

https://unsplash.com/photos/EgGIPA68Nwo

The idea of “Compat” libraries was probably one of the key aspects of Android dominating the mobile space. Other than with iOS, Android users often could not update their operating system after a new version launch, simply as their phones won’t allow them to, the Android problem of fragmentation. But developers still wanted to use the latest features to compete. The solution was simple: instead of adding new APIs to the operating system, you shipped those directly with your app by using a “backport” version Google gave you.

It all started with ActionBar Sherlock by Jake Wharton then got adopted by Google with in their “support libraries”. Later on, this was mirrored as AndroidX under the Jetpack umbrella.

Same but different

Under the hood, not all of those “compat”-APIs are made the same way. Some, like the ones for Fragments, are complete copies of the code. You either use android.app.Fragment from the OS (actually deprecated) or androidx.fragment.app.Fragment. Both don't share any code or have a common base class (which is why we also have two versions of the FragmentManager).

On the other handAppCompatActivity for example, simply extends the original Activity. AlsoAppCompatImageButton still is an ImageButton!

We can see that sometimes these “Compat”-classes are just a “bridge” to add missing functionalities and sometimes they are complete duplicates.

Let’s look at another example!

One area that changed a lot over time is the notification API from Android. There was a time where every Google I/O introduced a new API change.

Good that we have NotificationManagerCompat to save us!?

If, for example, we need to get the notification channel groups:

val groups = notificationManagerCompat.notificationChannelGroups

We don't need to worry about the groups being supported on all OS versions, as it is handled under the hood for us:

public List<NotificationChannelGroup> getNotificationChannelGroups() {
if (Build.VERSION.SDK_INT >= 26) {
return mNotificationManager.getNotificationChannelGroups();
}
return Collections.emptyList();
}

If we were before API level 26 we simply get an empty list, otherwise, we get the new channel groups that were introduced in 26.

You can find even more complex checks inside NotificationManagerCompat.

But if you look closer,NotificationManagerCompat will return us the actual API classes. In the example code above a list of NotificationChannelGroup, this is not a copied “compat”-version, but as of the check, safe to use:

val groups = notificationManagerCompat.notificationChannelGroups
val channels = groups.flatMap {
it.channels.filter { it.shouldShowLights() }
}

Here we only want those groups whose channels are triggering lights, which is API level 26 and above. As we are using a class that is of higher API level than our minimum SDK, the compiler will warn us here:

The compiler doesn't care that we used NotificationManagerCompat to get there.

We have multiple ways of solving this:

Adding the RequiresApi annotation to our method won’t make much sense, as we would simply move the warning to the calling function. Surrounding with a check feels obsolete as this check was already done by NotificationManagerCompat as shown above!

Seems the best option is to pick suppression:

@SuppressLint("NewApi")
private fun checkChannels() {
val groups = notificationManagerCompat.notificationChannelGroups
val channels = groups.flatMap {
it.channels.filter { it.shouldShowLights() }
}
...
}

New requirements coming in

Let’s assume we get the additional requirement to filter out the groups that got blocked. We can add a simple check for that:

@SuppressLint("NewApi")
private fun checkChannels() {
val groups = notificationManager.notificationChannelGroups
val channels = groups.filterNot { it.isBlocked }.flatMap {
it.channels.filter { it.shouldShowLights()}
}
...
}

Everything looks fine, right?

Boom!

But we just introduced a crash!
The reason is: isBlocked was only introduced in API level 28 and we did not check for that! Despite we used NotificationManagerCompat, we still ran into an API level issue!

And because we suppressed theNewApi warnings, we didn't get any warning on this one!

We need to be really careful when it comes to suppression of this annotation!

Solutions?

As it is only available for method-level (not for individual statements), the best approach is to compose one-liner methods that can fit our needs.

Thanks to extension functions this can be very easy:

@SuppressLint("NewApi") // SDK 26
fun NotificationChannelGroup.lightingChannels() =
channels.filterLightingOnes()

@SuppressLint("NewApi") // SDK 26
private fun List<NotificationChannel>.filterLightingOnes() =
filter { it.shouldShowLights() }

If we used this approach with the above example, we would have gotten the warning the moment we added isBlocked.

Of course, it is a bit more work for us as developers but our users will appreciate a crash-free app!

The Linter

The example shown was not a bug of a compat-library but rather hidden by the suppression. This could have happened with many other APIs as well.

Don’t fall into this trap !
Using Compat versions might give us false security and trick us into believing that we won’t have to think about these issues.

And again, try to avoid suppressing NewApi!

Instead, use direct version checks like:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)

Unfortunately, the linter is not very smart here. It would not understand slight variations like:

.filter { Build.VERSION.SDK_INT >= Build.VERSION_CODES.P }

Call for help?

Maybe some of you want to look more into this, with some custom lint rules. Basically, we would need something like:

@CheckedUpTo(Build.VERSION_CODES.P)

That would internally work similar to SuppressLint(“NewApi”) but only for API calls that require nothing higher than P.

For now, make the existing linter functionality work for you. For example by also adding @RequiresApi(Build.VERSION_CODES.P) to your own code, so you are always forced to handle those.

Remember, these annotations are also considered as documentation to the reader of your code.

PS: the latest alpha of NotificationCompat will bring us compat-versions for NotificationChannel and NotificationChannelGroup 🥳

--

--

Android @ Soundcloud, Google Developer Expert, Goth, Geek, writing about the daily crazy things in developer life with #Android and #Kotlin