Dagger / Hilt 2.32-alpha Hilt Jetpack 1.0.0-alpha03 + Dynamic Feature + ViewModel + SavedStateHandle и dagger.assisted.AssistedInject

Я создал образец проекта для использования Dagger / Hilt с динамической функцией и ViewModel, и все отлично работает с Dagger/Hilt 2.29.1-alpha и Hilt Jetpack 1.0.0-alpha02, вы можете проверить исходный код здесь. Теперь доступна новая версия Dagger / Hilt, Dagger/Hilt 2.32-alpha Hilt Jetpack 1.0.0-alpha03, и вы можете проверить исходный код здесь < / а>. В модуле app я создал следующий класс, который будет фабрикой модели общего представления.

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 в модуле app для обеспечения фабрики модели представления

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 в модели app для выявления необходимых зависимостей с помощью модуля динамических функций, который является home модулем динамических функций

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
}

Затем в модуле динамических функций home я создал класс HomeComponent для внедрения 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 класс для предоставления home зависимостей модуля

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 класс

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

}

И, наконец, класс HomeFragment

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

}

Я не могу создать проект из-за следующей ошибки, и я не знаю, что не так и как это исправить. Кроме того, я проверил документацию по hilt-multi-module

/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]'

person Ibrahim Disouki    schedule 10.02.2021    source источник


Ответы (1)


Определите зависимость, которую вы хотите иметь в модели просмотра

@AssistedFactory
interface HomeViewModelFactory {
    fun create(handle: SavedStateHandle): HomeViewModel
}

class HomeViewModel @AssistedInject constructor(
    private val latestExchangeRateUseCase: LatestExchangeRateUseCase,
    @Assisted private val savedStateHandle: SavedStateHandle
) : CoreViewModel<HomeIntent, HomeResult, HomeState>(HomeState.Idle) {

    companion object {
        fun provideFactory(
            assistedFactory: HomeViewModelFactory,
            owner: SavedStateRegistryOwner,
            defaultArgs: Bundle? = null,
        ): AbstractSavedStateViewModelFactory =
            object : AbstractSavedStateViewModelFactory(owner, defaultArgs) {
                override fun <T : ViewModel?> create(
                    key: String,
                    modelClass: Class<T>,
                    handle: SavedStateHandle
                ): T {
                    return assistedFactory.create(handle) as T
                }

            }
    }
}

и предоставить зависимости

class HomeFragment : Fragment() {
    @Inject
    lateinit var homeViewModelFactory: HomeViewModelFactory

    private val viewModel: HomeViewModel by viewModels {
        HomeViewModel.provideFactory(homeViewModelFactory, this)
    }
}
person Saeed Lotfi    schedule 10.02.2021
comment
Но я хочу создать общий ViewModelFactory, чтобы не создавать по одному для каждого ViewModel. Не могли бы вы применить свое решение к общим классам? - person Ibrahim Disouki; 11.02.2021
comment
github.com/google/dagger/issues/2287 @IbrahimDisouki - person Saeed Lotfi; 14.02.2021