How to create a @Provides function for a class that have @AssistedInject with android Hilt

66 views Asked by At

So I'm trying to figure out how to create a @Provides function for a class that have @Assisted inject, is it possible or even a good practice?

I'm trying to inject a Pager from the android Paging library 3 into my Repository, I already managed to do it when the PagingSource don't have dynamic(runtime) arguments. Here is my code:


@Qualifier
annotation class ComicPager

private const val PAGE_SIZE = 10

@Module
@InstallIn(SingletonComponent::class)
internal object PagerModule {
    @ComicPager
    @Provides
    fun provideComicPager(
        config: PagingConfig,
        pagingSource: ComicPagingSource
    ): Pager<Int, ComicDto> = 
Pager(config = config, pagingSourceFactory = { pagingSource })
    
        @Provides
        fun providePagingConfig(): PagingConfig = PagingConfig(pageSize = PAGE_SIZE)
}

And this is my ComicPagingSource constructor:

internal class ComicPagingSource @Inject constructor(
private val comicDataSource: ComicDataSource
) : PagingSource<Int, ComicDto>() {
    // Paging logic here...
}

And the ComicRepository where the Pager is injected:

@Singleton
internal class ComicRepository @Inject constructor(
    private val comicDataSource: ComicDataSource,
    private val pager: Pager<Int, ComicDto> // <- Pager injected!
) {


    fun getComics(): Flow<PagingData<Comic>> {
        return pager.flow
            .map { pagingData ->
                pagingData.map { comic -> comic.toComic() }
            }
    }
}

So far so good the Pager is provided to the ComicRepository.

Now I want to do the same thing for another PagingSource that I have that receives a id as a parameter in the constructor, I thought that would be simple to just pass the parameter to the provider method but I can't find a way to do it. This is the constructor for the other PagingSource:

internal class CreatorPagingSource @AssistedInject constructor(
    @Assisted private val creatorId: Int,
    private val creatorDataSource: CreatorDataSource
) : PagingSource<Int, ComicDto>() {
    // Paging logic here...
}

@AssistedFactory
internal interface Factory {
    fun create(creatorId: Int): CreatorPagingSource
}


internal class CreatorPagingSourceFactoryProvider @Inject constructor(
    private val creatorDataSource: CreatorDataSource
) :
    CreatorPagingSource.Factory {
    override fun create(creatorId: Int): CreatorPagingSource =
        CreatorPagingSource(creatorId, creatorDataSource = creatorDataSource)
}

This is what I accomplished until now on my repository:

internal class CreatorRepository @Inject constructor(
    private val pagingConfig: PagingConfig,
    private val pagingSourceFactoryProvider: PagingSourceFactoryProvider,
) {
    fun getCreatorComics(creatorId: Int): Flow<PagingData<Comic>> {
        return Pager(
            config = pagingConfig,
            pagingSourceFactory = { pagingSourceFactoryProvider.create(creatorId) } // <- Unable to inject the Pager because can't create a Provider to this injector
        ).flow
            .map { pagingData ->
                pagingData.map { comic -> comic.toComic() }
            }
    }
}

But it is not to my liking because I'm still creating the Pager inside the getCreatorComics methods, can someone help me figure out how to pass an @Assisted parameter to a @Provides or point me to a better direction?

1

There are 1 answers

2
Fred On

Usually, you inject the factory because you will only be able to create the object at run time. You can delegate the creation itself, but someone has to create it.

If the problem is because you create concrete implementations and therefore couple implementations I can provide an example of how you can avoid this. I'm not sure it's necessarily better but let me explain the reasoning as I go.

First, I've noticed you've implemented CreatorPagingSource.Factory but since this is an @AssisteFactory dagger will create the implementation for you. So I'd remove the implementation.

I often prefer to depend on interfaces rather than concrete classes, so for these cases I often do the following:

interface PagingSourceFactory {
   fun create(creatorId: Int): PagingSource<Int, ComicDto>
}

This is an interface for a PagingSource factory. You can now implement it using @AssistedFactory like so:

@AssistedFactory
internal interface Factory : PagingSourceFactory {
   override fun create(creatorId: Int): CreatorPagingSource
}

Notice how we extend the factory and override the create method changing it's return type. This is required by AssistedInject - to return the concrete implementation and not an abstraction. This is legal in Java - you can change the return type as long as it's within the bounds of the method it overrides.

You can now inject only PagingSourceFactory where you need it by writing a provider:

@Binds
fun bindPagingSourceFactory(
  creatorPagingSourceFactory: Factory, // this is the AssistedFactory you've defined
): PagingSourceFactory

And the repo can become:

internal class CreatorRepository @Inject constructor(
    private val pagingConfig: PagingConfig,
    private val pagingSourceFactory: PagingSourceFactory,
) {
    fun getCreatorComics(creatorId: Int): Flow<PagingData<Comic>> {
        return Pager(
            config = pagingConfig,
            pagingSourceFactory = { pagingSourceFactory.create(creatorId) } 
        ).flow
            .map { pagingData ->
                pagingData.map { comic -> comic.toComic() }
            }
    }
}

The difference is that you're now creating the PagingSource<Int, ComicDto> abstraction and your repository doesn't need to know about CreatorPagingSource. In the future you can change the dagger modules to return other implementations. I think it's even better like this for testing and injecting mock factories that create mock objects.

Not sure if this will help your issue but I think it could be useful.