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

SingleOf does not generate truly singleton objects #2078

Open
ksharma-xyz opened this issue Dec 15, 2024 · 7 comments
Open

SingleOf does not generate truly singleton objects #2078

ksharma-xyz opened this issue Dec 15, 2024 · 7 comments
Labels
question Usage question

Comments

@ksharma-xyz
Copy link

ksharma-xyz commented Dec 15, 2024

Describe the bug
When navigating between screens (ScreenA to ScreenB) that use ViewModelA and ViewModelB, and then switching between dark/light themes, the Repo object injected into ViewModelB is recreated. This behavior occurs despite defining Repo as a singleton in the Koin module. As a result, ViewModelB does not share the same Repo instance as ViewModelA, which breaks the expected singleton behavior. Based on compose multiplatform project.

To Reproduce
Steps to reproduce the behavior:

  • Create a module with a interface and it's implementation.
val repoModule = module {
    singleOf(::RealRepo) { bind<Repo>() }
}
  • Create two Screen composables and ViewModels and inject the repo in the constructor
    ScreenA / ViewModel A and ScreenB / ViewModel B
class ViewModelA(
    private val repo: Repo,
) : ViewModel() { ...  }

class ViewModelB(
    private val repo: Repo,
) : ViewModel() {  ... }
  • Navigate from Screen A to B and then switch between dark / light theme.
  • Navigate Back to screen A and then again to Screen B.
  • See Error: Notice that ViewModelB has a new repo object created.

Expected behavior
ViewModelB should have the same Repo object as is present inside ViewModelA

Koin module and version:

koin = "4.0.1-Beta1"
io.insert-koin:koin-android
io.insert-koin:koin-compose-viewmodel
io.insert-koin:koin-compose-viewmodel-navigation

Snippet or Sample project to help reproduce
Add a snippet or even a small sample project to help reproduce your case.

Creating viewmodel like this in composable scope.

internal fun NavGraphBuilder.myDestination(navController: NavHostController) {
    composable<ARoute> { backStackEntry ->
        val route = backStackEntry.toRoute<ARoute>()
        val viewModel: ViewModelA = koinViewModel<ViewModelA>()
        ScreenA()
    }
}


internal fun NavGraphBuilder.myDestination(navController: NavHostController) {
    composable<BRoute> { backStackEntry ->
        val route = backStackEntry.toRoute<BRoute>()
        val viewModel: ViewModelB = koinViewModel<ViewModelB>()
        ScreenB()
    }
}


interface Repo {
    fun do()
}

class RealRepo : Repo {

    init {
        println("Repo created $this")
    }
@arnaudgiuliani arnaudgiuliani added the question Usage question label Dec 17, 2024
@arnaudgiuliani
Copy link
Member

I don't think it's related to Koin here. How do you restart your app/them?

@ksharma-xyz
Copy link
Author

ksharma-xyz commented Dec 18, 2024

In Hilt, if we mark a class as Singleton, then the instance is same as long as the app process is active. It should be same for Koin too? If a configuration change happens, then a new object should not be created.

  • Using Koin, the Singleton object is created again when a config change happens.
  • Using Hilt, the singleton object truly remains singleton, when config change happens.
Koin Hilt
Koin_Singleton_NewObjects.mov
Hilt_Singleton.mov

I've created a sample android project to highlight this here: https://github.com/ksharma-xyz/KoinSingletonBug

  • main branch - sample using koin
  • hilt branch - sample using hilt

@afTrolle
Copy link

afTrolle commented Dec 25, 2024

You are having scope issues, you are creating a koin instance too the activity scope. That's why it's recreated on configuration changes.

Start your koin instance in the Application class instead.

Change MainApplication & KoinApplication.

class MainApplication : Application() {

    override fun onCreate() {
        super.onCreate()

        startKoin {
            modules(
                repoModule,
                viewModelsModule,
            )
            androidContext(this@MainApplication)
        }
    }

} 

And replace with KoinApplication with KoinAndroidContext

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            val navController = rememberNavController()

            KoinAndroidContext {
            ...
            }
        ...
}

See Docs here
https://insert-koin.io/docs/reference/koin-android/get-instances#using-the-android-context-in-a-definition

ksharma-xyz added a commit to ksharma-xyz/KoinSingletonBug that referenced this issue Dec 25, 2024
@ksharma-xyz
Copy link
Author

Thanks! @afTrolle That worked for an android application. Do you have any suggestions for a ComposeMultiplatform Application?

Seems like setting android context in MainApplication using startKoin is not enough, we also need to set it in the org.koin.compose.KoinApplication. Otherwise, the following error occurs:

java.lang.IllegalStateException: Koin context has not been initialized in rememberKoinApplication

startKoin {
    androidContext(this@KrailApplication) // not enough as KoinApplication also expects androidContext.
}

@afTrolle
Copy link

Thanks! @afTrolle That worked for an android application. Do you have any suggestions for a ComposeMultiplatform Application?

Seems like setting android context in MainApplication using startKoin is not enough, we also need to set it in the org.koin.compose.KoinApplication. Otherwise, the following error occurs:

java.lang.IllegalStateException: Koin context has not been initialized in rememberKoinApplication

startKoin {

    androidContext(this@KrailApplication) // not enough as KoinApplication also expects androidContext.

}

It might depend on which platform/target we are talking about.

I'm no expert in koin.

But you could probably use previous solution for the others.
Just do a check if Koin is instantiated already otherwise use other solution.

In my iOS code I do similar to android application but call function init function when app starts. But that was just convient because I'm also starting up crashlytics from swift.

@ksharma-xyz
Copy link
Author

Any updates on this @arnaudgiuliani. The issue is for Compose Multiplatform.

@arnaudgiuliani
Copy link
Member

If you want to start Koin before Compose, you need to use KoinAndroidContext or KoinContext instead of KoinApplication. Else it will try to start a new context.

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

No branches or pull requests

3 participants