Dependency Injection (DI)using Dagger2 using MVVM Kotlin Android
What is Dependency Injection?
Dependency Injection is based on concept called “Inversion of Control”. This concept means a class should get its dependencies from external class rather than instantiating them in the class
Dependency Injection in Android using Dagger2:
Dagger2 is a fully static ,compile-time dependency injection framework based on the Java Specification Request (JSR) 330 used for both Android and Java. It uses code generation and is based on annotations. The generated code is very relatively easy to read and debug.The earlier version was created by Square and now its maintained by Google.
3 Major parts of Dagger2:
(1)Dependency Provider
(2) Dependency Consumer
(3) Component
(1)Dependency Provider:
Is the one who provide the objects that are called dependencies . The class that is responsible for providing dependencies is annotated as @Module and the methods that provides dependency(objects) in this class to be annotated as @Provides.
(2)Dependency Consumer:Dependency consumer is a class where we need to instantiate the objects. But we don’t need to instantiate it with the new keyword . do not even need to get it as an argument. But dagger will provide the dependency, and for this, we just need to annotate the object declaration with @Inject.
(3)Component: Acts as a interface between dependency consumer and dependency provider annotated with @Component and it is an interface.
Retrofit using Dagger2:
This program fetches list of data from Network using Retrofit .
Used Dagger2 + MVVM + DataBinding + RecyclerView + Fragment + Activity:
Dependencies:
Dagger2 Dependencies:
ext.dagger2_version = '2.24'
// Basic Dagger 2 (required)
implementation "com.google.dagger:dagger:$dagger2_version"
kapt "com.google.dagger:dagger-compiler:$dagger2_version"
// dagger.android package (optional)
implementation "com.google.dagger:dagger-android:$dagger2_version"
kapt "com.google.dagger:dagger-android-processor:$dagger2_version"
// Support library support (optional)
kapt "com.google.dagger:dagger-android-support:$dagger2_version"
Retrofit Dependencies:
implementation 'com.squareup.retrofit2:retrofit:2.6.1'
implementation 'com.squareup.retrofit2:retrofit-converters:2.6.1'
implementation 'com.squareup.retrofit2:retrofit-adapters:2.6.1'
implementation 'com.squareup.retrofit2:converter-gson:2.6.1'
All the dependencies used in this program:
dependencies {
ext.dagger2_version = '2.24'
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.0.0-beta01'
implementation 'androidx.constraintlayout:constraintlayout:1.1.2'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.1.0-alpha4'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0-alpha4'
implementation 'com.squareup.retrofit2:retrofit:2.6.1'
implementation 'com.squareup.retrofit2:retrofit-converters:2.6.1'
implementation 'com.squareup.retrofit2:retrofit-adapters:2.6.1'
implementation 'com.squareup.retrofit2:converter-gson:2.6.1'
// Basic Dagger 2 (required)
implementation "com.google.dagger:dagger:$dagger2_version"
kapt "com.google.dagger:dagger-compiler:$dagger2_version"
// dagger.android package (optional)
implementation "com.google.dagger:dagger-android:$dagger2_version"
kapt "com.google.dagger:dagger-android-processor:$dagger2_version"
// Support library support (optional)
kapt "com.google.dagger:dagger-android-support:$dagger2_version"
def lifecycle_version = "2.1.0"
// ViewModel and LiveData
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.0.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'org.mockito:mockito-core:3.0.0'
implementation 'org.mockito:mockito-inline:3.0.0'
testImplementation 'android.arch.core:core-testing:1.1.1'
}
Dependency Provider:
@Module
class APIModule constructor(baseURL:String) {
var baseURL:String?=""
init {
this.baseURL = baseURL
}
@Singleton
@Provides
fun provideOKHttpClient():OkHttpClient{
return OkHttpClient.Builder()
.readTimeout(1200,TimeUnit.SECONDS)
.connectTimeout(1200,TimeUnit.SECONDS)
.build()
}
@Singleton
@Provides
fun provideGSON(): GsonConverterFactory {
return GsonConverterFactory.create()
}
@Singleton
@Provides
fun provideRetrofit(gsonConverterFactory: GsonConverterFactory,okHttpClient: OkHttpClient):Retrofit{
return Retrofit.Builder()
.baseUrl(baseURL)
.addConverterFactory(gsonConverterFactory)
.client(okHttpClient)
.build()
}
@Provides
fun provideRetroRepository():RetrofitRepository{
return RetrofitRepository()
}
}
Component:
@Singleton
@Component(modules = [APIModule::class])
interface APIComponent {
fun inject(retrofitRepository: RetrofitRepository)
fun inject(retroViewModel: RetroViewModel)
fun inject(retroFragment: RetroFragment)
fun inject(retroViewModelFactory:RetroViewModelFactory)
}
DaggerComponent Initialization:
Initiate DaggerComponent globally to be accessible across application in Application class :
fun initDaggerComponent():APIComponent{
apiComponent = DaggerAPIComponent
.builder()
.aPIModule(APIModule(APIURL.BASE_URL))
.build()
return apiComponent
}
Application Class:
Create a subclass of Application class to initiate DaggerComponent so that it can accessible globally across:
class MyRetroApplication : Application() {
companion object {
var ctx: Context? = null
lateinit var apiComponent:APIComponent
}
override fun onCreate() {
super.onCreate()
ctx = applicationContext
apiComponent = initDaggerComponent()
}
fun getMyComponent(): APIComponent {
return apiComponent
}
fun initDaggerComponent():APIComponent{
apiComponent = DaggerAPIComponent
.builder()
.aPIModule(APIModule(APIURL.BASE_URL))
.build()
return apiComponent
}
}
Dependency Consumer:
RetroRepository is the class that consumes dependency using @Inject annotation:
@Inject
lateinit var retrofit: Retrofit
RetroRepository:
class RetrofitRepository {
lateinit var apiComponent: APIComponent
var postInfoMutableList: MutableLiveData<List<PostInfo>> = MutableLiveData()
@Inject
lateinit var retrofit: Retrofit
init {
var apiComponent :APIComponent = MyRetroApplication.apiComponent
apiComponent.inject(this)
}
fun fetchPostInfoList(): LiveData<List<PostInfo>> {
var apiService:APIService = retrofit.create(APIService::class.java)
var postListInfo : Call<List<PostInfo>> = apiService.makeRequest()
postListInfo.enqueue(object :Callback<List<PostInfo>>{
override fun onFailure(call: Call<List<PostInfo>>, t: Throwable) {
Log.d("RetroRepository","Failed:::"+t.message)
}
override fun onResponse(call: Call<List<PostInfo>>, response: Response<List<PostInfo>>) {
var postInfoList = response.body()
postInfoMutableList.value = postInfoList
}
})
return postInfoMutableList
}
ViewModelFactory:
Use view model factory class to pass arguments to ViewModel. In this program RetroRepository is injected in ViewModelProvider and sent as argument to ViewModel
class RetroViewModelFactory : ViewModelProvider.Factory {
lateinit var apiComponent: APIComponent
@Inject
lateinit var retrofitRepository: RetrofitRepository
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
var apiComponent :APIComponent = MyRetroApplication.apiComponent
apiComponent.inject(this)
if (modelClass.isAssignableFrom(RetroViewModel::class.java)) {
return RetroViewModel(retrofitRepository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
RetroViewModel:
RetrofitRepository is passed as argument no instantiation using new
class RetroViewModel(retrofitRepository: RetrofitRepository): ViewModel() {
lateinit var retrofitRepository:RetrofitRepository
var postInfoLiveData: LiveData<List<PostInfo>> = MutableLiveData()
init {
this.retrofitRepository = retrofitRepository
fetchPostInfoFromRepository()
}
fun fetchPostInfoFromRepository(){
postInfoLiveData = retrofitRepository.fetchPostInfoList()
}
}
Classes that contain URL and service call interface:
class APIURL {
companion object {
const val BASE_URL = "https://jsonplaceholder.typicode.com/"
}
}
Service Interface:
interface APIService {
@GET("posts")
fun makeRequest(): Call<List<PostInfo>>
}
Adapter class to map data to View:
class PostListAdapter(var context: Context) : RecyclerView.Adapter<PostListAdapter.ViewHolder>() {
private var list: List<PostInfo> = emptyList<PostInfo>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding: PostListItemBinding = DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.post_list_item, parent, false)
return PostListAdapter.ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
Log.d("ADapter","Info:::"+list.get(position).body)
holder.bind(list.get(position))
}
fun setAdapterList(list: List<PostInfo> ){
this.list = list
notifyDataSetChanged()
}
override fun getItemCount(): Int = list.size
class ViewHolder(val binding: com.example.myapplication.databinding.PostListItemBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(data: Any) {
binding.setVariable(BR.postmodel, data) //BR - generated class; BR.user - 'user' is variable name declared in layout
binding.executePendingBindings()
}
}
}
Fragment to map data to layout using Adapter :
class RetroFragment: Fragment() {
lateinit var retroViewModel: RetroViewModel
var fragmentView:View?=null
private var listAdapter:PostListAdapter?=null
private var postListLayoutBinding:PostListLayoutBinding?=null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initViewModel()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
postListLayoutBinding = DataBindingUtil.inflate(inflater,R.layout.post_list_layout,container,false)
fragmentView = postListLayoutBinding?.root
initAdapter()
setAdapter()
fetchRetroInfo()
return fragmentView
}
fun initViewModel(){
var retroViewModelFactory = RetroViewModelFactory()
retroViewModel = ViewModelProviders.of(this,retroViewModelFactory).get(RetroViewModel::class.java)
}
fun fetchRetroInfo(){
retroViewModel.postInfoLiveData?.observe(this,object:Observer<List<PostInfo>>{
override fun onChanged(t: List<PostInfo>?) {
t?.apply {
listAdapter?.setAdapterList(t)
}
}
})
}
private fun setAdapter(){
fragmentView?.post_list?.apply {
layoutManager = LinearLayoutManager(activity)
addItemDecoration(DividerItemDecoration(activity, DividerItemDecoration.VERTICAL))
adapter = listAdapter
}
}
private fun initAdapter(){
listAdapter = PostListAdapter(this@RetroFragment.requireActivity())
}
}
Git Repository: