Ensure public interface reliability: Tracking API compatibility for Android and Kotlin

This article explores how to ensure public API reliability by tracking compatibility changes, by exploring the RevenueCat SDK.

Jaewoong Eum
PublishedLast updated

Whether you’re building open-source libraries, SDKs, or common modules for other teams, it’s important to make deliberate and careful changes to public APIs, as they directly impact development resources required for migration.

Unintentional or drastic API (Application Programming Interface) changes can break dependent projects and force consuming developers to spend time adapting to the new APIs. In this context, those developers are essentially your API users.

In large-scale projects, tracking public API changes manually is error-prone and often overlooked. This article covers how to ensure your team stays fully aware of API changes by integrating plugins like Binary Compatibility Validator and Metalava into your project by exploring real-world examples from RevenueCat’s Android SDK.

The approach

When building an independent module that exposes public APIs, whether as a library, SDK, or common module, those APIs become part of a contract with other developers who consume them. Any changes to parameters, method names, or interface/class hierarchies can directly affect these users, potentially causing breakages or requiring significant effort to adopt newer versions.

It’s not necessarily a problem if you’re fully aware of the API changes and communicate them properly. However, unintended or unnoticed changes to the API surface can lead to frustrating developer experiences for your library or SDK users/customers, especially when they encounter breaking changes you never intended to expose.

Also, since Kotlin’s default visibility modifier is public, it’s easy to unintentionally expose or change APIs, making these kinds of issues more likely than you’d expect. Even if you’re not building a library or SDK, you can still apply this approach in a multi-module architecture to track which classes or interfaces are publicly exposed and potentially accessible from other modules.

There are two great tools that help you automatically track and monitor changes to your public APIs: Binary Compatibility Validator and Metalava. These tools ensure you’re aware of any modifications that could impact your users before releasing a new version.

Kotlin binary compatibility validator

The Binary Compatibility Validator, developed by JetBrains, allows you to generate and track a snapshot of your public API, comparing it against a previously recorded baseline to detect and alert you to any binary-incompatible changes.

Dependency configuration

It’s a Kotlin Gradle plugin, making it easy to integrate into your project. You can simply apply it to the library module where you want to track and validate public API changes.

1// libs.versions.toml
2[plugins]
3kotlinx-binaryCompatibilityValidator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version = "0.17.0" }
4
5// root build.gradle.kts
6plugins {
7 alias(libs.plugins.kotlinx.binaryCompatibilityValidator) apply false 
8}
9
10// library module's build.gradle.kts
11plugins {
12 alias(libs.plugins.kotlinx.binaryCompatibilityValidator) 
13}

Once you correctly added the plugin, you can configure the parameters for the plugin by using the apiValidation like the example below:

1apiValidation {  
2    /**
3     * Sub-projects that are excluded from API validation
4     */
5    ignoredProjects.addAll(listOf("apiTester", "composeApp", "mappings"))
6
7    /**
8     * Classes (fully qualified) that are excluded from public API dumps even if they
9     * contain public API.
10     */
11    ignoredClasses.add("com.revenuecat.purchases.BuildConfig")
12        
13    /**
14     * Set of annotations that exclude API from being public.
15     * Typically, it is all kinds of `@InternalApi` annotations that mark
16     * effectively private API that cannot be actually private for technical reasons.
17     */
18    nonPublicMarkers.add("com.revenuecat.purchases.InternalRevenueCatAPI")
19}

RevenueCat’s Kotlin Multiplatform SDK already integrates this plugin to manage its public API surface effectively for a better developer experience. It serves as a great real-world example of how this plugin can be used in practice.

Validating binary compatibility

With the setup complete, you can now easily validate binary compatibility for your target library modules using the following command:

./gradlew apiDump

You’ll notice that an .api file has been automatically generated inside the library module’s api directory. Opening this file will show you the current public API surface, as illustrated below:

