Skip to Content

Koin - Dependency Injection đơn giản

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)

// ❌ 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 đề:

  1. Khó test: Muốn test UserViewModel, phải có database thật
  2. Khó thay thế: Muốn đổi database khác → phải sửa code
  3. 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)

“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 DI class 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:

  1. Dễ test: Truyền fake database khi test
  2. Dễ thay thế: Đổi database → chỉ cần truyền object khác
  3. Loose coupling: Các class không biết chi tiết của nhau

Koin là gì?

Koin là một framework DI đơn giản cho Kotlin. So với Hilt/Dagger:

KoinHilt/Dagger
Độ khóDễ họcKhó hơn
SetupÍt codeNhiều annotation
Compile timeNo code generationGenerate code
Runtime errorsCó thểPhát hiện lúc compile
Phù hợpSmall-medium appsLarge 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

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

Click Sync Now hoặc File > Sync Project with Gradle Files


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

// data/repository/GreetingRepository.kt // 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

// presentation/GreetingViewModel.kt import androidx.lifecycle.ViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import 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

Module là nơi bạn “dạy” Koin cách tạo các object.

3.1. Tạo file module

// di/AppModule.kt import org.koin.androidx.viewmodel.dsl.viewModel import 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 chung
  • factory { }: Tạo mới mỗi lần inject
  • viewModel { }: Dành riêng cho ViewModel
  • get(): “Lấy cho tôi dependency này” - Koin tự tìm và inject

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

4.1. Tạo Application class

// MyApplication.kt import android.app.Application import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidLogger import 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

<!-- AndroidManifest.xml --> <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

5.1. Inject ViewModel

// presentation/GreetingScreen.kt import androidx.compose.runtime.* import org.koin.androidx.compose.koinViewModel @Composable fun 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

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

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.kt

6.2. Định nghĩa Module đầy đủ

// di/AppModule.kt import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module import retrofit2.Retrofit import 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ả modules val allModules = listOf( networkModule, repositoryModule, viewModelModule )

6.3. Load nhiều modules

// MyApplication.kt startKoin { androidLogger() androidContext(this@MyApplication) modules(allModules) // Load tất cả modules }

Bước 7: Inject với Parameters

Đôi khi cần truyền parameter khi inject:

// Module val viewModelModule = module { // ViewModel cần userId viewModel { (userId: Int) -> UserDetailViewModel(userId, get()) } } // Sử dụng @Composable fun UserDetailScreen(userId: Int) { val viewModel: UserDetailViewModel = koinViewModel { parametersOf(userId) } // ... }

Bước 8: Testing với Koin

8.1. Tạo Fake Repository

class FakeGreetingRepository : GreetingRepository { override fun getGreeting(name: String): String { return "Test greeting for $name" } }

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

Các bước setup Koin:

  1. Thêm dependencies vào build.gradle
  2. Tạo classes với constructor injection
  3. Định nghĩa module - dạy Koin cách tạo objects
  4. Khởi tạo Koin trong Application class
  5. Inject bằng koinViewModel() hoặc get()

Bảng tóm tắt DSL:

DSLÝ nghĩaKhi nào dùng
single { }SingletonRepository, API client
factory { }Tạo mới mỗi lầnObject có state ngắn hạn
viewModel { }ViewModelTất cả ViewModels
get()Lấy dependencyTrong module definition
koinViewModel()Inject ViewModelTrong Composable

So sánh với Hilt:

KoinHilt
single { }@Singleton @Provides
viewModel { }@HiltViewModel
koinViewModel()hiltViewModel()
startKoin { }@HiltAndroidApp

Quy tắc vàng:

  1. Không bao giờ tự new dependency trong class
  2. Luôn nhận dependency qua constructor
  3. Định nghĩa cách tạo trong module
  4. Để Koin lo việc tạo và inject
Last updated on