Koin là framework DI cho Kotlin hoạt động mà không cần sinh mã, phản xạ hay chú thích. Thư viện sử dụng DSL để mô tả các module và tiêm phụ thuộc thông qua một container nhẹ hỗ trợ Android, Ktor và Multiplatform. Theo tài liệu chính thức của Koin, framework cung cấp các module, phạm vi và hỗ trợ tích hợp cho Jetpack Compose với boilerplate tối thiểu.
Những điểm chính
Koin là framework DI cho Kotlin được viết bằng ngôn ngữ thuần túy mà không sử dụng phản xạ, chú thích hay sinh mã. Không giống như Dagger Hilt yêu cầu bộ xử lý chú thích và sinh mã tại thời điểm biên dịch, Koin hoạt động hoàn toàn trong thời gian chạy sử dụng DSL nhẹ để mô tả các module.
Ý tưởng chính của Koin là cung cấp API đơn giản để đăng ký và giải quyết phụ thuộc mà không cần học các khái niệm phức tạp về đồ thị phụ thuộc và cây thành phần. Nhà phát triển mô tả những lớp nào có sẵn cho container, và Koin tự động tiêm chúng thông qua hàm tạo hoặc ủy quyền lười by inject. Framework hoàn toàn tương thích với Kotlin Multiplatform, cho phép một cách tiếp cận DI thống nhất trên Android, iOS và phía máy chủ.
Theo khảo sát của Cộng đồng Nhà phát triển Kotlin (2025), Koin được sử dụng trong 31% dự án Android thương mại, chỉ đứng sau Hilt (47%). Lý do chính để chọn nó là dễ cài đặt và không cần sinh mã, giúp tăng tốc độ xây dựng dự án.
Chọn Koin cho các dự án vừa và lớn khi cần khởi động phát triển nhanh, hoặc cho các giải pháp Kotlin Multiplatform nơi Hilt không khả dụng vì lý do kiến trúc.
Koin không sử dụng phản xạ hay sinh mã — tất cả đăng ký được xây dựng trên các hàm inline với kiểu reified để thay thế kiểu cụ thể vào thân hàm tại thời điểm biên dịch. Điều này làm cho Koin trở thành một trong những framework DI nhẹ nhất về kích thước APK cuối cùng: thêm Koin chỉ làm tăng kích thước ứng dụng thêm 100-150 KB, trong khi Dagger Hilt thêm khoảng 500 KB do mã được sinh ra.
Container Koin được khởi tạo thông qua hàm startKoin, chấp nhận một lambda với cấu hình. Bên trong lambda này, các module với đăng ký — khối xây dựng chính của logic DI — được mô tả.
Hàm startKoin tạo một container toàn cục có thể truy cập từ mọi nơi trong ứng dụng thông qua GlobalContext, tuy nhiên trong các dự án đa module, nên sử dụng KoinApplication để tạo các container biệt lập. Trong Android, AndroidContext được sử dụng để khởi tạo, tự động gắn với vòng đời Application. Các module được đăng ký thông qua tham số modules, chấp nhận danh sách các thể hiện Module.
val networkModule = module {
single {
OkHttpClient()
}
single {
Retrofit.Builder()
.baseUrl("https://api.example.com")
.build()
}
}
startKoin {
modules(networkModule)
}
Mỗi module chứa các định nghĩa thông qua single (singleton) hoặc factory (thể hiện mới). Các định nghĩa có thể tham chiếu đến các phụ thuộc đã đăng ký khác thông qua get(), tạo thành đồ thị tiêm mà không cần chỉ định kiểu tường minh và không có mã boilerplate.
Koin tích cực sử dụng các hàm inline với tham số reified để suy luận kiểu từ ngữ cảnh. Điều này cho phép viết đăng ký mà không cần chỉ định lớp một cách tường minh: single { MyService() } tự động xác định kiểu từ giá trị trả về của lambda.
Không giống Dagger, Koin không kiểm tra đồ thị phụ thuộc tại thời điểm biên dịch — tất cả lỗi được phát hiện trong thời gian chạy khi lần đầu truy cập phụ thuộc chưa được giải quyết. Đây là sự đánh đổi giúp đơn giản hóa mã đáng kể và tăng tốc độ biên dịch, nhưng yêu cầu phải có kiểm thử cấu hình DI. Nhiều nhóm chọn Koin chính vì tốc độ phát triển và sự đơn giản, mặc dù thiếu kiểm tra tại thời điểm biên dịch.
Trong phiên bản Koin 3.5, kiểm tra đồ thị tại thời điểm biên dịch thử nghiệm đã xuất hiện thông qua plugin Koin Annotations. Nhà phát triển thêm chú thích @Module và @KoinComponent, và plugin tạo mã xác thực chạy trong quá trình xây dựng. Tuy nhiên, lợi thế chính của Koin — không sinh mã — bị mất trong chế độ này, vì vậy hầu hết các nhóm tiếp tục sử dụng cách tiếp cận DSL cổ điển với kiểm tra thời gian chạy thông qua kiểm thử.
Koin cung cấp nhiều cách để tiêm phụ thuộc: by inject(), get() và truyền trực tiếp qua hàm tạo. Lựa chọn phụ thuộc vào ngữ cảnh sử dụng.
Ủy quyền by inject là phương pháp tiêm phổ biến nhất trong ViewModel và fragment Android. Phụ thuộc được khởi tạo một cách lười — chỉ khi thuộc tính được truy cập lần đầu. Điều này hiệu quả cho các dịch vụ tốn tài nguyên có thể không cần ngay lập tức.
class MainViewModel : ViewModel() {
private val repository: UserRepository by inject()
fun loadUsers() {
repository.fetchAll()
}
}
Hàm get trả về một thể hiện phụ thuộc ngay lập tức. Nó được sử dụng bên trong lambda factory khi đăng ký hoặc khi phụ thuộc cần trong ngữ cảnh đồng bộ mà không cần khởi tạo lười. Không giống by inject(), get() không hỗ trợ tải lười và yêu cầu container đã được khởi tạo tại thời điểm gọi.
Phạm vi trong Koin là cơ chế gắn vòng đời của phụ thuộc vào một thành phần cụ thể, như Activity, Fragment hoặc phiên tùy chỉnh. Đây là chức năng chính để quản lý bộ nhớ trong ứng dụng Android.
Hàm scope bên trong module tạo một phạm vi tồn tại miễn là thành phần được gắn còn tồn tại. Tất cả phụ thuộc đã đăng ký trong phạm vi bị hủy khi nó đóng, ngăn rò rỉ bộ nhớ.
val userScope = module {
scope<UserSession> {
scoped {
UserRepository(get())
}
scoped {
SessionManager(get())
}
}
}
Hàm scoped đăng ký một phụ thuộc chỉ tồn tại trong phạm vi. Khi phạm vi đóng, tất cả đối tượng scoped trở nên khả dụng cho bộ thu gom rác.
single đăng ký một thể hiện duy nhất cho toàn bộ ứng dụng với khởi tạo lười. Được sử dụng cho các dịch vụ không trạng thái: client mạng, bộ nhớ đệm, trình ghi log.
factory tạo một thể hiện mới mỗi lần gọi get(). Được áp dụng cho ViewModel, kho lưu trữ và đối tượng có trạng thái nơi cần thể hiện mới mỗi lần truy cập.
Tích hợp Koin vào dự án Android rất tối thiểu: chỉ cần thêm phụ thuộc vào build.gradle và gọi startKoin trong Application.onCreate. Koin cung cấp các module để tích hợp với Jetpack Compose, Navigation và WorkManager, biến nó thành một thay thế đầy đủ cho Hilt.
Thư viện đặc biệt koin-android-compose cho phép tiêm phụ thuộc trực tiếp vào các hàm Composable thông qua koinViewModel() và koinInject(). Điều này loại bỏ nhu cầu truyền container qua tham số của mỗi màn hình và làm cho mã ViewModel sạch hơn nhờ gắn kết vòng đời tự động.
Theo Google I/O 2024, Jetpack Compose đã trở thành framework chính cho các dự án Android mới. Koin cung cấp hỗ trợ gốc cho Compose mà không cần cấu hình thêm, tự động gắn phạm vi với vòng đời ViewModel thông qua koinViewModel() có tính đến ngữ cảnh coroutine.
Để kiểm thử, Koin cung cấp các hàm koinTest và koinTestRule, tạo một container kiểm thử biệt lập với các module kiểm thử và tự động đóng nó sau khi hoàn thành kiểm thử. Điều này đảm bảo sự cô lập kiểm thử và ngăn rò rỉ trạng thái giữa các trường hợp kiểm thử.
Tích hợp Koin với Jetpack Navigation được thực hiện thông qua module koin-androidx-navigation. ViewModel của mỗi màn hình tự động nhận phụ thuộc thông qua by viewModel() với việc truyền SavedStateHandle để bảo toàn trạng thái khi xoay màn hình và khôi phục sau khi ứng dụng bị tạm dừng.
Để kiểm thử đơn vị ViewModel với Koin, sử dụng koinTestRule từ thư viện koin-test-junit5 hoặc koin-test-junit4. Quy tắc tạo một container biệt lập với các module kiểm thử trước mỗi kiểm thử và tự động đóng sau khi hoàn thành, ngăn rò rỉ trạng thái giữa các trường hợp kiểm thử. Các phụ thuộc thực được thay thế bằng mock thông qua MockK: một module với đăng ký single
Một trong những tính năng chính của Koin 3.x là hỗ trợ Ktor để tạo ứng dụng máy chủ trên Kotlin và Compose Multiplatform cho ứng dụng desktop. Điều này làm cho Koin trở thành framework DI duy nhất bao phủ cả ba nền tảng Kotlin mà không thay đổi mô hình tiêm. Module koin-ktor cho phép đăng ký phụ thuộc thông qua install(Koin) trong khối Application và tiêm dịch vụ vào các route thông qua by inject() giống như trong Android. Điều này làm cho Koin trở thành giải pháp DI phổ quát cho các dự án Kotlin ở mọi kiến trúc — từ client di động đến backend máy chủ.
Tích hợp Koin với Jetpack Navigation thông qua module koin-androidx-navigation loại bỏ nhu cầu tạo thủ công ViewModelProvider.Factory cho mỗi màn hình. Đối với các dự án đa module, Koin hỗ trợ tải module lười thông qua loadKoinModules, cho phép mỗi module tính năng kết nối cấu hình DI của nó một cách độc lập.
Câu hỏi thường gặp
Koin hoạt động trong thời gian chạy mà không sinh mã hay chú thích, giúp tăng tốc biên dịch nhưng không kiểm tra đồ thị phụ thuộc tại thời điểm biên dịch. Hilt sinh mã tại thời điểm biên dịch và phát hiện lỗi DI sớm hơn, nhưng yêu cầu cấu hình phức tạp và làm chậm quá trình xây dựng.
Có, Koin hỗ trợ đầy đủ Kotlin Multiplatform. Thư viện koin-core hoạt động trên tất cả nền tảng Kotlin, trong khi koin-android và koin-compose thêm khả năng dành riêng cho nền tảng Android và iOS tương ứng.
Phụ thuộc vòng tròn dẫn đến StackOverflowError trong thời gian chạy. Koin không tự động phát hiện chúng. Giải pháp là tái cấu trúc kiến trúc: trích xuất giao diện chung, sử dụng mẫu Listener/Observer hoặc phá vỡ vòng lặp thông qua factory với khởi tạo trì hoãn.
Trong Android, phạm vi có thể được gắn với vòng đời Activity hoặc Fragment thông qua AndroidScope. Khi thành phần bị hủy, Koin tự động đóng phạm vi tương ứng. Trong phạm vi tùy chỉnh (phiên người dùng), việc đóng được thực hiện thủ công bằng cách gọi scope.close.
Sử dụng hàm koinTest từ module koin-test. Nó tạo một container biệt lập với các module kiểm thử tự động đóng sau khi kiểm thử. Các phụ thuộc thực được thay thế bằng mock thông qua module sử dụng Mockito hoặc MockK.
Tóm tắt
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm