How Bilibili Scaled Kotlin Multiplatform Across Android, iOS, and HarmonyOS
This article details Bilibili's practical experience with Kotlin Multiplatform (KMP), covering the choice of Bazel as a build system, multi‑language interop, dependency injection, modular export, state‑machine driven single‑direction data flow, and the successful deployment of shared logic and UI across Android, iOS, and HarmonyOS platforms.
Project Structure Overview
This is the third article in the KMP technical series, summarizing Bilibili's experience using KMP's Share Logic and Share UI across three platforms and the complementary infrastructure engineering.
Build System
After comparing Gradle and Bazel, the team selected Bazel as the foundation for the KMP project, sacrificing some native capabilities but gaining freedom to combine and tune compiler options. Two custom functions were implemented to leverage Bazel's toolchain registration and Aspect mechanisms.
Multi‑Language Interop
Early on, seamless calls between Kotlin, Swift, Objective‑C, and C/C++ were identified as crucial. Using Bazel's registration and Kotlin's c‑interop, the team created bindings that allow Kotlin code to directly use symbols from ObjC and Swift modules without extra configuration.
<code>kt_library(
name = "kt_module",
deps = [
"c_module",
{
"ios" = [":objc_module", ":swift_module"]
},
]
)
objc_library(
name = "objc_module",
srcs = ["oc_module.oc"],
hdrs = ["oc_module.h"]
)
swift_library(
name = "swift_module",
srcs = ["swift_module.swift"]
)
cc_library(
name = "c_module",
srcs = ["module.c"],
hdrs = ["module.h"]
)</code>With this setup, Kotlin code can use symbols from ObjC and Swift modules directly, enabling easy addition of platform‑specific APIs, and the approach has been extended to support Zig, Go, Rust, and other languages.
Dependency Injection and Modular Export
In Bilibili's architecture, KMP modules are low‑level dependencies injected into each platform's private‑message module. Dependency injection frameworks simplify usage of existing platform interfaces. Images illustrate the injection flow on Android and iOS.
To avoid large monolithic frameworks, the team implemented a custom modular export using Bazel Aspects, extracting Kotlin metadata and compiling each Swift module as an independent clang module, then linking them together into a single iOS framework.
From Engineering to Business
After infrastructure setup, the team delivered shared logic (Share Logic) across Android, iOS, and HarmonyOS, and shared Compose UI (Share UI) across Android and iOS. The private‑message conversation list page was chosen as the pilot because it is logic‑heavy, display‑focused, and fits KMP's strengths.
Technical Choices
The architecture emphasizes three pillars: single‑direction data flow, dependency injection, and functional programming. A state‑machine driven single‑direction flow is implemented with FlowRedux, providing precise intent scoping, automatic coroutine cancellation, and easy nesting.
<code>spec {
inState<ContentState> {
on<LoadMore> { action, state ->
val nextPage = load()
// Append page data
state.mutate { /* ... */ }
}
on<Refresh> { action, state ->
state.override { Refreshing() }
}
}
inState<Refreshing> {
onEnter { state ->
val newPage = refresh()
state.override { ContentState(newPage) }
}
}
}</code>Core business logic is exposed via
val state: Flow<SessionState>and
suspend fun dispatch(action: SessionAction), keeping UI interaction surface minimal.
Functional Module Implementation
Modules are written as pure functions with no mutable state. Interfaces describe functionality, and implementations are provided via dependency injection, enabling easy testing and parallel development.
<code>interface AService {
suspend fun doSth(param: Param): Result<String>
}
interface BService {
fun getDataFlow(): Flow<Data>
}
class BServiceImpl @Inject constructor(private val aService: AService) : BService {
override fun getDataFlow(): Flow<Data> = flow {
aService.doSth(Param()).fold { emit(Data(it)) }
}
}</code>Integration with Platform Code
All platforms use a monorepo. Bazel builds the KMP modules and integrates them with Android via generated Gradle files, with HarmonyOS using binary .so integration, and iOS importing the generated Swift modules directly. The shell module aggregates exported Kotlin modules, but after modular export the shell is no longer needed.
<code>// Shell module
kt_module(
name = "KMP_Shell",
srcs = [...],
deps = ["ModuleA", "ModuleB"]
)
// Module A
kt_module(name = "ModuleA", srcs = [...])
// Module B
kt_module(name = "ModuleB", srcs = [...])</code>iOS imports the needed modules with
import ModuleA, and Android includes the generated
build.gradle.ktsas a regular Gradle project.
Coroutine‑First Strategy
NativeCoroutines is used to expose Kotlin
Flowand
suspendfunctions to Swift. A custom
@NativeCoroutineScopeannotation marks coroutine scopes, and Swift wrappers cancel the scope in
deinit. On HarmonyOS,
Flowis converted to an Observable and managed via a subscription manager.
Keeping Symbols Internal
To reduce iOS binary size, symbols are kept internal whenever possible; public symbols are annotated with
@HiddenFromObjcor exposed via NativeCoroutines only when needed.
Conclusion
The successful rollout of KMP in Bilibili's private‑message module demonstrates that the engineering foundation can support complex, real‑world business scenarios. Future articles will explore further Compose Multiplatform deployments and cross‑language dependency injection techniques.
Bilibili Tech
Provides introductions and tutorials on Bilibili-related technologies.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.