Keeping public API in check with the Kotlin binary validator plugin
Tags: android, kotlin, library
Published: 2021-07-14 07:50:56

Within the [Stream Chat Android SDK](https://github.com/GetStream/stream-chat-android) project, we use the [Kotlin binary compatibility validator plugin](https://github.com/Kotlin/binary-compatibility-validator) to keep track of all the changes we make to our public API. This is a first-party plugin by JetBrains, though it's still experimental (it's an [incubator project](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub) by JetBrains on GitHub).

In this article, you'll learn what the plugin does, how to set up and configure it, and how you can use it to make your library project better.

## Core concept

Let's start with a TL;DR of what the plugin does:

> It generates `.api` files that describe the public API for each module, and if you make changes to public API, you have to explicitly update the content of the `.api` files (otherwise, checks will fail, alerting you of the accidental API change).

By doing this, it guarantees that developers on the project are always aware of the exact changes they're making to public API. Since the plugin works on binary API, it will catch incompatible changes that might not be obvious when looking at source code. For example, when writing Kotlin code, using data classes, default implementations, or companion objects might create binary API that you're usually unaware of.

## Setup and configuration

The binary validator is available as a simple Gradle plugin, which makes setup really quick. Just add this code to your top level `build.gradle` file:

```groovy
buildscript {
    dependencies {
        classpath 'org.jetbrains.kotlinx:binary-compatibility-validator:0.6.0'
    }
}

apply plugin: 'binary-compatibility-validator'
```

With the plugin added, it's now time to configure it. You can do this inside the `apiValidation` block. Here's [the configuration we use in our SDK](https://github.com/GetStream/stream-chat-android/blob/fd990322367c97eafc165995de1f065a9c89d9ec/build.gradle#L66-L82) as an example:

```groovy
apiValidation {
    ignoredPackages += [ // 1
            'com/getstream/sdk/chat/databinding',
            'io/getstream/chat/android/ui/databinding',
    ]

    ignoredProjects += [ // 2
            'stream-chat-android-docs',
            'stream-chat-android-sample',
            'stream-chat-android-ui-components-sample',
            'stream-chat-android-test',
    ]

    nonPublicMarkers += [ // 3
            'io.getstream.chat.android.core.internal.InternalStreamChatApi',
    ]
}
```

Let's look at each of the options we're using:

1. `ignoredPackages` allows you to exclude some packages from validation. In our case, we don't want to keep track of the files generated by [View Binding](https://developer.android.com/topic/libraries/view-binding). You could also use `ignoredClasses` to exclude individual classes by name.
2. By default, the plugin will be enabled for all modules of a project. We use `ignoredProjects` to exclude non-published modules, like documentation and sample apps.
3. The `nonPublicMarkers` entry allows you to specify any Kotlin [Opt-in annotations](https://kotlinlang.org/docs/opt-in-requirements.html) that you're using on API that's not considered publicly available. To learn more about these, watch [Mastering API Visibility in Kotlin](https://zsmb.co/talks/mastering-api-visibility/).

## Usage

To (re)generate the `.api` files, you'll have to run the `apiDump` Gradle task. When you're doing this for the first time, you should review all the generated `.api` files to make sure that everything in there is *really supposed to be* public API, and then commit them to your repository.

Here's a small sample of what you'll see in these files:

```
public abstract interface class io/getstream/chat/android/ui/StyleTransformer {
	public abstract fun transform (Ljava/lang/Object;)Ljava/lang/Object;
}

public final class io/getstream/chat/android/ui/common/Debouncer {
	public fun <init> (J)V
	public final fun shutdown ()V
	public final fun submit (Lkotlin/jvm/functions/Function0;)V
	public final fun submitSuspendable (Lkotlin/jvm/functions/Function1;)V
}

public abstract interface class io/getstream/chat/android/ui/common/UrlSigner {
	public abstract fun signFileUrl (Ljava/lang/String;)Ljava/lang/String;
	public abstract fun signImageUrl (Ljava/lang/String;)Ljava/lang/String;
}
```

You can then use the `apiCheck` Gradle task to verify that your current source code still has the same API as your committed `.api` files. This task will create up-to-date `.api` dumps in temporary build folders, and compare them to the `.api` files you have in your repository. If they don't match, it will fail, and report the differences detected.

For example, see this error the task generates after moving a property from a data class' constructor to its body:

```
Execution failed for task ':stream-chat-android-client:apiCheck'.
> API check failed for project stream-chat-android-client.
   @@ -218,9 +218,8 @@
   
   public final class io/getstream/chat/android/client/api/models/AutocompleteFilterObject : io/getstream/chat/android/client/api/models/FilterObject {
   	public final fun component1 ()Ljava/lang/String;
  -	public final fun component2 ()Ljava/lang/String;
  -	public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lio/getstream/chat/android/client/api/models/AutocompleteFilterObject;
  +	public final fun copy (Ljava/lang/String;)Lio/getstream/chat/android/client/api/models/AutocompleteFilterObject;
   	public fun equals (Ljava/lang/Object;)Z
   	public final fun getFieldName ()Ljava/lang/String;
   	public final fun getValue ()Ljava/lang/String;
  
   You can run :stream-chat-android-client:apiDump task to overwrite API declarations

```

If those differences are intentional, you should run the `apiDump` task again, which updates the stored `.api` files, now including any changes in the API you've created by changing the source code. Running the `apiCheck` task at this point will complete successfully, as the sources and the `.api` files are in sync again.

*This is the goal of the plugin: making developers explicitly run a task to update the API description files when they touch public API.*

You should then commit the changes you've introduced to the `.api` files. These will show up in commits and on pull requests, allowing reviewers to easily tell when you've made changes to the binary API, and catch any unintentional changes.

### Git hooks and CI checks

The power of the plugin really shows if you have **automated checks** in place to make sure that whenever you change your public API, you've actually updated the `.api` files to match.

At Stream, we run the checks in two ways:

- Using a pre-commit [git hook](https://github.com/GetStream/stream-chat-android/blob/fd990322367c97eafc165995de1f065a9c89d9ec/hooks/pre-commit#L16-L25) in our repository that will run the `apiCheck` task (and automatically run `apiDump` for you if it fails) when you try to create a new commit in the repository.
- With [a GitHub Action step](https://github.com/GetStream/stream-chat-android/blob/fd990322367c97eafc165995de1f065a9c89d9ec/.github/workflows/pr-checks.yml#L19-L30) in our PR checks workflow that makes sure that the `api` files are up-to-date before we merge changes.

## Limitations

This plugin is a *binary* validator. This means that it doesn't track public API changes that don't affect binary compatibility. [For example, reified functions are part of source-level API but are not present in the binary API](https://github.com/Kotlin/binary-compatibility-validator/issues/35). Thankfully, changes in these should be obvious from the source file changes anyway (and they're not very frequent).

There are also some known issues in the library, [as you can see on GitHub](https://github.com/Kotlin/binary-compatibility-validator/issues). We've reported some of these ourselves, mostly for problems in respecting non-public markers, which we use extensively in our project (see [#36](https://github.com/Kotlin/binary-compatibility-validator/issues/36) and [#58](https://github.com/Kotlin/binary-compatibility-validator/issues/58)).

## Conclusion

Even with those limitations in mind, the binary compatibility validator plugin is a great tool for making sure that you don't make any accidental changes in your public binary API when building a library. Check it out [on GitHub](https://github.com/Kotlin/binary-compatibility-validator).

You'll also find our [Android Chat SDK on GitHub](https://github.com/GetStream/stream-chat-android), and you can try our [In-App Messaging Tutorial](https://getstream.io/tutorials/android-chat/) to get started with it.

More library development topics you might be interested in:

- Our guide to [Publishing Android libraries to MavenCentral](https://getstream.io/blog/publishing-libraries-to-mavencentral-2021/)
- [Maintaining Compatibility in Kotlin Libraries
](https://zsmb.co/maintaining-compatibility-in-kotlin-libraries/)
- [Mastering API Visibility in Kotlin](https://zsmb.co/mastering-api-visibility-in-kotlin/)