I created a sample project to use Dagger/Hilt with dynamic feature and ViewModel and everything was working great with Dagger/Hilt 2.29.1-alpha
and Hilt Jetpack 1.0.0-alpha02
you can check the source code here. Now a new version of Dagger/Hilt is here which is Dagger/Hilt 2.32-alpha Hilt Jetpack 1.0.0-alpha03
and you can check the source code here. In the app
module I created the following class to be a generic view model factory
package com.ibrahim.currencyconverter.di
import android.annotation.SuppressLint
import android.os.Bundle
import androidx.hilt.lifecycle.ViewModelAssistedFactory
import androidx.lifecycle.AbstractSavedStateViewModelFactory
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.SavedStateViewModelFactory
import androidx.lifecycle.ViewModel
import androidx.savedstate.SavedStateRegistryOwner
import javax.inject.Provider
class DFMSavedStateViewModelFactory(
owner: SavedStateRegistryOwner,
defaultArgs: Bundle?,
private val delegateFactory: SavedStateViewModelFactory,
private val viewModelFactories: @JvmSuppressWildcards Map<String, Provider<ViewModelAssistedFactory<out ViewModel>>>
) : AbstractSavedStateViewModelFactory(owner, defaultArgs) {
@SuppressLint("RestrictedApi")
override fun <T : ViewModel?> create(
key: String,
modelClass: Class<T>,
handle: SavedStateHandle
): T {
val factoryProvider = viewModelFactories[modelClass.name]
?: return delegateFactory.create("$KEY_PREFIX:$key", modelClass)
@Suppress("UNCHECKED_CAST")
return factoryProvider.get().create(handle) as T
}
companion object {
private const val KEY_PREFIX = "androidx.hilt.lifecycle.HiltViewModelFactory"
}
}
FragmentViewModelModule
class in the app
module to provide the view model factory
package com.ibrahim.currencyconverter.di
import android.app.Application
import androidx.fragment.app.Fragment
import androidx.hilt.lifecycle.ViewModelAssistedFactory
import androidx.lifecycle.SavedStateViewModelFactory
import androidx.lifecycle.ViewModel
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.FragmentComponent
import javax.inject.Provider
@Module
@InstallIn(FragmentComponent::class)
object FragmentViewModelModule {
@Provides
fun provideSavedStateViewModelFactory(
application: Application,
fragment: Fragment,
viewModelFactories: @JvmSuppressWildcards Map<String, Provider<ViewModelAssistedFactory<out ViewModel>>>,
): DFMSavedStateViewModelFactory {
val defaultArgs = fragment.arguments
val delegate = SavedStateViewModelFactory(application, fragment, defaultArgs)
return DFMSavedStateViewModelFactory(fragment, defaultArgs, delegate, viewModelFactories)
}
}
AppDependencies
class in the app
model to expose required dependencies by the dynamic feature module which is the home
dynamic feature module
package com.ibrahim.currencyconverter.di
import android.app.Application
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import retrofit2.Retrofit
@EntryPoint
@InstallIn(SingletonComponent::class)
interface AppDependencies {
fun exposeApplication(): Application
fun exposeRetrofit(retrofit: Retrofit): Retrofit
}
Then in the home
dynamic feature module, I created HomeComponent
class to inject the HomeFragment
package com.ibrahim.home
import androidx.fragment.app.Fragment
import com.ibrahim.currencyconverter.di.AppDependencies
import dagger.BindsInstance
import dagger.Component
import kotlinx.coroutines.ExperimentalCoroutinesApi
import javax.inject.Singleton
@Singleton
@Component(dependencies = [AppDependencies::class], modules = [HomeModule::class])
interface HomeComponent {
@ExperimentalCoroutinesApi
fun inject(fragment: HomeFragment)
fun fragment(): Fragment
@Component.Builder
interface Builder {
fun fragment(@BindsInstance fragment: Fragment): Builder
fun appDependencies(appDependencies: AppDependencies): Builder
fun build(): HomeComponent
}
}
HomeModule
class to provide the home
module dependencies
package com.ibrahim.home
import com.ibrahim.currencyconverter.di.AppModule
import com.ibrahim.currencyconverter.di.FragmentViewModelModule
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.FragmentComponent
import kotlinx.coroutines.ExperimentalCoroutinesApi
import retrofit2.Retrofit
@Module(
includes = [
FragmentViewModelModule::class,
AppModule::class
]
)
@InstallIn(FragmentComponent::class)
object HomeModule {
@Provides
fun provideHomeRemoteDataSource(retrofit: Retrofit): IHomeRemoteDataSource {
return retrofit.create(IHomeRemoteDataSource::class.java)
}
@ExperimentalCoroutinesApi
@Provides
fun provideHomeRepository(repository: HomeRepository): IHomeRepository {
return repository
}
}
HomeViewModel
class
package com.ibrahim.home
import androidx.lifecycle.SavedStateHandle
import com.ibrahim.core.CoreViewModel
import com.ibrahim.core.None
import com.ibrahim.core.exhaustive
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collect
import timber.log.Timber
@ExperimentalCoroutinesApi
class HomeViewModel @AssistedInject constructor(
private val latestExchangeRateUseCase: LatestExchangeRateUseCase,
@Assisted private val savedStateHandle: SavedStateHandle
) :
CoreViewModel<HomeIntent, HomeResult, HomeState>(HomeState.Idle) {
override suspend fun handleIntent(intent: HomeIntent) {
when (intent) {
is HomeIntent.GetLatestExchangeRate -> {
latestExchangeRateUseCase.execute(None()).collect {
updateState(it)
}
}
}.exhaustive
}
override fun reduce(result: HomeResult): HomeState {
return when (result) {
is HomeResult.Loading -> {
HomeState.Loading
}
is HomeResult.ExchangeRateSuccess -> {
HomeState.ExchangeRateSuccess(result.exchangeRates)
}
is HomeResult.ExchangeRateFailure -> {
HomeState.ExchangeRateFailure(result.failure)
}
}.exhaustive
}
override fun onCleared() {
Timber.i("HomeViewModel: onCleared()")
super.onCleared()
}
}
And finally the HomeFragment
class
package com.ibrahim.home
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.DividerItemDecoration
import com.ibrahim.core.Failure
import com.ibrahim.core.exhaustive
import com.ibrahim.currencyconverter.di.AppDependencies
import com.ibrahim.currencyconverter.di.DFMSavedStateViewModelFactory
import com.ibrahim.currencyconverter.exchangerate.ExchangeRateData
import com.ibrahim.home.databinding.FragmentHomeBinding
import dagger.hilt.android.EntryPointAccessors
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import timber.log.Timber
import java.net.SocketTimeoutException
import javax.inject.Inject
@ExperimentalCoroutinesApi
class HomeFragment : Fragment() {
private var _binding: FragmentHomeBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
@Inject
lateinit var savedStateViewModelFactory: DFMSavedStateViewModelFactory
private val viewModel by viewModels<HomeViewModel> { savedStateViewModelFactory }
private val adapter: ExchangeRatesAdapter by lazy {
ExchangeRatesAdapter {
val base =
if (viewModel.state.value is HomeState.ExchangeRateSuccess) {
(viewModel.state.value as HomeState.ExchangeRateSuccess).exchangeRates.base
} else {
""
}
findNavController().navigate(
HomeFragmentDirections.actionHomeFragmentToExchangeRateFragment(
ExchangeRateData(
base = base,
target = it.name,
rate = it.rate
)
)
)
}
}
override fun onAttach(context: Context) {
super.onAttach(context)
DaggerHomeComponent.builder()
.fragment(this)
.appDependencies(
EntryPointAccessors.fromApplication(
requireContext().applicationContext,
AppDependencies::class.java
)
)
.build()
.inject(this)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// Inflate the layout for this fragment
_binding = FragmentHomeBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.rvRates.addItemDecoration(
DividerItemDecoration(
requireContext(),
DividerItemDecoration.VERTICAL
)
)
binding.rvRates.setHasFixedSize(true)
binding.rvRates.adapter = adapter
lifecycleScope.launchWhenStarted {
viewModel.state.collect { state ->
render(state)
}
}
}
private fun render(state: HomeState) {
when (state) {
is HomeState.Idle -> {
binding.progressBar.visibility = View.VISIBLE
}
is HomeState.Loading -> {
binding.progressBar.visibility = View.VISIBLE
}
is HomeState.ExchangeRateSuccess -> {
onLatestExchangeRateSuccess(state.exchangeRates)
binding.progressBar.visibility = View.GONE
}
is HomeState.ExchangeRateFailure -> {
onLatestExchangeRateFailure(state.failure)
binding.progressBar.visibility = View.GONE
}
}.exhaustive
}
private fun onLatestExchangeRateSuccess(exchangeRates: ExchangeRates) {
Timber.i("latestExchangeRate: ${exchangeRates.exchangeRates.size}")
requireActivity().title = exchangeRates.base
if (adapter.items.isNullOrEmpty()) {
adapter.items.addAll(exchangeRates.exchangeRates)
adapter.notifyDataSetChanged()
}
}
private fun onLatestExchangeRateFailure(failure: Failure) {
Timber.i("latestExchangeRateFailure: $failure")
var errorMessage: String = getString(R.string.something_went_wrong_please_try_again_later)
when (failure) {
is Failure.NetworkConnection -> {
errorMessage =
if (failure.throwable is SocketTimeoutException)
getString(R.string.looks_like_the_server_is_taking_too_long_to_respond_please_try_again_later)
else
getString(R.string.no_internet_connection)
}
is Failure.ServerError -> {
errorMessage = getString(R.string.no_internet_connection)
}
is Failure.FeatureFailure -> {
}
}
Toast.makeText(
requireContext(),
errorMessage,
Toast.LENGTH_SHORT
).show()
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
if (viewModel.state.value !is HomeState.ExchangeRateSuccess) {
lifecycleScope.launch {
viewModel dispatch HomeIntent.GetLatestExchangeRate
}
}
}
override fun onDestroyView() {
super.onDestroyView()
Timber.i("onDestroyView")
}
override fun onDetach() {
super.onDetach()
Timber.i("onDetach")
}
override fun onDestroy() {
super.onDestroy()
Timber.i("onDestroy")
}
}
I'm unable to build the project due to the following error and I don't know what is wrong and how I can fix it. Also, I have checked the hilt-multi-module documentation
/home/ibrahim/Android/Projects/CurrencyConverter/home/build/tmp/kapt3/stubs/debug/com/ibrahim/home/HomeComponent.java:8: error: [Dagger/MissingBinding] java.util.Map<java.lang.String,javax.inject.Provider<androidx.hilt.lifecycle.ViewModelAssistedFactory<? extends androidx.lifecycle.ViewModel>>> cannot be provided without an @Provides-annotated method.
public abstract interface HomeComponent {
^
java.util.Map<java.lang.String,javax.inject.Provider<androidx.hilt.lifecycle.ViewModelAssistedFactory<? extends androidx.lifecycle.ViewModel>>> is injected at
com.ibrahim.currencyconverter.di.FragmentViewModelModule.provideSavedStateViewModelFactory(…, viewModelFactories)
com.ibrahim.currencyconverter.di.DFMSavedStateViewModelFactory is injected at
com.ibrahim.home.HomeFragment.savedStateViewModelFactory
com.ibrahim.home.HomeFragment is injected at
com.ibrahim.home.HomeComponent.inject(com.ibrahim.home.HomeFragment)warning: The following options were not recognized by any processor: '[dagger.hilt.android.internal.disableAndroidSuperclassValidation, kapt.kotlin.generated]'