public final class com/revenuecat/purchases/kmp/Purchases {
	public static final field Companion Lcom/revenuecat/purchases/kmp/Purchases$Companion;
	public synthetic fun <init> (Lcom/revenuecat/purchases/Purchases;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
	public static final fun canMakePayments (Ljava/util/List;Lkotlin/jvm/functions/Function1;)V
	public final fun checkTrialOrIntroPriceEligibility (Ljava/util/List;Lkotlin/jvm/functions/Function1;)V
	public final fun close ()V
	public final fun collectDeviceIdentifiers ()V
	public static final fun configure (Lcom/revenuecat/purchases/kmp/PurchasesConfiguration;)Lcom/revenuecat/purchases/kmp/Purchases;
	public final fun enableAdServicesAttributionTokenCollection ()V
	public final fun getAppUserID ()Ljava/lang/String;

Another useful command is apiCheck, which builds the project and verifies that the current public API matches the contents of the generated .api file. This task is automatically integrated into the check lifecycle, meaning that both build and check tasks will validate the public API during execution, making it especially helpful for enforcing binary compatibility in CI pipelines.

Metalava

Another powerful tool is Metalava, developed by Google and widely used across the Android platform and AndroidX libraries. It’s a metadata generator designed for JVM-based projects, and it also works with non-Android libraries.

Metalava offers several key features: it can extract public APIs into signature text files, generate stub API files (which can be compiled into artifacts like android.jar), export source-level annotations into external annotation files, and perform API diffs to compare versions and determine whether newer versions remain compatible with previous ones. You can read more about the meaning of the compatibility in Metalava documentation.

So why might you choose Metalava over JetBrains’ Binary Compatibility Validator? One key reason is that, unfortunately, Binary Compatibility Validator doesn’t support modules configured with product flavors. If your library module includes multiple product flavors, you won’t be able to apply the plugin effectively or target specific variants, making Metalava the more flexible option in such cases.

Dependency configuration

You can easily access the full functionality of Metalava by using the metalava-gradle plugin from the open-source community, which serves as a convenient Gradle wrapper around Google’s official Metalava tool.

1// libs.versions.toml
2[plugins]
3metalava = { id = "me.tylerbwong.gradle.metalava", version = "0.4.0-alpha03" }
4
5// root build.gradle.kts
6plugins {
7 alias(libs.plugins.metalava) apply false 
8}
9
10// library module's build.gradle.kts
11plugins {
12 alias(libs.plugins.metalava) 
13}

Once you correctly added the plugin, you can configure the parameters for the plugin by using the `metalava` like the example below:

1metalava {
2    /**
3     * Treat any elements annotated with the given annotation as hidden.
4     */
5    hiddenAnnotations.add("com.revenuecat.purchases.InternalRevenueCatAPI")
6    
7    /**
8     * Allows passing in custom metalava arguments.
9     */   
10    arguments.addAll(listOf("--hide", "ReferencesHidden"))
11    
12    /**
13     * Source directories to exclude.
14     */    
15    excludedSourceSets.setFrom(
16        "src/test",
17        "src/testDefaults",
18        "src/testCustomEntitlementComputation",
19        "src/androidTest",
20        "src/androidTestDefaults",
21        "src/androidTestCustomEntitlementComputation",
22    )
23}

RevenueCat’s Android SDK recently integrated this plugin to effectively manage its public API surface and enhance the overall developer experience. It stands as a good real-world example of how this plugin can be applied in production projects.

Validating binary compatibility

Once the setup is complete, you can validate binary compatibility for your target library modules by running the following command:

./gradlew metalavaGenerateSignature

Let’s assume you want to target the pro and demo product flavors with the release build type. In this case, you can validate binary compatibility using the following commands for each product flavor:

./gradlew metalavaGenerateSignatureProRelease
./gradlew metalavaGenerateSignatureDemoRelease

After running the command, you’ll see that an api.txt file has been automatically generated in the root directory of the library module. This file reflects the current public API surface, as shown in the example below:

// Signature format: 4.0
package com.revenuecat.purchases.ui.revenuecatui {

  @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR) @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY}) public @interface ExperimentalPreviewRevenueCatUIPurchasesAPI {
  }

  public final class PaywallDialogKt {
    method @androidx.compose.runtime.Composable public static void PaywallDialog(com.revenuecat.purchases.ui.revenuecatui.PaywallDialogOptions paywallDialogOptions);
  }

Another valuable command is metalavaCheckCompatibility, which builds the project and checks that the current public API matches the contents of the generated api.txt file. This task is automatically included in the check lifecycle, so both build and check executions will validate the public API, making it particularly useful for maintaining binary compatibility in CI pipelines.

Pre-check with Git hooks and CI pipeline

When combined with CI checks, this plugin becomes especially powerful, allowing you to catch unintended public API changes before pushing or merging code into the project. There are two primary ways to enable this early validation: using Git hooks locally and integrating checks into your CI(Continuous Integration) pipeline, such as with GitHub Actions.

Git hooks

This is one of the simplest ways to pre-check binary compatibility for your APIs. Git hooks let you run specific shell scripts before making a commit, helping prevent unchecked or breaking changes from being pushed to your remote repository by mistake.

./gradlew apiCheck -q
EXIT_CODE=$?
if [ $EXIT_CODE -ne 0 ]; then
  echo "apiCheck failed, running apiDump for you..."

  ./gradlew apiDump -q

  echo "API dump done, please check the results and then try your commit again!"
  exit $EXIT_CODE
fi

CI Pipeline

If your project already uses CI tools like GitHub Actions or CircleCI, integrating binary compatibility checks into your pipeline is one of the most effective ways to automate validation. This ensures that if a commit introduces incompatible API changes, the CI pipeline will fail, preventing the changes from being merged into the main branch until the issue is resolved. It’s a reliable safeguard against accidentally pushing unchecked or breaking changes.

RevenueCat’s Android SDK uses CircleCI, providing a real-world example of how to pre-check binary compatibility with Metalava in a production CI environment.

Conclusion

Key takeaways:

  • Track public API changes automatically to avoid unintentional breakages that frustrate developers.
  • Use Binary Compatibility Validator if you’re building Kotlin libraries without complex flavor configurations.
  • Use Metalava if you need greater flexibility, such as supporting product flavors or JVM-based projects.
  • Integrate API checks into your CI pipelines to catch breaking changes early, before they reach production.
  • Apply these tools even in multi-module projects, not just SDKs, to maintain clean internal and external API boundaries.
  • Reference RevenueCat SDK examples to see real-world, production-ready setups.

In this article, you’ve explored how to improve module reliability by tracking public API changes using two powerful plugins. When developing libraries or SDKs, maintaining a stable and well-defined API surface is key to delivering a consistent and reliable developer experience. 

Monitoring public APIs helps prevent unintended changes that could break downstream usage. Even if you’re not building a library, these tools are still valuable, they help ensure that your library modules aren’t unintentionally exposing or changing classes/interfaces, allowing you to catch and restrict access before they’re used elsewhere in the project.

As always, happy coding!

Jaewoong

You might also like

Share this post

Want to see how RevenueCat can help?

RevenueCat enables us to have one single source of truth for subscriptions and revenue data.

Olivier Lemarié, PhotoroomOlivier Lemarié, Photoroom
Read Case Study