Bỏ qua để đến nội dung

Koin - DI đơn giản

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

  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

“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à 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

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")
}

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


Tạo một ví dụ đơn giản: App hiển thị lời chào cho user.

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


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

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

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

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)
}
}
}
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!


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.

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”
app/
├── di/
│ └── AppModule.kt
├── data/
│ ├── api/
│ │ └── ApiService.kt
│ ├── repository/
│ │ └── UserRepository.kt
│ └── model/
│ └── User.kt
├── presentation/
│ ├── UserViewModel.kt
│ └── UserScreen.kt
└── MyApplication.kt
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
)
MyApplication.kt
startKoin {
androidLogger()
androidContext(this@MyApplication)
modules(allModules) // Load tất cả modules
}

Đô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) }
// ...
}

class FakeGreetingRepository : GreetingRepository {
override fun getGreeting(name: String): String {
return "Test greeting for $name"
}
}
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)
}
}

  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()
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
Koin Hilt
single { } @Singleton @Provides
viewModel { } @HiltViewModel
koinViewModel() hiltViewModel()
startKoin { } @HiltAndroidApp
  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