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 đề:
- 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)
“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:
- 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ì?
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
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 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?
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.kt6.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:
- 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:
| 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:
| Koin | Hilt |
|---|---|
single { } | @Singleton @Provides |
viewModel { } | @HiltViewModel |
koinViewModel() | hiltViewModel() |
startKoin { } | @HiltAndroidApp |
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
Last updated on