Koin - DI đơn giản
Dependency Injection là gì?
Phần tiêu đề “Dependency Injection là gì?”Trước khi học Koin, hãy hiểu vấn đề mà nó giải quyết.
Vấn đề: Code phụ thuộc chặt (Tight Coupling)
Phần tiêu đề “Vấn đề: Code phụ thuộc chặt (Tight Coupling)”// ❌ Cách viết có vấn đềclass UserRepository { // UserRepository TỰ TẠO Database private val database = AppDatabase.getInstance()
fun getUsers() = database.userDao().getAll()}
class UserViewModel { // UserViewModel TỰ TẠO Repository private val repository = UserRepository()
fun loadUsers() = repository.getUsers()}Vấn đề:
- Khó test: Muốn test UserViewModel, phải có database thật
- Khó thay thế: Muốn đổi database khác → phải sửa code
- Khó tái sử dụng: Repository bị “gắn chặt” với một loại database
Giải pháp: Dependency Injection (DI)
Phần tiêu đề “Giải pháp: Dependency Injection (DI)”“Injection” = “Tiêm vào”. Thay vì tự tạo dependency, ta nhận nó từ bên ngoài:
// ✅ Cách viết tốt với DIclass UserRepository( private val database: AppDatabase // NHẬN từ bên ngoài) { fun getUsers() = database.userDao().getAll()}
class UserViewModel( private val repository: UserRepository // NHẬN từ bên ngoài) { fun loadUsers() = repository.getUsers()}Lợi ích:
- Dễ test: Truyền fake database khi test
- Dễ thay thế: Đổi database → chỉ cần truyền object khác
- Loose coupling: Các class không biết chi tiết của nhau
Koin là gì?
Phần tiêu đề “Koin là gì?”Koin là một framework DI đơn giản cho Kotlin. So với Hilt/Dagger:
| Koin | Hilt/Dagger | |
|---|---|---|
| Độ khó | Dễ học | Khó hơn |
| Setup | Ít code | Nhiều annotation |
| Compile time | No code generation | Generate code |
| Runtime errors | Có thể | Phát hiện lúc compile |
| Phù hợp | Small-medium apps | Large apps |
Khi nào dùng Koin?
- Dự án nhỏ-trung bình
- Muốn setup nhanh
- Team chưa quen DI
Bước 1: Thêm Koin vào dự án
Phần tiêu đề “Bước 1: Thêm Koin vào dự án”1.1. Mở build.gradle.kts (Module: app)
Phần tiêu đề “1.1. Mở build.gradle.kts (Module: app)”dependencies { // Koin core implementation("io.insert-koin:koin-android:3.5.3")
// Koin cho Jetpack Compose implementation("io.insert-koin:koin-androidx-compose:3.5.3")}1.2. Sync Gradle
Phần tiêu đề “1.2. Sync Gradle”Click Sync Now hoặc File > Sync Project with Gradle Files
Bước 2: Tạo các class cần inject
Phần tiêu đề “Bước 2: Tạo các class cần inject”Tạo một ví dụ đơn giản: App hiển thị lời chào cho user.
2.1. Tạo Repository
Phần tiêu đề “2.1. Tạo Repository”// Interface - định nghĩa "hợp đồng"interface GreetingRepository { fun getGreeting(name: String): String}
// Implementation - triển khai thực tếclass GreetingRepositoryImpl : GreetingRepository { override fun getGreeting(name: String): String { return "Xin chào, $name! Chào mừng đến với Koin." }}Tại sao tạo Interface?
- Khi test, có thể tạo FakeGreetingRepository
- Dễ thay đổi implementation sau này
2.2. Tạo ViewModel
Phần tiêu đề “2.2. Tạo ViewModel”import androidx.lifecycle.ViewModelimport kotlinx.coroutines.flow.MutableStateFlowimport kotlinx.coroutines.flow.StateFlowimport kotlinx.coroutines.flow.asStateFlow
class GreetingViewModel( private val repository: GreetingRepository // Inject qua constructor) : ViewModel() {
private val _greeting = MutableStateFlow("") val greeting: StateFlow<String> = _greeting.asStateFlow()
fun greet(name: String) { _greeting.value = repository.getGreeting(name) }}Lưu ý: ViewModel nhận GreetingRepository qua constructor, không tự tạo.
Bước 3: Định nghĩa Koin Module
Phần tiêu đề “Bước 3: Định nghĩa Koin Module”Module là nơi bạn “dạy” Koin cách tạo các object.
3.1. Tạo file module
Phần tiêu đề “3.1. Tạo file module”import org.koin.androidx.viewmodel.dsl.viewModelimport org.koin.dsl.module
val appModule = module {
// Cách 1: single - tạo MỘT instance duy nhất (Singleton) single<GreetingRepository> { GreetingRepositoryImpl() }
// Cách 2: factory - tạo instance MỚI mỗi lần inject // factory<GreetingRepository> { GreetingRepositoryImpl() }
// Cách 3: viewModel - dành cho ViewModel viewModel { GreetingViewModel(get()) } // ↑ // get() = lấy GreetingRepository từ Koin}Giải thích:
single { }: Singleton - chỉ tạo 1 lần, dùng chungfactory { }: Tạo mới mỗi lần injectviewModel { }: Dành riêng cho ViewModelget(): “Lấy cho tôi dependency này” - Koin tự tìm và inject
3.2. Tại sao single cho Repository?
Phần tiêu đề “3.2. Tại sao single cho Repository?”Repository thường là Singleton vì:
- Không cần tạo nhiều instance
- Tiết kiệm bộ nhớ
- Database connection nên dùng chung
Bước 4: Khởi tạo Koin trong Application
Phần tiêu đề “Bước 4: Khởi tạo Koin trong Application”4.1. Tạo Application class
Phần tiêu đề “4.1. Tạo Application class”import android.app.Applicationimport org.koin.android.ext.koin.androidContextimport org.koin.android.ext.koin.androidLoggerimport org.koin.core.context.startKoin
class MyApplication : Application() {
override fun onCreate() { super.onCreate()
// Khởi tạo Koin startKoin { // Log để debug (optional) androidLogger()
// Cung cấp Android Context androidContext(this@MyApplication)
// Load các modules modules(appModule) } }}4.2. Đăng ký Application trong Manifest
Phần tiêu đề “4.2. Đăng ký Application trong Manifest”<application android:name=".MyApplication" <!-- Thêm dòng này --> android:icon="@mipmap/ic_launcher" android:label="@string/app_name" ...>
<activity ...> ... </activity></application>Quan trọng: Nếu quên đăng ký, Koin sẽ không được khởi tạo!
Bước 5: Sử dụng trong Compose
Phần tiêu đề “Bước 5: Sử dụng trong Compose”5.1. Inject ViewModel
Phần tiêu đề “5.1. Inject ViewModel”import androidx.compose.runtime.*import org.koin.androidx.compose.koinViewModel
@Composablefun GreetingScreen( viewModel: GreetingViewModel = koinViewModel() // Koin inject ViewModel) { val greeting by viewModel.greeting.collectAsState() var name by remember { mutableStateOf("") }
Column( modifier = Modifier .fillMaxSize() .padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { // Input tên OutlinedTextField( value = name, onValueChange = { name = it }, label = { Text("Nhập tên của bạn") }, modifier = Modifier.fillMaxWidth() )
Spacer(modifier = Modifier.height(16.dp))
// Button chào Button( onClick = { viewModel.greet(name) }, modifier = Modifier.fillMaxWidth() ) { Text("Chào!") }
Spacer(modifier = Modifier.height(24.dp))
// Hiển thị lời chào if (greeting.isNotEmpty()) { Card( modifier = Modifier.fillMaxWidth(), colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.primaryContainer ) ) { Text( text = greeting, modifier = Modifier.padding(16.dp), style = MaterialTheme.typography.bodyLarge ) } } }}Điểm quan trọng: koinViewModel() thay vì viewModel() của Compose.
5.2. MainActivity
Phần tiêu đề “5.2. MainActivity”class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MaterialTheme { Surface { GreetingScreen() } } } }}Bước 6: Ví dụ phức tạp hơn - App có API
Phần tiêu đề “Bước 6: Ví dụ phức tạp hơn - App có API”6.1. Cấu trúc thư mục
Phần tiêu đề “6.1. Cấu trúc thư mục”app/├── di/│ └── AppModule.kt├── data/│ ├── api/│ │ └── ApiService.kt│ ├── repository/│ │ └── UserRepository.kt│ └── model/│ └── User.kt├── presentation/│ ├── UserViewModel.kt│ └── UserScreen.kt└── MyApplication.kt6.2. Định nghĩa Module đầy đủ
Phần tiêu đề “6.2. Định nghĩa Module đầy đủ”import org.koin.androidx.viewmodel.dsl.viewModelimport org.koin.dsl.moduleimport retrofit2.Retrofitimport retrofit2.converter.gson.GsonConverterFactory
val networkModule = module {
// Retrofit instance (Singleton) single { Retrofit.Builder() .baseUrl("https://api.example.com/") .addConverterFactory(GsonConverterFactory.create()) .build() }
// ApiService single { get<Retrofit>().create(ApiService::class.java) }}
val repositoryModule = module {
// Repository với ApiService được inject single<UserRepository> { UserRepositoryImpl(get()) }}
val viewModelModule = module {
// ViewModel với Repository được inject viewModel { UserViewModel(get()) }}
// Combine tất cả modulesval allModules = listOf( networkModule, repositoryModule, viewModelModule)6.3. Load nhiều modules
Phần tiêu đề “6.3. Load nhiều modules”startKoin { androidLogger() androidContext(this@MyApplication) modules(allModules) // Load tất cả modules}Bước 7: Inject với Parameters
Phần tiêu đề “Bước 7: Inject với Parameters”Đôi khi cần truyền parameter khi inject:
// Moduleval viewModelModule = module { // ViewModel cần userId viewModel { (userId: Int) -> UserDetailViewModel(userId, get()) }}
// Sử dụng@Composablefun UserDetailScreen(userId: Int) { val viewModel: UserDetailViewModel = koinViewModel { parametersOf(userId) } // ...}Bước 8: Testing với Koin
Phần tiêu đề “Bước 8: Testing với Koin”8.1. Tạo Fake Repository
Phần tiêu đề “8.1. Tạo Fake Repository”class FakeGreetingRepository : GreetingRepository { override fun getGreeting(name: String): String { return "Test greeting for $name" }}8.2. Test ViewModel
Phần tiêu đề “8.2. Test ViewModel”class GreetingViewModelTest {
@Test fun `greet should update greeting state`() { // Tạo ViewModel với fake repository val fakeRepository = FakeGreetingRepository() val viewModel = GreetingViewModel(fakeRepository)
// Gọi function viewModel.greet("Android")
// Kiểm tra kết quả assertEquals("Test greeting for Android", viewModel.greeting.value) }}📝 Tóm tắt cho người mới
Phần tiêu đề “📝 Tóm tắt cho người mới”Các bước setup Koin:
Phần tiêu đề “Các bước setup Koin:”- Thêm dependencies vào build.gradle
- Tạo classes với constructor injection
- Định nghĩa module - dạy Koin cách tạo objects
- Khởi tạo Koin trong Application class
- Inject bằng
koinViewModel()hoặcget()
Bảng tóm tắt DSL:
Phần tiêu đề “Bảng tóm tắt DSL:”| DSL | Ý nghĩa | Khi nào dùng |
|---|---|---|
single { } |
Singleton | Repository, API client |
factory { } |
Tạo mới mỗi lần | Object có state ngắn hạn |
viewModel { } |
ViewModel | Tất cả ViewModels |
get() |
Lấy dependency | Trong module definition |
koinViewModel() |
Inject ViewModel | Trong Composable |
So sánh với Hilt:
Phần tiêu đề “So sánh với Hilt:”| Koin | Hilt |
|---|---|
single { } |
@Singleton @Provides |
viewModel { } |
@HiltViewModel |
koinViewModel() |
hiltViewModel() |
startKoin { } |
@HiltAndroidApp |
Quy tắc vàng:
Phần tiêu đề “Quy tắc vàng:”- Không bao giờ tự
newdependency trong class - Luôn nhận dependency qua constructor
- Định nghĩa cách tạo trong module
- Để Koin lo việc tạo và inject