Building Android SDKs with Kotlin: API Design, Publishing & Versioning
An Android SDK is a reusable software component that other applications integrate into their projects.
Unlike application code, an SDK becomes a public contract.
That means API design, compatibility, documentation, packaging, and versioning are critical.
SDK Architecture
A well-designed SDK can look like:
Host Application
|
v
Public SDK API
|
v
Internal Services
/ | \
Network Storage Native APIs
Only the required surface should be public.
Public API Design
Prefer small, stable interfaces.
For example:
interface VModalSdk {
fun initialize(config: SdkConfig)
fun start()
fun stop()
}
Configuration:
data class SdkConfig(
val apiKey: String,
val environment: Environment
)
Keep implementation classes internal:
internal class SdkEngine {
// Internal implementation.
}
Kotlin's internal visibility helps prevent consumers from depending on implementation details.
Initialization
Avoid APIs that require many constructor parameters.
Prefer a configuration object:
data class SdkConfig(
val apiKey: String,
val environment: Environment,
val enableLogging: Boolean = false
)
This makes future additions easier.
Callbacks vs Flow
For simple events:
interface SdkListener {
fun onConnected()
fun onDisconnected()
fun onError(error: SdkError)
}
For modern Kotlin applications, Flow can be useful:
val connectionState: StateFlow<ConnectionState>
Choose based on the consumers your SDK must support.
Avoid Leaking Dependencies
If your SDK internally uses a particular networking or JSON library, avoid forcing consumers to use those implementation types in your public API.
For example, avoid exposing:
fun getResponse(): RetrofitResponse
Prefer:
fun getUser(): SdkUser
This reduces dependency coupling.
Threading Contract
Document which thread callbacks occur on.
A good SDK should define:
Initialization -> background
Network work -> background
Callbacks -> main thread
or expose a clear coroutine/Flow contract.
Do not leave threading behavior ambiguous.
Error Design
Create stable SDK-specific errors:
sealed class SdkError {
data object Unauthorized : SdkError()
data object Network : SdkError()
data object InvalidConfiguration : SdkError()
data class Unknown(val message: String) : SdkError()
}
Consumers can then handle errors without parsing strings.
Android Lifecycle
SDKs must be careful about:
- Activity references
- Context leaks
- background work
- configuration changes
- process death
Prefer application context when an Activity context is not required.
Avoid storing Activity references in long-lived objects.
API Compatibility
Once an API is published, consumers may depend on it for years.
Avoid casually changing:
fun start(config: Config)
to:
fun start(newConfig: NewConfig)
Prefer additive changes or introduce a new API.
Semantic Versioning
A common versioning scheme is:
MAJOR.MINOR.PATCH
For example:
2.4.1
Meaning:
2 = major
4 = minor
1 = patch
Use major releases for breaking public API changes.
Minor releases can add backward-compatible features.
Patch releases should normally contain backward-compatible fixes.
Publishing
An Android SDK can be distributed through:
- Maven Central
- GitHub Packages
- a private Maven repository
- an organization's artifact repository
A typical consumer dependency looks like:
dependencies {
implementation("com.example:my-sdk:2.4.1")
}
The exact publishing configuration depends on the repository.
Gradle Publishing
A library module normally uses:
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("maven-publish")
}
Publishing configuration should produce the required Maven metadata and artifacts.
Keep repository credentials outside source control.
Release Checklist
Before publishing:
[ ] Public API reviewed
[ ] Documentation updated
[ ] CHANGELOG updated
[ ] Tests passing
[ ] Sample application tested
[ ] ProGuard/R8 rules verified
[ ] Version incremented
[ ] Artifact generated
[ ] Publication tested
ProGuard / R8
If your SDK uses reflection, serialization, JNI, or libraries that require keep rules, test release builds carefully.
Provide consumer rules when necessary:
consumer-rules.pro
Do not disable shrinking globally just because one SDK component has a configuration issue.
Documentation
Every public SDK should explain:
- installation
- initialization
- permissions
- lifecycle
- threading
- error handling
- supported Android versions
- configuration
- migration notes
- sample code
A sample application is often more useful than a large API reference alone.
Backward Compatibility
Maintain a clear policy.
For example:
2.x
-> backward-compatible feature releases
3.x
-> breaking API changes
Deprecate before removing public APIs where practical:
@Deprecated(
message = "Use start(config) instead."
)
fun startLegacy() {
}
Separate Public and Internal Packages
A useful structure:
com.example.sdk
public API
com.example.sdk.internal
implementation
Consumers should depend only on the documented public package.
Conclusion
Building an Android SDK is fundamentally different from building an Android application.
The application can change quickly because you control the client. An SDK must assume that unknown developers will depend on its public API.
Therefore, design the public API carefully, hide implementation details, document threading and lifecycle behavior, publish reproducible artifacts, and use disciplined versioning.
Useful Links
SDK Flutter: https://github.com/v-modal/vmodal_sdk_flutter
SDK Android: https://github.com/v-modal/vmodal_sdk_android
Discord: https://discord.gg/K72z28KUx
Top comments (0)