Dagger2. How to use multibinding with supporting of assisted injection? - android

I have an interface PaymentProvider:
interface PaymentProvider {
fun pay(amount: Double): Boolean
}
Here is different implementations:
class PayPalPayment #AssistedInject constructor(
private val payPalRepo: PayPalRepository,
#Assisted private val paymentData: PaymentData
): PaymentProvider {
#AssistedFactory
interface Factory : PaymentProviderFactory
override fun pay(amount: Double): Boolean {
// some code
}
}
class CreditCardPayment(
private val creditCardPaymentRepo: CreditCardPaymentRepository
): PaymentProvider {
override fun pay(amount: Double): Boolean {
// some code
}
}
Also I have PaymentManager class who should receive PaymentProvider.
class PaymentManager(
private val paymentProvider:PaymentProvider
) {
// some logic
}
I want this class not to know which specific implementation of the PaymentProvider interface it works with. For this I'm provide with dagger implementation using multibindings.
Here is my code:
#Binds
#[IntoMap PaymentProviderKey(Keys.PAYPAL)]
fun bindPayPalPayment (factory: PayPalPayment.Factory): PaymentProviderFactory
#Binds
#[IntoMap PaymentProviderKey(Keys.CREDIT_CARD)]
fun bindCreditCardPayment (factory: CreditCardPayment.Factory): PaymentProviderFactory
interface PaymentProviderFactory{
fun create(key: PaymentProviderKey, paymentData: PaymentData): PaymentProvider
}
Now I have a map of factories, but I don't understand how to provide specific implementation of interface. I tried to provide PaymentProviderFactory:
#Provides
fun providePaymentManager(
factory: PaymentProviderFactory
): PaymentManager = PaymentManager (
factory
)
and i get this kind of error:
Invalid return type: PaymentProvider. An assisted factory's abstract
method must return a type with an #AssistedInject-annotated
constructor.
Please, help me. I have a problem when using #AssistedInject, without it I provide map of PaymentProvider and then I provide factory to create concrete implementation:
#Provides
fun providePaymentProviderFactory(
paymentProviders: Map<PaymentProviderKey, #JvmSuppressWildcards Provider<PaymentProvider>>
): PaymentProviderFactory=
object : PaymentProviderFactory{
override fun create(key: PaymentProviderKey): PaymentProvider? {
val paymentProvider = paymentProviders[key]
return paymentProvider?.get()
}
}
In this case everything works, however I need to use #AssistedInject because I need to pass the data that I receive at runtime.

Related

MVVM: Set a getDrawable from a ViewModel

I'm moving all my app's logic from the fragment to the ViewModel and cannot move those methods that set drawables.
Example:
marker.icon = ContextCompat.getDrawable(requireContext(), R.drawable.waypoints_sq_blank)
The reason being that the context is not reachable from a ViewModel.
Any way I can get this to work?
The best practice for your queestion is using hilt dependency injection and injecting a custom class to your view model constructor
you can create a class to use it in all view models like this:
class AppResourceProvider(private val context: Context) {
fun getString(id: Int): String {
return context.getString(id)
}
fun getDrawable(#DrawableRes id: Int): Drawable{
return ContextCompat.getDrawable(context, id)
}
}
Then you need to inject this class to your appModule:
#Module
#InstallIn(SingletonComponent::class)
class AppModule {
#Provides
#Singleton
fun provideAppResourceProvider(#ApplicationContext context: Context): AppResourceProvider{
return AppResourceProvider(context)
}
}
finally you can use it in your all viewModels like this:
class SampleViewModel #Inject constructor(
private val resourceProvider: AppResourceProvider
){
fun whereYouNeed(){
marker.icon = resourceProvider.getDrawable(R.drawable.waypoints_sq_blank)
}
}

Error: cannot be provided without an #Provides-annotated method

