ViewModelProviders with Dagger 2, not able to grasp the concept

后端 未结 2 1933
渐次进展
渐次进展 2020-12-09 06:02

I have a Retrofit Service like this

public interface BrandsService {
    @GET(\"listBrand\")
    Call> getBrands();
}
相关标签:
2条回答
  • 2020-12-09 06:35

    EDIT: An important note. To use Jetpack ViewModel, you don't need map-multibinding. Read on.


    The answer can be simpler than Mumi's approach, which is that you expose the ViewModel on your component:

    @Singleton
    @Component(modules={...})
    public interface SingletonComponent {
        BrandsViewModel brandsViewModel();
    }
    

    And now you can access this method on the component inside the ViewModelFactory:

    // @Inject
    BrandsViewModel brandsViewModel;
    
    ...
    brandsViewModel = new ViewModelProvider(this, new ViewModelProvider.Factory() {
        @Override
        public <T extends ViewModel> create(Class<T> modelClazz) {
            if(modelClazz == BrandsViewModel.class) {
                return singletonComponent.brandsViewModel();
            }
            throw new IllegalArgumentException("Unexpected class: [" + modelClazz + "]");
        }).get(BrandsViewModel.class);
    

    All this can be simplified and hidden with Kotlin:

    inline fun <reified T: ViewModel> AppCompatActivity.createViewModel(crossinline factory: () -> T): T = T::class.java.let { clazz ->
        ViewModelProvider(this, object: ViewModelProvider.Factory {
            override fun <T : ViewModel?> create(modelClass: Class<T>): T {
                if(modelClass == clazz) {
                    @Suppress("UNCHECKED_CAST")
                    return factory() as T
                }
                throw IllegalArgumentException("Unexpected argument: $modelClass")
            }
        }).get(clazz)
    }
    

    which now lets you do

    brandsViewModel = createViewModel { singletonComponent.brandsViewModel() }
    

    Where now BrandsViewModel can receive its parameters from Dagger:

    class BrandsViewModel @Inject constructor(
        private val appContext: Context,
        /* other deps */
    ): ViewModel() {
        ...
    }
    

    Though the intent might be cleaner if a Provider<BrandsViewModel> is exposed from Dagger instead

    interface SingletonComponent {
        fun brandsViewModel(): Provider<BrandsViewModel>
    }
    
    brandsViewModel = createViewModel { singletonComponent.brandsViewModel().get() }
    

    With some additional trickery coming in from android-ktx, you could even do

    @Suppress("UNCHECKED_CAST")
    inline fun <reified T : ViewModel> Fragment.fragmentViewModels(
        crossinline creator: () -> T
    ): Lazy<T> {
        return createViewModelLazy(T::class, storeProducer = {
            viewModelStore
        }, factoryProducer = {
            object : ViewModelProvider.Factory {
                override fun <T : ViewModel?> create(
                    modelClass: Class<T>
                ): T = creator.invoke() as T
            }
        })
    }
    

    And then

    class ProfileFragment: Fragment(R.layout.profile_fragment) {
        private val viewModel by fragmentViewModels {
            singletonComponent.brandsViewModelFactory().get()
        }
    

    Where brandsViewModelFactory() is

    fun brandsViewModelFactory(): Provider<BrandsViewModel>
    
    0 讨论(0)
  • The answer is based on android-architecture-components.

    You can use Map multibinding in Dagger.

    First, to declare map key like this.

    @MustBeDocumented
    @Target(
        AnnotationTarget.FUNCTION,
        AnnotationTarget.PROPERTY_GETTER,
        AnnotationTarget.PROPERTY_SETTER
    )
    @Retention(AnnotationRetention.RUNTIME)
    @MapKey
    annotation class ViewModelKey(val value: KClass<out ViewModel>)
    

    Second, to create map.

    @Module
    abstract class ViewModelModule {
        @Binds
        @IntoMap
        @ViewModelKey(UserViewModel::class)
        abstract fun bindUserViewModel(userViewModel: UserViewModel): ViewModel
    
        @Binds
        @IntoMap
        @ViewModelKey(SearchViewModel::class)
        abstract fun bindSearchViewModel(searchViewModel: SearchViewModel): ViewModel
    }
    

    Next, to create the factory file to handle the map which uses key to choose ViewModel.

    @Singleton
    class GithubViewModelFactory @Inject constructor(
        private val creators: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>
    ) : ViewModelProvider.Factory {
        override fun <T : ViewModel> create(modelClass: Class<T>): T {
            val creator = creators[modelClass] ?: creators.entries.firstOrNull {
                modelClass.isAssignableFrom(it.key)
            }?.value ?: throw IllegalArgumentException("unknown model class $modelClass")
            try {
                @Suppress("UNCHECKED_CAST")
                return creator.get() as T
            } catch (e: Exception) {
                throw RuntimeException(e)
            }
        }
    }
    

    Finally, to inject factory in your activity or fragment.

    class SearchFragment : Fragment(), Injectable {
    
        @Inject
        lateinit var viewModelFactory: ViewModelProvider.Factory
    
        override fun onActivityCreated(savedInstanceState: Bundle?) {
            super.onActivityCreated(savedInstanceState)
            searchViewModel = ViewModelProviders.of(this, viewModelFactory)
                .get(SearchViewModel::class.java)
    }
    

    By this way, you can inject repository in your ViewModel.

    class SearchViewModel @Inject constructor(repoRepository: RepoRepository) : ViewModel() {
    }
    
    0 讨论(0)
提交回复
热议问题