Search code examples
androidkotlinkoin

Android - cannot inject Interface class into module


I'm new to the Koin so hopefully someone will be able to point out the direction of the issue I'm encountering.

I've an Interface class:

interface UserApi {

    @POST("/refreshToken")
    @Headers("Accept: application/json")
    suspend fun refreshToken(@Body x: X): TokenResponseDto
}

I've a class where I use UserApi to do API call.

class TokenAuthenticator(
    private val userApi: UserApi
) : Authenticator {

    override fun authenticate(route: Route?, response: Response): Request?  = synchronized(this) {
        runBlocking { userApi.refreshToken() }
    }
}

This far everything is fine, but now I want to Inject TokenAuthenticator class. If I remove constructor for testing purposes, I can see app running and everything is fine, but when I add userApi constructor variable - as I need it, I get and error.

I've NetworkModule that looks like this:

val networkModule = module {
    single<UserApi> {
        Retrofit.Builder()
            .client(get(named("httpClient")))
            .baseUrl(get<String>(named("...")))
            .addConverterFactory(
                ...
            )
            .build()
            .create(UserApi::class.java)
    }

    single(named("httpClient")) {
        val tokenAuthenticator: TokenAuthenticator = get()

        OkHttpClient.Builder()
            .authenticator(tokenAuthenticator)
            .connectTimeout(30, TimeUnit.SECONDS)
            .readTimeout(30, TimeUnit.SECONDS)
            .build()
    }

    single {
        TokenAuthenticator(get())
    }
}

Error:

at org.koin.core.instance.SingleInstanceFactory$get$1.invoke(SingleInstanceFactory.kt:53)

Solution

  • UPDATE: Someone advised to use a lambda function in TokenAuthenticator. I think this solution is more simplier.

    class TokenAuthenticator(
        private val userApi : () -> UserApi
    ) {
        // ...
      fun authenticate(...) {
        userApi().refreshToken()
      }
    }
    

    In this case you can define your koin definition like this.

    single {
        TokenAuthenticator { 
            get() 
        }
    }
    

    My answer was:

    There may be better solutions but this is a rushed one. You may improve it.

    Let's decouple TokenAuthenticator and UserApi. They will be connected later by a TokenRefresher.

    interface TokenRefresher {
      fun refreshToken()
    }
    
    class TokenAuthenticator(
            private val tokenRefresher: TokenRefresher
    ) : Authenticator {
    
        override fun authenticate(route: Route?, response: Response): Request?  = synchronized(this) {
            runBlocking { tokenRefresher.refreshToken() }
        }
    }
    

    Add a token refresher into koin module.

    val networkModule = module {
        single<TokenRefresher> {
            object : TokenRefresher {
    
                // now use the userApi
                override fun refreshToken() {
                    val userApi: UserApi = get()
                    userApi.refreshToken()
                }
            }
        }
    
        single<UserApi> {
            Retrofit.Builder()
                .client(get(named("httpClient")))
                .baseUrl(get<String>(named("...")))
                .addConverterFactory(
                    ...
                )
                .build()
                .create(UserApi::class.java)
        }
    
        single(named("httpClient")) {
            val tokenAuthenticator: TokenAuthenticator = get()
    
            OkHttpClient.Builder()
                .authenticator(tokenAuthenticator)
                .connectTimeout(30, TimeUnit.SECONDS)
                .readTimeout(30, TimeUnit.SECONDS)
                .build()
        }
    
        single {
            TokenAuthenticator(get())
        }
    
    }
    

    Hope it helps.