Explicit API mode for Kotlin/Android
Kotlin 1.4 introduced an exciting new feature called Explicit API mode. In this new mode, libraries need to provide explicit visibility modifiers and explicit return types. Library authors can then be free from future regret in case they accidentally expose APIs to their dependents.
While applying the explicit API mode in a real-world code base, I find it extremely useful to identify the unintended exposed APIs in the library. Meanwhile, almost as expected for every new features, I discovered a few issues along with the adoption in our Android code base.
Note: The Kotlin version used in this story is 1.4.10
According to the documentation, with the explicit mode enabled, the compiler would have the additional requirements to compile the code:
Visibility modifiers are required: Even if we intended to leave out the visibility modifier so that it became public, we still need to add public so that we do not unintentionally expose the API.
Explicit type specifications are required for properties and functions. This can improve readiness as well as maintaining backward compatibility so that the type cannot be implicitly changed.
Beyond the visibility modifier and return types, KEEP also mentioned that explicit propagation of opt-in annotation and KDoc on public APIs are verified. However, these are NOT implemented as of Kotlin 1.4.10.
The gradle script change to enable this is simple. However, since the codebase I am working on is Android libraries, we need to tweak it to work with Android Gradle Plugin:
android.kotlinOptions.freeCompilerArgs = [“-Xexplicit-api=strict”]
./gradlew build would is almost guaranteed to fail if we apply the explicit mode for the first time. The error messages are straightforward:
e: AppLaunchMonitor.kt: (72, 5): Visibility must be specified in explicit API mode
e: AppLaunchMonitor.kt: (72, 16): Return type must be specified in explicit API mode
e: AppLaunchMonitor.kt: (530, 1): Visibility must be specified in explicit API mode// AppLaunchMonitor.ktval useActivityLifecycleCallbacks = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q@VisibleForTesting
const val DEFAULT_WARM_TYPE_THRESHOLD_MILLIS: Int = 5000
Two examples are exemplified here and both are good catches:
At line 72, we have a boolean property to perform an SDK version check. We are missing both
public as visibility modifier and
Boolean as the return type. This is a good catch and we should make them explicit!
At line 530, we have unintentionally exposed a public API for the constant. If the clients are not ignoring the
VisibleForTesting annotation, this should be okay. Since we can’t guarantee that, let’s fix it!
It would be relaxing that the workflow above is what all we need. But as always, we find new problems and new solutions to them.
public visibility modifier fades out in Android Studio, because it tells us that they are redundant. To prettify the look of our code, we can completely turn off the default-on inspection
Kotlin | Redundant constructs | Redundant visibility modifier , or lower its severity enough so that it is no longer different from other code in coloring.
android.kotlinOptions.freeCompilerArgs = [“-Xexplicit-api=strict”] , the compiler argument is added to both the main source set 👍as well as the test source sets👎. This is extremely inconvenient because the test code is not assembled into the AAR, hence not exposed as public API. We need explicit API mode OFF for compilation, otherwise, we need to refactor all our test code!
To turn on explicit API mode explicitly for the main code, I have written a standalone Gradle plugin like the following, so that all the compilation tasks for test code are excluded:
Conflicting Detekt rules
Since we have Detekt configured as our style checking tool, there are two rules to call out here:
RedundantVisibilityModifierRule: This rule is indeed at odds with explicit API mode. The recommendation would be to inactivate this rule in your Detekt config. Fortunately, this issue is already reported and addressed by skipping the rule check with explicit API mode is on.
LibraryCodeMustSpecifyReturnType: This rule intends to check the explicit return type of public APIs. However, I have found the definition of public APIs is not necessarily to same as the effectively public API defined in Kotlin compiler. I have reported this issue and the fix will be addressed in the next release. For the time being, if we have enabled Kotlin Explicit API mode, it is not necessary to keep this rule active.
I listed a few problems and provided their solutions when you start to adopt explicit API mode in Kotlin. This is an exceedingly useful and recommended feature that would make library authors more aware of their exposed APIs. And library authors can be free from being yelled at for backward incompatibility.
The verbosity of Java is manifested by the necessity to add
public and return type all the time. This is in fact a pretty good characteristic that makes the contract between the library and its dependents explicit. Kotlin initially changed their default behavior to simplify the code but at the cost of obscure the contract. Luckily, this issue is addressed in Kotlin lately to be on par with Java.