Sáu tháng cùng Kotlin Multiplatform: Nhìn lại quá trình triển khai thực tế
Sáu tháng trước, đội ngũ của chúng tôi đã gặp bế tắc trong việc bảo trì. Chúng tôi phải quản lý ba bộ mã nguồn độc lập cho một ứng dụng fintech: Android (Kotlin), iOS (Swift) và một dashboard Web (TypeScript). Bất cứ khi nào logic tính toán lãi suất thay đổi, chúng tôi phải cập nhật ở ba repository riêng biệt.
Điều này tất yếu dẫn đến ba nhóm bug khác nhau và làm tăng gấp ba khối lượng công việc của QA. Để giải quyết vấn đề này, chúng tôi đã chuyển đổi logic nghiệp vụ cốt lõi sang Kotlin Multiplatform (KMP). Sau nửa năm vận hành thực tế, tốc độ phát triển của chúng tôi đã tăng khoảng 40%.
Khác với Flutter hay React Native vốn cố gắng kiểm soát toàn bộ UI stack, KMP tập trung vào việc chia sẻ những phần thực sự quan trọng: logic. Bạn vẫn giữ UI native—SwiftUI cho iOS và Jetpack Compose cho Android—nhưng bạn chỉ cần viết data model, networking và logic kiểm tra (validation) một lần duy nhất. Nếu bạn muốn mở rộng sản phẩm di động mà không phải tăng gấp đôi nhân sự kỹ thuật hàng năm, việc làm chủ KMP không còn là một lựa chọn mà là sự bắt buộc.
Bắt đầu nhanh: Khởi tạo dự án đầu tiên trong 5 phút
Việc thiết lập KMP từng là một quy trình thủ công tẻ nhạt, nhưng hệ sinh thái này đã trưởng thành. Trình hướng dẫn web (web wizard) của JetBrains hiện đã xử lý hầu hết các cấu hình phức tạp.
1. Kiểm tra môi trường
Bạn sẽ cần Android Studio và Xcode. Trước khi viết code, hãy cài đặt kdoctor. Công cụ này sẽ xác định các dependency còn thiếu như CocoaPods hoặc các phiên bản Java cụ thể thường gây lỗi build:
brew install kdoctor
kdoctor
Nếu kết quả hiển thị tất cả các dấu tích xanh, máy tính của bạn đã sẵn sàng để phát triển multiplatform.
2. Tạo dự án
Truy cập kmp.jetbrains.com. Chọn Android, iOS và Web (Wasm). Sau khi tải về và mở dự án trong Android Studio, bạn sẽ thấy ba thư mục chính:
composeApp/: Mã nguồn UI dùng chung (nếu bạn chọn sử dụng Compose Multiplatform).iosApp/: Một dự án Xcode tiêu chuẩn sử dụng logic dùng chung của bạn.shared/: “Phòng máy” nơi chứa logic nghiệp vụ đa nền tảng của bạn.
3. Chạy ứng dụng
Chọn cấu hình “composeApp” để chạy trên trình giả lập Android. Đối với iOS, bạn có thể khởi chạy ứng dụng trực tiếp từ Android Studio bằng plugin KMP. Ngoài ra, hãy mở iosApp/iosApp.xcworkspace trong Xcode để debug trên iPhone thật.
Cơ chế “Expect/Actual”: Cầu nối giữa các nền tảng
KMP không ép buộc bạn vào một môi trường hạn chế kiểu “mẫu số chung thấp nhất”. Nếu bạn cần truy cập các API đặc thù của nền tảng, chẳng hạn như hệ thống tệp hoặc UUID của thiết bị, bạn sử dụng mô hình expect/actual. Đây chính là bí quyết tạo nên sự linh hoạt của KMP.
Định nghĩa kỳ vọng (Expectation)
Trong thư mục commonMain, bạn định nghĩa interface mà bạn cần:
// shared/src/commonMain/kotlin
expect fun getPlatformName(): String
Triển khai thực tế (Reality)
Trình biên dịch sau đó yêu cầu bạn cung cấp các triển khai trong các thư mục đặc thù của nền tảng. Nó sẽ tự động chèn mã chính xác vào thời điểm biên dịch.
// androidMain
actual fun getPlatformName(): String = "Android ${android.os.Build.VERSION.SDK_INT}"
// iosMain
import platform.UIKit.UIDevice
actual fun getPlatformName(): String = UIDevice.currentDevice.systemName()
Networking và Serialization
Đối với các ứng dụng thực tế, tôi khuyên dùng Ktor cho các yêu cầu HTTP và kotlinx.serialization cho JSON. Sự kết hợp này cho phép bạn viết một API client duy nhất hoạt động ở mọi nơi. Bằng cách định nghĩa repository trong commonMain, các ứng dụng Android, iOS và Web của bạn sẽ chia sẻ cùng một logic mạng và data model. Chỉ riêng điều này đã giúp giảm 30% lỗi liên quan đến logic của chúng tôi trong quý đầu tiên.
Sử dụng nâng cao: Quản lý State và Web (Wasm)
Chia sẻ networking là điều tuyệt vời, nhưng chia sẻ logic “ViewModel” mới là nơi bạn thấy được hiệu quả thực sự. Bằng cách sử dụng Kotlin Coroutines và Flow, bạn có thể tạo ra một StateHolder tập trung.
Chia sẻ State với Flow
Chúng tôi cấu trúc logic dùng chung theo mô hình reactive UI State. Điều này đảm bảo UI luôn là một sự phản chiếu “thụ động” của state dùng chung.
class SharedViewModel {
private val _state = MutableStateFlow(UiState())
val state: StateFlow<UiState> = _state.asStateFlow()
fun loadData() {
// Logic Coroutine được viết tại đây
}
}
Android sử dụng mã này thông qua Jetpack Compose một cách dễ dàng. Trên iOS, bạn có thể thu thập (collect) các Flow này trong một ObservableObject của SwiftUI. Đối với Web, Kotlin/Wasm cho phép bạn biên dịch chính mã nguồn này thành WebAssembly. Điều này mang lại hiệu suất gần như native trong trình duyệt, loại bỏ sự chậm trễ của các cầu nối JavaScript truyền thống.
Kinh nghiệm thực chiến từ thực tế
Việc di chuyển một ứng dụng quy mô lớn đã dạy chúng tôi rằng KMP cần một cách tiếp cận chiến lược. Nó không phải là chiếc đũa thần, và bạn phải tránh việc thiết kế quá phức tạp (over-engineering) cho các module dùng chung.
1. Đừng trừu tượng hóa UI quá mức
Rất dễ bị cám dỗ trong việc chia sẻ từng nút bấm hay hiệu ứng chuyển động. Tuy nhiên, chúng tôi nhận thấy rằng các hiệu ứng UI phức tạp thường dễ viết hơn bằng code native. Hãy chia sẻ Logic, Dữ liệu và State, nhưng hãy để các framework UI đặc thù của nền tảng xử lý các điểm ảnh ở những nơi cần thiết.
2. Khắc phục khả năng tương thích Swift với SKIE
Việc debug mã Kotlin trên trình giả lập iPhone có thể gây nản lòng vì Swift không hiểu một cách tự nhiên các Sealed Class hoặc Flow của Kotlin. Hãy sử dụng plugin Touchlab SKIE. Nó tạo ra mã thân thiện với Swift, giúp các module Kotlin dùng chung có cảm giác như các thư viện Swift native. Nếu không có nó, các nhà phát triển iOS của bạn có thể sẽ gặp khó khăn với các kiểu generic cồng kềnh.
3. CI/CD và chi phí Build
Hệ thống build (pipeline) của bạn giờ đây sẽ yêu cầu một máy chạy macOS để biên dịch các mục tiêu iOS. Hãy lưu ý rằng các máy chạy macOS trên GitHub Actions đắt hơn đáng kể so với máy chạy Linux. Chúng tôi đã tối ưu hóa điều này bằng cách chỉ chạy kiểm tra build iOS trên các pull request có thay đổi trong thư mục shared hoặc iosApp.
4. Tránh cái bẫy Expect/Actual
Chỉ tự viết mã expect/actual như là giải pháp cuối cùng. Hệ sinh thái KMP rất rộng lớn. Nếu đã có một thư viện như SQLDelight cho cơ sở dữ liệu cục bộ, hãy sử dụng nó. Càng ít mã đặc thù nền tảng mà bạn phải bảo trì, dự án của bạn sẽ càng dễ nâng cấp trong tương lai.
Quá trình làm quen ban đầu với KMP mất khoảng một tuần. Tuy nhiên, lợi ích lâu dài là rất lớn. Việc chứng kiến một lỗi duy nhất được sửa và cập nhật đồng thời lên Android, iOS và Web là một bước ngoặt lớn cho bất kỳ đội ngũ kỹ thuật nào.

