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

How to inject VM interface rather that concrete class using koin? #346

Open
emanuelecastelli opened this issue Jul 3, 2024 · 9 comments

Comments

@emanuelecastelli
Copy link

Hi, for testing purpose i need to inject/retrieve viewmodels inside composable functions using interfaces rather that concrete classes (Koin for DI).
For mocking i use MocKMP, actually the only mocking lib in KMM env.
It works well, but can mock just interfaces (with Mockito you can mock classes too).

Now i have something like this, but when testing koin can not find vm class injected.
There isn't a way to find vm concrete class by it's interface in stateholder?

Thx

// Test.kt
// ....
@Mock
  lateinit var loginViewModel: ILoginViewModel

  private var testModule: Module = module {
      factory<ILoginViewModel>(qualifier = named("loginVM")) { loginViewModel }
  }
// ....
// Composable.kt
// ...
    val viewModel: ILoginViewModel = koinViewModel(LoginViewModel::class, named("loginVM"))
// ...
@Tlaster
Copy link
Owner

Tlaster commented Jul 3, 2024

Have you call startKoin in your testing code? You can check out this docs for testing with koin: https://insert-koin.io/docs/reference/koin-test/testing/

@emanuelecastelli
Copy link
Author

emanuelecastelli commented Jul 3, 2024

Have you call startKoin in your testing code? You can check out this docs for testing with koin: https://insert-koin.io/docs/reference/koin-test/testing/

Yeah, it's started correctly, in fact the error i get is

org.koin.core.error.NoBeanDefFoundException: No definition found for type 'viewmodels.LoginViewModel' and qualifier 'loginVM'. Check your Modules configuration and add missing type and/or qualifier!
	at org.koin.core.scope.Scope.throwDefinitionNotFound(Scope.kt:301)
	at org.koin.core.scope.Scope.resolveValue(Scope.kt:271)
	at org.koin.core.scope.Scope.resolveInstance(Scope.kt:233)
	at org.koin.core.scope.Scope.get(Scope.kt:212)
	at moe.tlaster.precompose.koin.KoinKt$resolveViewModel$1.invoke(Koin.kt:44)
	at moe.tlaster.precompose.koin.KoinKt$resolveViewModel$1.invoke(Koin.kt:43)
	at moe.tlaster.precompose.stateholder.StateHolder.getOrPut(StateHolder.kt:18)
....

@Tlaster
Copy link
Owner

Tlaster commented Jul 3, 2024

You might need to try koinViewModel(ILoginViewModel::class, named("loginVM")) instead of koinViewModel(LoginViewModel::class, named("loginVM"))

@emanuelecastelli
Copy link
Author

ILoginViewModel it's an interface and i can not inherit it from ViewModel as needed by koinViewModel

fun <T : ViewModel> koinViewModel(
    vmClass: KClass<T>,
//...
interface ILoginViewModel : IBaseViewModel {
    suspend fun getAppStartupCount(): Int?

    suspend fun addAppStartupCount()

    suspend fun resetAppStartupCount()
//...

Maybe i need some refactor on my side?
Any suggestion is appreciated :)

@Tlaster
Copy link
Owner

Tlaster commented Jul 3, 2024

Oops, my fault, I forgot the generic limitation of the koinViewModel.
There're some workaround I can think of:

  • Use factory<LoginViewModel> instead of factory<ILoginViewModel> in your testModule definition.
  • Make your IBaseViewModel or ILoginViewModel a abstract class and extend from ViewModel.
  • If you're totally not care about Lifecycle things, you can just get<LoginViewModel> in your testing code.

@emanuelecastelli
Copy link
Author

Oops, my fault, I forgot the generic limitation of the koinViewModel. There're some workaround I can think of:

  • Use factory<LoginViewModel> instead of factory<ILoginViewModel> in your testModule definition.
  • Make your IBaseViewModel or ILoginViewModel a abstract class and extend from ViewModel.
  • If you're totally not care about Lifecycle things, you can just get<LoginViewModel> in your testing code.

Thx for reply.
I need interface because MocKMP can mock just interfaces, so i can not use abstract classes.
The problem is in composable function rather that in test: how can i retrieve from Koin the right VM instance and having it bound to lifecycle?

@Composable
fun LoginScreen() {

//    val viewModel: ILoginViewModel = getKoinInstance() -> not bound to lifecycle
//    val viewModel:ILoginViewModel = viewModel(LoginViewModel::class) {
//        LoginViewModel()
//    }  -> same as following solution

    val viewModel: ILoginViewModel = koinViewModel(LoginViewModel::class, named("loginVM")) // not retrieved from Koin DI cause i'm using interface

Im looking for an alternative lib for mocking, maybe with more luck :)

@Tlaster
Copy link
Owner

Tlaster commented Jul 3, 2024

I starting to wonder how Koin it self will handle this use case in Android without PreCompose 🤔.
There's still a workaround, if you look into this file, which is some copy&&pasting and little editing from koin, you can make your own koinViewModel function without the ViewModel generic limitation.

@emanuelecastelli
Copy link
Author

I starting to wonder how Koin it self will handle this use case in Android without PreCompose 🤔. There's still a workaround, if you look into this file, which is some copy&&pasting and little editing from koin, you can make your own koinViewModel function without the ViewModel generic limitation.

Yeah this is the first idea that i had, but before start a pr i was looking for a pre baked solution 😀
I'll try and keep you informed 😉

@emanuelecastelli
Copy link
Author

ok @Tlaster i resolved it using this fun:

@Composable
fun <T : IBaseViewModel> resolveViewModel(
    vmClass: KClass<T>,
    stateHolder: StateHolder = checkNotNull(LocalStateHolder.current) {
        "No StateHolder was provided via LocalStateHolder"
    },
    key: String? = null,
    scope: Scope = LocalKoinScope.current,
    qualifier: Qualifier? = null,
    parameters: ParametersDefinition? = null,
): T {
    return stateHolder.getOrPut(qualifier?.value ?: key ?: vmClass.canonicalName ?: "") {
        scope.get(vmClass, qualifier, parameters)
    }
}

I don't like very much the bounding to IBaseViewModel because maybe i will need to retrieve VM of other kind, but by now i can run tests and app with correctly injected/retrieved VM.
Maybe you consider to include a fun like that in future?

In any case, thanks for the answers and for your work!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants