Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Is it possible to change Singleton (or any parameter) values after the initial Kodein instantiation? #247

Closed
JimVanG opened this issue Sep 24, 2019 · 10 comments
Assignees
Labels
type: question Issue thats just a question / discussion. Might be post on stackoverflow / slack

Comments

@JimVanG
Copy link

JimVanG commented Sep 24, 2019

I’d really like to be able to change the value of my HTTP clients baseUrl value, because it can be set dynamically while using the application. I’m having trouble finding a way to change parameter values dynamically.

I’ve looked into Multiton binding as it seemed like a type of Singleton-Factory that would allow the arguments to be changed. But I had no luck with that.

Here is my Api singleton:

interface LoginApi {

    @POST("Account/Login")
    @FormUrlEncoded
    fun login(
        @Field("username") username: String,
        @Field("password") password: String,
        @Field("sessionType") sessionType: String
    ): Call<BaseApiResponse<Login>>

    @GET("Account/Logout")
    fun logout(
        @Query("token") token: String
    ): Call<BaseApiResponse<Any>>

    companion object {
        operator fun invoke(
            baseUrl: String,
            cache: Cache
        ): LoginApi {

            val okHttpClient = OkHttpClient.Builder()
                .cache(cache)
                .readTimeout(60, TimeUnit.SECONDS)
                .connectTimeout(60, TimeUnit.SECONDS)
                .build()

            return Retrofit.Builder()
                .client(okHttpClient)
                .baseUrl(baseUrl)
                .addConverterFactory(JacksonConverterFactory.create())
                .build()
                .create(LoginApi::class.java)
        }
    }
}

Dependency Declaration with muliton (doesn’t work):

     import(androidXModule(this@MyApplication))

        bind<Any>() with multiton { baseUrl: String, applicationContext: Context ->
            createLoginApi(baseUrl, applicationContext)
            LoginApi(
                BuildConfig.BASE_URL,
                Cache(applicationContext.cacheDir, 10 * 1024 * 102)
            )
        }

        bind<LoginDataSource>() with singleton { LoginService(instance()) }
        bind<LoginRepository>() with singleton { LoginRepositoryImpl(instance()) }
        bind() from singleton { LoginViewModelFactory(instance()) }
        bind() from singleton { LoginViewModel(instance()) }
    }

Dependency Declaration with no multiton (works):

    import(androidXModule(this@MyApplication))

        bind() from singleton {
            LoginApi(
                BuildConfig.BASE_URL,
                Cache(applicationContext.cacheDir, 10 * 1024 * 102)
            )
        }

        bind<LoginDataSource>() with singleton { LoginService(instance()) }
        bind<LoginRepository>() with singleton { LoginRepositoryImpl(instance()) }
        bind() from singleton { LoginViewModelFactory(instance()) }
        bind() from singleton { LoginViewModel(instance()) }
    }

Version

    implementation 'org.kodein.di:kodein-di-generic-jvm:6.3.2'
    implementation "org.kodein.di:kodein-di-framework-android-x:6.3.2”

Let me know if you need any other information. This is pretty very simple example -- I’d just like to be able to change parameter values dynamically. If that isn’t possible, then is there any way that would allow me to do such a thing

Thank you in advance!

@romainbsl
Copy link
Member

Hello,

We don't recommend to change the kodein instance at runtime. In fact, it's not possible out of the box.

You are on the right tracks, Multiton can be your solution, but binding Any is not a good idea. That's certainly why it's not working.

You need to bind LoginApi, as in your working example, bind() from ... binds the return type of the lambda, thus LoginApi.

Be careful, in 6.5 we will depreciate mutli arguments factories/multiton, we recommend to use data classes, giving you something like:

data class LoginArguments(val baseUrl: String, val applicationContext: Context)

bind<LoginApi>() with multiton { args: LoginArguments ->
            createLoginApi(args.baseUrl, args.applicationContext)
            LoginApi(
                BuildConfig.BASE_URL,
                Cache(args.applicationContext.cacheDir, 10 * 1024 * 102)
            )
        }

tell me if that helps.

@romainbsl romainbsl self-assigned this Sep 24, 2019
@romainbsl romainbsl added the type: question Issue thats just a question / discussion. Might be post on stackoverflow / slack label Sep 24, 2019
@JimVanG
Copy link
Author

JimVanG commented Sep 24, 2019

Thanks for the reply

Here’s the new Multiton implementation:

        bind<LoginApi>() with multiton { baseUrl: String, applicationContext: Context ->
            createLoginApi(baseUrl, applicationContext)
            LoginApi(
                baseUrl,
                Cache(applicationContext.cacheDir, 10 * 1024 * 102)
            )
        }

createLoginApi() function: (pretty much same thing in Multiton block)

    private fun createLoginApi(baseUrl: String, applicationContext: Context) =
        LoginApi(
            baseUrl,
            Cache(applicationContext.cacheDir, 10 * 1024 * 102)
     )

Error when using Multiton:

java.lang.ClassCastException: kotlin.Unit cannot be cast to org.kodein.di.Multi2
at com.myapp$kodein$1$$special$$inlined$multiton$3.invoke(Unknown Source:2)
at org.kodein.di.bindings.Multiton$getFactory$1$1$1.invoke(standardBindings.kt:66)
at org.kodein.di.bindings.SingletonReference.make(references.kt:34)
at org.kodein.di.bindings.Multiton$getFactory$1$1.invoke(standardBindings.kt:66)
at org.kodein.di.bindings.Multiton$getFactory$1$1.invoke(standardBindings.kt:37)
at org.kodein.di.bindings.StandardScopeRegistry.getOrCreate(scopes.kt:65)
at org.kodein.di.bindings.Multiton$getFactory$1.invoke(standardBindings.kt:66)

Let me know if you need any other information - I haven’t changed anything else.

Also:

Be careful, in 6.5 we will depreciate mutli arguments factories/multiton, we recommend to use data classes...

This sound very nice! Looking forward!

@romainbsl
Copy link
Member

This sound very nice! Looking forward!

You can now use a data class, no need to wait until 6.5. In 6.5 the ability to use mutliple argument in multiton will be deprecated. So you must in data class from now.

How do you try to inject your multiton ?

@JimVanG
Copy link
Author

JimVanG commented Sep 24, 2019

Alright so i just changed to use the data class version of the Multiton. I’m now receiving a different error, the error that I originally faced when initially trying to use the multiton extension.

Multiton w/ data class

        bind<LoginApi>() with multiton { args: LoginArguments ->
            createLoginApi(args.baseUrl, applicationContext)
            LoginApi(
                args.baseUrl,
                Cache(applicationContext.cacheDir, 10 * 1024 * 102)
            )
        }

// The Datasource is then bound with the LoginApi supplied using the `instance()` function
        bind<LoginDataSource>() with singleton { LoginService(instance()) }

Error (see comment I included in StackTrace):

org.kodein.di.Kodein$NotFoundException: No binding found for bind<LoginApi>() with ?<FragmentActivity>().? { ? }
Available bindings for this type:
            //  
            // notice how it suggests doing this, when already is registered in the Kodien Container
            //
            bind< LoginApi >() with multiton { LoginArguments -> LoginApi }
Registered in this Kodein container:
            bind< LoginApi >() with multiton { LoginArguments -> LoginApi }
            bind<LoginDataSource>() with singleton { LoginService }
            bind<LoginRepository>() with singleton { LoginRepositoryImpl }
            bind<LoginViewModelFactory>() with singleton { LoginViewModelFactory }
            bind<LoginViewModel>() with singleton { LoginViewModel }

How do you try to inject your multiton ?

Well I guess it’s injected via the LoginService constructor: class LoginService(private val api: CooltrackLoginApi) : LoginDataSource.

When it comes to retrieving the dependencies (i.e. LoginViewModel) I use retrieval, with the KodeinAware interface.

@JimVanG
Copy link
Author

JimVanG commented Sep 24, 2019

I’m doing everything you previously suggested in a similar issue: #178 (comment)

@romainbsl
Copy link
Member

Well, I get what you're trying to do.
There is no magic in Kodein, if you bind a multiton with some arguments, like:

bind<LoginApi>() with multiton { args: LoginArguments -> ... }

You can't expect from Kodein to retrieve it with some dynamic values by applying the instance function, as you can have multiple instances to inject.

What you could do is to use another multiton, like:

