Trong bài hướng dẫn này, mình và các bạn sẽ làm quen với Koin , một trong những framework mới phổ biến nhất cho DI hết sức mạnh mẽ và hỗ trợ rất tốt trong Kotlin. Trước khi bắt đầu mình muốn các bạn tìm hiểu qua về DI trong nguyên lý SOLID. Từ đó sẽ có cái nhìn tổng quan cũng như hiểu rõ hơn về cách thức hoạt động của nó. Cuối cùng, chúng ta sẽ áp dụng nó cho một ứng dụng mẫu sử dụng Koin làm khung DI và minh họa các lợi ích của nó.
1. Dependency Injection là gì ?
Dependency injection (DI) là một kỹ thuật lập trình giúp tách một class độc lập với các biến phụ thuộc. Với lập trình hướng đối tượng, chúng ta hầu như luôn phải làm việc với rất nhiều class trong một chương trình. Các class được liên kết với nhau theo một mối quan hệ nào đó. Dependency là một loại quan hệ giữa 2 class mà trong đó một class hoạt động độc lập và class còn lại phụ thuộc bởi class kia.
Hiểu một cách đơn giản là chúng ta có 2 class là class A và class B, khi chúng ta sử dụng class A tham chiếu với class B với việc sử dụng các method của class B, lúc này sinh ra quan hệ dependency giữa hai class A và B. Để class A có thể gọi method của B thì trước tiên nó phải tạo một instance của class B. Vậy ta có thể hiểu, việc chuyển giao nhiệm vụ khởi tạo object đó cho một bên khác và trực tiếp sử dụng các dependency đó được gọi là dependency injection.
2. Koin là gì
Được ra mắt vào năm 2018 Koin là dependency injection framework mạnh mẽ dành cho Kotlin. Koin là một lightweight dependency injection framework được viết bằng Kotlin thuần túy sử dụng functional nên không proxy, không sinh code, không ánh xạ.
Vì vậy, tại sao lại sử dụng Koin thay vì một trong các khung DI khác?
Câu trả lời: Koin ngắn gọn và minh bạch hơn những thư viện khác. Lấy Dagger 2 phổ biến làm ví dụ. Để sử dụng Dagger 2 , trước tiên bạn cần làm quen với các khái niệm như module và component và các annotations như @Inject . Mặc dù hướng dẫn sử dụng Dagger được chia sẻ rất nhiều, nhưng để tận dụng tối đa nó, bạn vẫn phải học một số khái niệm nâng cao như phạm vi và các thành phần con.
Ngược lại, Koin cho phép bạn chỉ cần khai báo các module, bao gồm các phụ thuộc tiềm năng, sẽ được sử dụng trong dự án và trực tiếp đưa chúng vào lớp quan tâm.
Hiểu các thuật ngữ trong Koin
Trong khi làm việc với Koin, chúng tôi cần hiểu một số thuật ngữ trước khi bắt đầu.
module: Tạo ra một module trong Koin sẽ được Koin sử dụng để cung cấp tất cả các phụ thuộc.
single: Tạo ra một singleton có thể được sử dụng trên toàn ứng dụng như một instance.
factory: Sẽ cung cấp một đối tượng mới cho mỗi lần chúng ta yêu cầu một thể hiện.
get: Được sử dụng trong hàm tạo của một lớp để cung cấp phụ thuộc bắt buộc.
viewModel: Để khai báo một ViewModel component.
3. Demo
Để cùng hiểu rõ hơn thì mình cùng với các bạn cùng làm một dự án cơ bản.
Đây là cấu trúc dự án
Bước 1: Thêm dependencies
// Koin for Android
implementation "org.koin:koin-android:$koin_version" implementation "org.koin:koin-android-scope:$koin_version" implementation "org.koin:koin-android-viewmodel:$koin_version" implementation "org.koin:koin-android-ext:$koin_version"
// Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
// For ViewModel
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
Bước 2: Tạo Model
data classUser(val id:Long, val login:String)
Bước 3: Tạo ApiService
interface ApiService {
@GET("users")fungetUsers(): Call<List<User>>
}
Bước 4: Tạo Repository
class UserRepository(private val apiService:ApiService) {
fun getAllUsers() = apiService.getUsers()
}
Bước 5: Tạo bộ dependency injection
Mọi người nên phân tách các di thành các class riêng biệt vì nếu trong dự án thật chúng ta sẽ phải sử dụng rất nhiều các module sẽ khó kiểm soát khi các bạn viết chúng trong 1 class.
get(): Lấy các instance đã được khai báo. Ví dụ như UserRepository cần instance của ApiService thì chúng ta chỉ cần truyền vào param get(). Nếu chúng ta chưa khai báo module cung cấp ApiService thì App sẽ bị crash.
single: Tạo một singleton, nó sẽ chỉ tạo một lần trong quá trình chạy App.
val repositoryModule = module {
single {
UserRepository(get())
}
}
val apiModule = module {
fun provideApi(retrofit: Retrofit): ApiService {
return retrofit.create(ApiService::class.java)
}
single { provideApi(get()) }
}
val retrofitModule = module {
fun provideGson(): Gson {
return GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.IDENTITY)
.create()
}
fun provideHttpClient(): OkHttpClient {
val okHttpClientBuilder = OkHttpClient.Builder()
return okHttpClientBuilder.build()
}
fun provideRetrofit(factory: Gson, client: OkHttpClient):
Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory
.create(factory))
.client(client)
.build()
}
single { provideGson() }
single { provideHttpClient() }
single { provideRetrofit(get(), get()) }
}
val viewModelModule = module {
viewModel {
UserViewModel(get())
}
}
viewModel ở đây là để khai báo một ViewModel component
Bước 6: Khởi tạo và sử dụng Koin
Khi chúng ta có các modules rồi thì đến lúc tiến hành start chúng với Koin. Mở class Application, nếu chưa có thì bạn hãy tạo và đừng quên định nghĩa ở file manifest.xml. Và cuối cùng sử dụng startKoin() để khởi tạo Koin.
class MainApplication : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidLogger()
androidContext(this@MainApplication)
modules(listOf(repositoryModule,
viewModelModule,
retrofitModule,
apiModule))
}
}
}
Bước 7: Tạo ViewModel
ViewModel ở đây đang đảm nhiệm việc lấy danh sách user từ git về
class UserViewModel(private val userRepository: UserRepository) : ViewModel() {
private val _data = MutableLiveData<List<User>>()
val data: LiveData<List<User>> get() = _data
var isGetDataSuccess = MutableLiveData<Boolean>()
.apply { value = false }
fun fetchData() {
userRepository.getAllUsers().enqueue(
object : Callback<List<User>> {
override fun onFailure(call: Call<List<User>>,
t: Throwable) {
isGetDataSuccess.value = false
}
override fun onResponse(call: Call<List<User>>,
response: Response<List<User>>) {
if (response.isSuccessful) {
_data.postValue(response.body())
isGetDataSuccess.value = true
}
}
})
}
}
Bước 8: Và giờ để sử dụng các thành phần ViewModel chỉ cần inject chúng vào Activity
class MainActivity : AppCompatActivity() {
private val userViewModel: UserViewModel by viewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initView()
observeField()
}
private fun initView() {
val activityMainBinding: ActivityMainBinding =
DataBindingUtil
.setContentView(this, R.layout.activity_main)
activityMainBinding.apply {
viewModel = userViewModel
executePendingBindings()
}
}
private fun observeField() {
userViewModel.data.observe(this, Observer {
Log.d("userViewModel", "userViewModel.data$it")
})
userViewModel.isGetDataSuccess
.observe(this, Observer { isSuccess ->
if (isSuccess) {
Toast.makeText(this, "Fetch data success!",
Toast.LENGTH_LONG).show()
} else {
Toast.makeText(this, "Fetch data fail!",
Toast.LENGTH_LONG).show()
}
})
}
}
4. Kết luận
Tổng kết lại một số ưu điểm khi sử dụng Koin như sau:
Không sử dụng annotation, lợi ích rõ nhất là build phase sẽ rất là nhanh và không sinh code trong quá trình build sẽ làm giảm dụng lượng của ứng dụng.
Dễ học.
Triển khai đơn giản.
Hi vọng qua bài viết này của mình các bạn sẽ có thêm một chút thông tin hữu ích về Koin cũng cách sử dụng nó.
Comments