I got an error
public abstract static class SingletonC implements FragmentGetContextFix.FragmentGetContextFixEntryPoint,
^
kotlin.jvm.functions.Function1<? super pl.beskidmedia.bm.viewModel.repozytory.model.Token,kotlin.Unit> is injected at
pl.beskidmedia.bm.domainlayer.viewmodel.tv.TvViewModel(fetchHlsUseCase, �)
pl.beskidmedia.bm.domainlayer.viewmodel.tv.TvViewModel is injected at
pl.beskidmedia.bm.domainlayer.viewmodel.tv.TvViewModel_HiltModules.BindsModule.binds(vm)
#dagger.hilt.android.internal.lifecycle.HiltViewModelMap java.util.Map<java.lang.String,javax.inject.Provider<androidx.lifecycle.ViewModel>> is requested at
dagger.hilt.android.internal.lifecycle.HiltViewModelFactory.ViewModelFactoriesEntryPoint.getHiltViewModelMap() [pl.beskidmedia.bm.MainApplication_HiltComponents.SingletonC ? pl.beskidmedia.bm.MainApplication_HiltComponents.ActivityRetainedC ? pl.beskidmedia.bm.MainApplication_HiltComponents.ViewModelC]
when i tried to provide my use-case to view model, here are my functions that should create use case invoke() instance that i want to use later in my view model
fun getHls(
tvRepository: TvRepository
): Flow<List<Hls>> {
return tvRepository.getHls()
}
typealias GetHlsUseCase = () -> Flow<List<Hls>>
fun fetchHls(
tvRepository: TvRepository,
token: Token
) {
return tvRepository.fetchHls(token.token)
}
typealias FetchHlsUseCase = (Token) -> Unit
and here are my modules
#Module
#InstallIn(ViewModelComponent::class)
class ProvideFetchHlsUseCase {
#Provides
fun provideFetchHlsUseCase(
tvRepository: TvRepository
): FetchHlsUseCase = { fetchHls(tvRepository, it) }
}
#Module
#InstallIn(ViewModelComponent::class)
class ProvideGetHlsUseCase {
#Provides
fun provideGetHlsUseCase(
tvRepository: TvRepository
): GetHlsUseCase = { getHls(tvRepository) }
}
and finally my view model
#HiltViewModel
class TvViewModel #Inject constructor(
private val fetchHlsUseCase: FetchHlsUseCase,
private val getHlsUseCase: GetHlsUseCase
) : BaseViewModel() {
val hls = getHlsUseCase().asLiveData(coroutineExceptionHandler)
}
I can't change my provide to binding (this is what i saw in different questions here) or at least I don't know how to do it. When I was implementing this into my project I was inspired by https://medium.com/swlh/functional-use-cases-f896f92e768f this article, but author show only how to implement view model (no interface or bindings). From what i understands dagger should use designated provide methods that I implemented in modules but for some reason it doesn't happen.
Ok so this was a tricky one cuz this problem is actually well known but it is hard to google if you don't know how to name it, this was resolved here
In short all I needed to do was insert #JvmSuppressWildcards behind my typealias "classes" in ViewModel at the insertion point like so
#HiltViewModel
class TvViewModel #Inject constructor(
private val fetchHlsUseCase: #JvmSuppressWildcards FetchHlsUseCase,
private val getHlsUseCase: #JvmSuppressWildcards GetHlsUseCase
) : BaseViewModel() {
putting this annotation behind private keyword did nothing that why i had problems with this :D

Using Hilt, how to inject into a class that does not have a context?

I have a class named NetworkManager. Since it is not one of the Android Components, I am using custom entry point, NetworkManagerEntryPoint with one fun that returns NetworkClient object which is what I want to inject.
Now, to inject an instance of this class using Hilt, I believe I need to use one of the Helper methods in EntryPointAccessors. But all of them requires a reference to android components. So, do I really have to pass an android component like Context to my class to inject an object using Hilt?
class NetworkManager() {
#InstallIn(SingletonComponent::class)
#EntryPoint
interface NetworkManagerEntryPoint {
fun getNetworkClient(): NetworkClient
}
var defaultNetworkClient: NetworkClient = EntryPointAccessors.fromApplication(
context, // Do I have to pass a context to this class to use Hilt?
NetworkManagerEntryPoint::class.java
).getNetworkClient()
fun <R : Any> executeRequest(
request:Request<R>,
networkClient: NetworkClient = defaultNetworkClient
): Response<R> {
// Do some operation
}
}
Hi there maybe you can try this way i have done , i follow the mvvm pattern
My RetrofitApi
interface RetrofitApi {
#GET("endpoint")
suspend fun getApi():Response<RetrofitApiResponse>
}
My NetworkModule
#Module
#InstallIn(SingletonComponent::class)
object NetworkModule{
#Singleton
#Provides
fun provideApi(): RetrofitApi = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(RetrofitApi::class.java)
#Singleton
#Provides
fun provideRepository(retrofitApi:RetrofitApi) : MainRepository =
DefualtMainRepository(retrofitApi)
}
and this module gets injected in my repository
class DefualtMainRepository #Inject constructor(
val retrofitApi: RetrofitApi
):MainRepository {
override suspend fun getQuotes(): Resource<RetrofitApiResponse> {
val response = retrofitApi.getApi()
val result = response.body()
if (response.successful){
}
}
}
If you are interested i have full project in my github and even wrote a medium article explaining it, Hopefully my answer is helpful to you
https://zaidzakir.medium.com/a-simple-android-app-using-mvvm-dagger-hilt-e9f45381f1bc
Okay. #Zaid Zakir's answer showed me that I can inject objects via constructor parameters, if not field injection. So, the solution for me ended up looking like this.
#Singleton
class NetworkManager #Inject constructor(
var defaultNetworkClient: NetworkClient
) {
fun <R : Any> executeRequest(
request:Request<R>,
networkClient: NetworkClient = defaultNetworkClient
): Response<R> {
// Do some operation
}
}
In another class named NetworkClientModule, I have this,
#Module
#InstallIn(SingletonComponent::class)
abstract class NetworkClientModule {
#Binds #Singleton
abstract fun bindDefaultNetworkClient(impl: DefaultNetworkClient): NetworkClient
}