bind<LoginDataSource>() with multiton { args: LoginArguments -> LoginService(instance(arg = args)) }

thus, the usage of LoginDataSource could be something like:

val dataSource: LoginDataSource by kodein.instance(arg = LoginArguments("/path", context))

@JimVanG
Copy link
Author

JimVanG commented Sep 24, 2019

Thanks for all the quick replies.

(If you haven’t noticed) DI is all new to me...

I’m curious how I would use the LoginDataSource the way suggested, as it’s being passed as an argument to the LoginReposity.

Here is my current Kodein declaration implementation:

    override val kodein = Kodein.lazy {
        // must import Application class. NOTE: must use `androidXModule` - we're using AndroidX.
        import(androidXModule(this@Application))
        registerContextTranslator { f: Fragment -> f.activity }

        bind<LoginApi>() with multiton { args: LoginArguments ->
            createLoginApi(args.baseUrl, applicationContext)
            LoginApi(
                args.baseUrl,
                Cache(applicationContext.cacheDir, 10 * 1024 * 102)
            )
        }

//        bind<LoginDataSource>() with singleton { LoginService(instance()) }
        bind<LoginDataSource>() with multiton { args: LoginArguments -> LoginService(instance(arg = args)) }
        bind<LoginRepository>() with singleton { LoginRepositoryImpl(instance()) }
        bind() from singleton { LoginViewModelFactory(instance()) }
        bind() from singleton { LoginViewModel(instance()) }
    }

LoginFragment

class LoginFragment : Fragment(R.layout.fragment_login), KodeinAware {

    override val kodein by closestKodein()
    val viewModel: LoginViewModel by lazy { on(requireActivity()).direct.instance<LoginViewModel>() }
...

I’m curious where this: val dataSource: LoginDataSource by kodein.instance(arg = LoginArguments("/path", context)) would come into play, and how this would “update” the baseUrl parameter that I need to be able to change.

Thanks again

@JimVanG
Copy link
Author

JimVanG commented Sep 24, 2019

Ok i think understand after messing around a bit and have it working with the muliton.

Here’s what I’ve changed in the Kodein declaration block:

        bind<LoginApi>() with multiton { args: LoginArguments ->
            createLoginApi(args.baseUrl, applicationContext)
            LoginApi(
                args.baseUrl,
                Cache(applicationContext.cacheDir, 10 * 1024 * 102)
            )
        }

        bind<LoginDataSource>() with multiton { args: LoginArguments -> LoginService(instance(arg = args)) }
        val loginDataSource: LoginDataSource by instance(arg = LoginArguments(BuildConfig.BASE_URL))
        // Supply the multiton loginDataSource 
        bind<LoginRepository>() with singleton { LoginRepositoryImpl(loginDataSource) }

Thank you!

@romainbsl
Copy link
Member

that should work, but you're code is not entirely decoupled.

bind<LoginApi>() with multiton { args: LoginArguments ->
    createLoginApi(args.baseUrl, applicationContext)
    LoginApi(
        args.baseUrl,
        Cache(applicationContext.cacheDir, 10 * 1024 * 102)
    )
}

bind<LoginDataSource>() with multiton { args: LoginArguments -> LoginService(instance(arg = args)) }

bind<LoginRepository>() with singleton { 
    LoginRepositoryImpl(instance(arg = LoginArguments(BuildConfig.BASE_URL)))
}

When retrieving the instance of LoginRepository Kodein will:

  1. Look for a binding for LoginRepository
    • that's the singleton with LoginRepositoryImpl waiting for a LoginDataSource
  2. Look for LoginDataSource binding that need a parameter args: LoginArguments
    • that's the multiton returning a LoginService with args: LoginArgumentsas parameter
      • this is the job of instance(arg = LoginArguments(BuildConfig.BASE_URL))

Hope this is clear enough.

@JimVanG
Copy link
Author

JimVanG commented Sep 24, 2019

Again thanks for all the help. Really appreciate.

but you're code is not entirely decoupled

How could I decouple this any further...? If you have time to help. Thank you!

UPDATE:
Oh nevermind I think the code sample you provided will help with that

Update 2: Yes, thank you very much. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: question Issue thats just a question / discussion. Might be post on stackoverflow / slack
Projects
None yet
Development

No branches or pull requests

2 participants