How to use AssistedInject to pass dynamic value as a parameter to ViewModel when using Dagger2 in Android

I am new to Dagger 2 in android. I am having trouble understanding how to inject ViewModel with dynamic value. So Far I have successfully injected ViewModel using dagger multi binding with pre-defined repository dependency. Here's my code.
ApplicationComponent
#Singleton
#Component(modules = [AppModule::class, SubComponentsModule::class, ViewModelFactoryModule::class])
interface ApplicationComponent {
#Component.Factory
interface Factory {
fun create(#BindsInstance applicationContext: Context): ApplicationComponent
}
fun activityComponent(): ActivitySubComponent.Factory
fun fragmentComponent(): FragmentSubComponent.Factory
}
FragmentModule
#Module
abstract class FragmentModule {
#Binds
#IntoMap
#ViewModelKey(WeatherViewModel::class)
abstract fun bindWeatherView(weatherViewModel: WeatherViewModel) : ViewModel
}
ViewModelFactoryModule
#Module
class ViewModelFactoryModule {
#Provides
#Singleton
fun viewModelFactory(providerMap: Map<Class<out ViewModel>, Provider<ViewModel>>): ViewModelProvider.Factory {
return ViewModelFactory(providerMap)
}
}
Application class
class ThisApplication: Application(),InjectorProvider {
override fun onCreate() {
super.onCreate()
Stetho.initializeWithDefaults(this)
}
override val component by lazy {
DaggerApplicationComponent.factory().create(applicationContext)
}
}
I'm using InjectorProvider interface to get dagger to fragments and activity without having to cast every time.
InjectorProvider
interface InjectorProvider {
val component: ApplicationComponent
}
val Activity.injector get() = (application as InjectorProvider).component
val Fragment.injector get() = (requireActivity().application as InjectorProvider).component
This is the simple ViewModel I used for testing ViewModel injection.
WeatherViewModel
class WeatherViewModel #Inject constructor(val repository: WeatherRepository): ViewModel() {
fun printMessage(){
Log.d("WeatherViewModel","ViewModel binding is working")
repository.printMessage()
}
}
Finally, I Injected this view model into a fragment like below.
WeatherFragment
class WeatherFragment : Fragment() {
#Inject
lateinit var viewModelFactory: ViewModelFactory
override fun onAttach(context: Context) {
injector.fragmentComponent().create().injectWeatherFragment(this)
super.onAttach(context)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val mainActivityViewModel =
ViewModelProvider(this,viewModelFactory)[WeatherViewModel::class.java]
mainActivityViewModel.printMessage()
}
}
This part is working fine. The Log message inside printMessage() getting printed. I saw in the dagger issue discussion that using AssistedInject is the best approach to handle this kind of scenario. I changed my ViewModle by adding a simple int value as a parameter.
Edited WeatherViewModel
class WeatherViewModel #AssistedInject constructor(val repository: WeatherRepository,
#Assisted val id: Int): ViewModel() {
#AssistedInject.Factory
interface Factory{ fun create(id: Int) : WeatherViewModel }
fun printMessage(){
Log.d("WeatherViewModel","ViewModel binding is working")
repository.printMessage()
}
}
Edited ApplicationComponent
#Singleton
#Component(modules = [AppModule::class, SubComponentsModule::class, ViewModelFactoryModule::class, AssistedInjectModule::class])
interface ApplicationComponent {
#Component.Factory
interface Factory {
fun create(#BindsInstance applicationContext: Context): ApplicationComponent
}
fun activityComponent(): ActivitySubComponent.Factory
fun fragmentComponent(): FragmentSubComponent.Factory
}
#AssistedModule
#Module(includes = [AssistedInject_AssistedInjectModule::class])
interface AssistedInjectModule
From this point onwards I don't understand how to inject ViewModel into fragment with repository plus dynamic "id" value. If I inject WeatherViewModel.Factory into the fragment by calling the create method (val mainActivityViewModel = factory.create(5)) it won't fulfill the repository dependency in ViewModel. How to combine these two solutions to have pre-defined repository dependency with dynamic value? OR is there any other better way of approaching this?
Not quite sure why your setup wont fulfill repository dependency by using create() method of factory. The repository dependency will be provided by Dagger's Acyclic Dependency Graph.
For example, below I'm saying to Dagger that I am responsible for providing SavedStateHandle and the NavigationDispatcher so don't even bother looking these up in your acyclic dependency graph.
class ProfileViewModel #AssistedInject constructor(
#Assisted val handle: SavedStateHandle,
#Assisted val navigationDispatcher: NavigationDispatcher,
private val eventTracker: EventTracker,
private val getUserUseCase: GetUserUseCase,
private val logOutUseCase: LogOutUseCase
) : ViewModel(), ProfileHandler {
#AssistedInject.Factory
interface Factory {
fun create(
handle: SavedStateHandle,
navigationDispatcher: NavigationDispatcher
): ProfileViewModel
}
In Fragment side, all I have to provide in the create method will be the dependencies i marked with #Assisted to fulfil my side of promise.
class ProfileFragment : Fragment() {
private val navigationDispatcher by getActivityViewModel {
getBaseComponent().navigationDispatcher
}
private val eventTracker by lazy {
getProfileComponent().eventTracker
}
private val viewModel by getViewModel { savedStateHandle ->
getProfileComponent().profileViewModelFactory.create(savedStateHandle, navigationDispatcher)
}
getViewModel is simply an extension function as follows:
inline fun <reified T : ViewModel> Fragment.getViewModel(crossinline provider: (handle: SavedStateHandle) -> T) =
viewModels<T> {
object : AbstractSavedStateViewModelFactory(this, arguments) {
override fun <T : ViewModel?> create(
key: String,
modelClass: Class<T>,
handle: SavedStateHandle
) = provider(handle) as T
}
}

Adding a custom Hilt component

good day, i'm trying to create custom component in hilt so i followed this tutorial medium and this tutorial as well documentation.
but when i run the application i get this error indicating that it failed to create the viewholdermanager
kotlin.UninitializedPropertyAccessException: lateinit property viewHolderManager has not been initialized.
so i believe that i'm missing something as per my understanding "We use the provider Dagger generates for us to create instances of ViewHolderComponent( custom component)"
really appreciate your help. thanks in advance
here is what i done :-
#Scope
#MustBeDocumented
#Retention(value = AnnotationRetention.RUNTIME)
annotation class ViewHolderScope
#ViewHolderScope
#DefineComponent(parent = ApplicationComponent::class)
interface ViewHolderComponent{
#DefineComponent.Builder
interface Builder {
fun build(): ViewHolderComponent
fun viewHolder(#BindsInstance viewHolder: RecyclerView.ViewHolder?): ViewHolderComponent.Builder?
}
}
#Singleton
class ViewHolderManager #Inject constructor(
private val viewHolderComponentProvider: Provider<ViewHolderComponent.Builder>
) {
var viewHolderComponent: ViewHolderComponent? = null
private set
fun setViewHolder(viewHolder: RecyclerView.ViewHolder) {
viewHolderComponent = viewHolderComponentProvider.get().viewHolder(viewHolder)?.build()
}
}
#Module
#InstallIn(ViewHolderComponent::class)
class ViewHolderModule{
#Provides
#ViewHolderScope
fun provideLifecycleRegistry(viewHolder: RecyclerView.ViewHolder): LifecycleRegistry = LifecycleRegistry(viewHolder)
#Provides
#ViewHolderScope
fun provideArrayAdapter(viewHolder: RecyclerView.ViewHolder): ArrayAdapter<DataItem> = ArrayAdapter((viewHolder as BaseItemViewHolder<*, *>).parent.context, R.layout.item_dropdown_menu_popup,
ArrayList())
}
class SavedCarItemViewHolder(): RecyclerView.ViewHolder(...){
#EntryPoint
#InstallIn(ViewHolderComponent::class)
interface ViewHolderEntryPoint {
fun getLifecycleRegistry(): LifecycleRegistry
fun getDataItemArrayAdapter(): ArrayAdapter<DataItem>
}
protected fun onCreate() {
injectDependencies()
}
lateinit var lifecycleRegistry: LifecycleRegistry
#Inject lateinit var viewHolderManager:ViewHolderManager
fun injectDependencies() {
val hiltEntryPoint = EntryPoints.get(viewHolderManager.viewHolderComponent, ViewHolderEntryPoint::class.java)
lifecycleRegistry = hiltEntryPoint.getLifecycleRegistry()
}
}
I think you have to change your class ViewHolderModule to object ViewHolderModule otherwise #Provides is not executed. Please share your experience with this result and when it does not work, I will try to dig deeper in.
You should use #Inject annotation to constructor and inject ViewHolderManager

Categories

Resources