Skip to content

Commit

Permalink
Merge pull request #1809 from InsertKoinIO/fix/viewmodel_key_scope
Browse files Browse the repository at this point in the history
Fix ViewModel key generation + Fix ViewModel scope handling
  • Loading branch information
arnaudgiuliani authored Mar 4, 2024
2 parents 7d354a5 + dc94854 commit 765c311
Show file tree
Hide file tree
Showing 17 changed files with 230 additions and 108 deletions.
72 changes: 71 additions & 1 deletion docs/reference/koin-android/scope.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ val androidModule = module {
on it and can't totally drop it via garbage collection.
:::
## Scope for Android Components (3.2.1 update)
## Scope for Android Components (since 3.2.1)
### Declare an Android Scope
Expand Down Expand Up @@ -164,6 +164,76 @@ class MyActivity() : AppCompatActivity(contentLayoutId), AndroidScopeComponent {
If you try to access Scope from `onDestroy()` function, scope will be already closed.
:::

### ViewModel Scope (since 3.5.4)

ViewModel is only created against root scope to avoid any leaking (leaking Activity or Fragment ...). This guard for the visibility problem, where ViewModel could have access to incompatible scopes.

:::note
If you ViewModel can't get access to a dependency, check in which scope it has been declared.
:::


:::note
If you _really_ need to bridge a dependency from outside a ViewModel scope, you can use "injected parameters" to pass some objects to your ViewModel.
:::

`ScopeViewModel` is a new class to help work on ViewModel scope. This handle ViewModel's scope creation, and provide `scope` property to allow inject with `by scope.inject()`:

```kotlin
module {
viewModelOf(::MyScopeViewModel)
scope<MyScopeViewModel> {
scopedOf(::Session)
}
}

class MyScopeViewModel : ScopeViewModel() {

// on onCleared, scope is closed

// injected from current MyScopeViewModel's scope
val session by scope.inject<Session>()

}
```

By using `ScopeViewModel` you can also overrode `onCloseScope()` function, to run code before scope is being closed.

:::note
All instances inside a ViewModel scope have the same visibility and will survive for lifetime of ViewModel instance, until ViewModel's onCleared function is called
:::

For example, Once an Activity or fragment has created a ViewModel, the associated scope is created:

```kotlin
class MyActivity : AppCompatActivity() {

// Create ViewModel and its scope
val myViewModel by viewModel<MyScopeViewModel>()

}
```

Once your ViewModel is created, all associated dependencies from within this scope can be created and injected.

To implement manually your ViewModel scope without `ScopeViewModel` class proceed as follow:

```kotlin
class MyScopeViewModel : ViewModel(), KoinScopeComponent {

override val scope: Scope = createScope(this)

// inject your dependency
val session by scope.inject<Session>()

// clear scope
override fun onCleared() {
super.onCleared()
scope.close()
}
}
```

## Scope Links

Scope links allow to share instances between components with custom scopes.
Expand Down
8 changes: 8 additions & 0 deletions docs/reference/koin-android/viewmodel.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ class DetailActivity : AppCompatActivity() {
}
```

:::note
ViewModel key is calculated against Key and/or Qualifier
:::

## Activity Shared ViewModel

One ViewModel instance can be shared between Fragments and their host Activity.
Expand Down Expand Up @@ -175,6 +179,10 @@ class NavFragment : Fragment() {
}
```

## ViewModel Scope API

see all API to be used for ViewModel and Scopes: [ViewModel Scope](/docs/reference/koin-android/scope.md#viewmodel-scope-since-354)

## ViewModel Generic API

Koin provides some "under the hood" API to directly tweak your ViewModel instance. The available functions are `viewModelForClass` for `ComponentActivity` and `Fragment`:
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.koin.sample.sandbox.components.mvvm

import androidx.lifecycle.ViewModel
import org.koin.core.component.KoinScopeComponent
import org.koin.core.component.createScope
import org.koin.core.scope.Scope
import org.koin.sample.sandbox.components.scope.Session

class MyScopeViewModel : ViewModel(), KoinScopeComponent {

override val scope: Scope = createScope(this)

val session by scope.inject<Session>()

override fun onCleared() {
super.onCleared()
scope.close()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.koin.sample.sandbox.components.mvvm

import org.koin.androidx.scope.ScopeViewModel
import org.koin.core.annotation.KoinExperimentalAPI
import org.koin.sample.sandbox.components.scope.Session

@OptIn(KoinExperimentalAPI::class)
class MyScopeViewModel2 : ScopeViewModel() {

val session by scope.inject<Session>()

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ class SavedStateViewModel(val handle: SavedStateHandle, val id: String, val serv
init {
val get = handle.get<String>(id)
println("handle: $get")
handle.set(id,UUID.randomUUID().toString())
handle[id] = UUID.randomUUID().toString()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ class Session {
var id: String = UUID.randomUUID().toString()
}

class SessionConsumer(private val session: Session){

fun getSessionId() = session.id

}

class SessionActivity {
val id: String = UUID.randomUUID().toString()
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import org.koin.core.module.dsl.*
import org.koin.core.module.includes
import org.koin.core.qualifier.named
import org.koin.dsl.lazyModule
import org.koin.dsl.module
import org.koin.sample.sandbox.components.Counter
import org.koin.sample.sandbox.components.SCOPE_ID
import org.koin.sample.sandbox.components.SCOPE_SESSION
Expand All @@ -21,6 +20,7 @@ import org.koin.sample.sandbox.components.mvp.ScopedPresenter
import org.koin.sample.sandbox.components.mvvm.*
import org.koin.sample.sandbox.components.scope.Session
import org.koin.sample.sandbox.components.scope.SessionActivity
import org.koin.sample.sandbox.components.scope.SessionConsumer
import org.koin.sample.sandbox.mvp.MVPActivity
import org.koin.sample.sandbox.mvvm.MVVMActivity
import org.koin.sample.sandbox.mvvm.MVVMFragment
Expand All @@ -45,13 +45,13 @@ val mvpModule = lazyModule {

scope<MVPActivity> {
scopedOf(::ScopedPresenter)// { (id: String) -> ScopedPresenter(id, get()) }

}
}

val mvvmModule = lazyModule {

viewModelOf(::SimpleViewModel)// { (id: String) -> SimpleViewModel(id, get()) }

viewModelOf(::SimpleViewModel) { named("vm1") } //{ (id: String) -> SimpleViewModel(id, get()) }
viewModel(named("vm2")) { (id: String) -> SimpleViewModel(id, get()) }

Expand All @@ -61,22 +61,30 @@ val mvvmModule = lazyModule {
// viewModel<AbstractViewModel> { ViewModelImpl(get()) }
viewModelOf(::ViewModelImpl) { bind<AbstractViewModel>() }

scope<MVVMActivity> {
viewModelOf(::MyScopeViewModel)
scope<MyScopeViewModel> {
scopedOf(::Session)
}

viewModelOf(::MyScopeViewModel2)
scope<MyScopeViewModel2> {
scopedOf(::Session)
scopedOf(::SessionConsumer)
}

viewModelOf(::SavedStateViewModel) { named("vm2") }

scope<MVVMActivity> {
scopedOf(::Session)
fragmentOf(::MVVMFragment) // { MVVMFragment(get()) }
viewModelOf(::ExtSimpleViewModel)
viewModelOf(::ExtSimpleViewModel) { named("ext") }
viewModelOf(::SavedStateViewModel) { named("vm2") }

scoped { MVVMPresenter1(get()) }
scoped { MVVMPresenter2(get()) }
}
scope<MVVMFragment> {
scoped { (id: String) -> ScopedPresenter(id, get()) }
// to retrieve from parent
// scopedOf(::Session)
viewModelOf(::ExtSimpleViewModel)
viewModelOf(::ExtSimpleViewModel) { named("ext") }
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,35 +8,28 @@ import org.koin.android.ext.android.inject
import org.koin.androidx.fragment.android.replace
import org.koin.androidx.fragment.android.setupKoinFragmentFactory
import org.koin.androidx.scope.ScopeActivity
import org.koin.androidx.viewmodel.ext.android.getViewModel
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.androidx.viewmodel.ext.android.viewModelForClass
import org.koin.core.parameter.parametersOf
import org.koin.core.qualifier.named
import org.koin.sample.sandbox.R
import org.koin.sample.sandbox.components.ID
import org.koin.sample.sandbox.components.mvp.FactoryPresenter
import org.koin.sample.sandbox.components.mvvm.*
import org.koin.sample.sandbox.components.scope.Session
import org.koin.sample.sandbox.components.scope.SessionConsumer
import org.koin.sample.sandbox.scope.ScopedActivityA
import org.koin.sample.sandbox.utils.navigateTo

class MVVMActivity : ScopeActivity(contentLayoutId = R.layout.mvvm_activity) {

lateinit var simpleViewModel: SimpleViewModel //by viewModel { parametersOf(ID) }

val vm: SimpleViewModel by viewModel { parametersOf("vm") }
val vm1: SimpleViewModel by viewModel(named("vm1")) { parametersOf("vm1") }
val vm2: SimpleViewModel by viewModel(named("vm2")) { parametersOf("vm2") }

val scopeVm: ExtSimpleViewModel by viewModel()
val extScopeVm: ExtSimpleViewModel by viewModel(named("ext"))

// val savedVm: SavedStateViewModel by stateViewModel { parametersOf("vm1") }
val savedVm: SavedStateViewModel by viewModel { parametersOf("vm1") }
val scopeVm1: MyScopeViewModel by viewModel()
val scopeVm2: MyScopeViewModel2 by viewModel()

// val state = Bundle().apply { putString("id", "vm1") }
// val stateVM: SavedStateBundleViewModel by stateViewModel(state = { state })
val stateVM: SavedStateBundleViewModel by viewModel()
val savedVm: SavedStateViewModel by viewModel { parametersOf("vm1") }

val abstractVM : AbstractViewModel by viewModel()

Expand All @@ -60,8 +53,6 @@ class MVVMActivity : ScopeActivity(contentLayoutId = R.layout.mvvm_activity) {
navigateTo<ScopedActivityA>(isRoot = true)
}

// simpleViewModel = viewModelForClass(SimpleViewModel::class, owner = this).value

checks()
}

Expand All @@ -77,14 +68,18 @@ class MVVMActivity : ScopeActivity(contentLayoutId = R.layout.mvvm_activity) {

private fun checks() {
assert(abstractVM is ViewModelImpl)
assert(scopeVm.session.id == extScopeVm.session.id)
assert(stateVM.result == "vm1")
assert(vm1.id != vm.id)
assert(vm1.id != vm2.id)

val p1 = scope.get<MVVMPresenter1>()
val p2 = scope.get<MVVMPresenter2>()

assert(p1.ctx == this)
assert(p2.ctx == getKoin().get<Application>())

assert(scopeVm1.session.id == scopeVm1.scope.get<Session>().id)
assert(scopeVm2.session.id == scopeVm2.scope.get<Session>().id)
assert(scopeVm2.scope.get<SessionConsumer>().getSessionId() == scopeVm2.scope.get<Session>().id)
assert(scopeVm1.session.id != scopeVm2.session.id)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,10 @@ import org.koin.androidx.scope.requireScopeActivity
import org.koin.androidx.viewmodel.ext.android.activityViewModel
import org.koin.androidx.viewmodel.ext.android.getActivityViewModel
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.androidx.viewmodel.ext.android.viewModelForClass
import org.koin.core.parameter.parametersOf
import org.koin.core.qualifier.named
import org.koin.core.scope.Scope
import org.koin.sample.sandbox.R
import org.koin.sample.sandbox.components.ID
import org.koin.sample.sandbox.components.mvvm.ExtSimpleViewModel
import org.koin.sample.sandbox.components.mvvm.SavedStateViewModel
import org.koin.sample.sandbox.components.mvvm.SimpleViewModel
import org.koin.sample.sandbox.components.scope.Session
Expand All @@ -27,14 +24,8 @@ class MVVMFragment(private val session: Session) : Fragment(R.layout.mvvm_fragme
override val scope: Scope by fragmentScope()

val simpleViewModel: SimpleViewModel by viewModel { parametersOf(ID) }

// Generic KClass Access
val scopeVm: ExtSimpleViewModel by viewModelForClass(ExtSimpleViewModel::class)
val extScopeVm: ExtSimpleViewModel by viewModel(named("ext"))

val shared: SimpleViewModel by activityViewModel { parametersOf(ID) }

val sharedSaved: SavedStateViewModel by activityViewModel { parametersOf(ID) }
val saved by viewModel<SavedStateViewModel> { parametersOf(ID) }
val saved2 by viewModel<SavedStateViewModel> { parametersOf(ID) }

Expand All @@ -48,13 +39,8 @@ class MVVMFragment(private val session: Session) : Fragment(R.layout.mvvm_fragme
checkNotNull(session)
assert(shared != simpleViewModel)

// TODO Handle shared isntance - out of Scope
// assert((requireActivity() as MVVMActivity).simpleViewModel == shared)
// assert((requireActivity() as MVVMActivity).savedVm == sharedSaved)

assert((requireActivity() as MVVMActivity).savedVm != saved)
assert((requireActivity() as MVVMActivity).savedVm != saved2)
assert(scopeVm.session.id == extScopeVm.session.id)


val shared2 = getActivityViewModel<SimpleViewModel> { parametersOf(ID) }
Expand Down
4 changes: 3 additions & 1 deletion examples/gradle/versions.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ ext {
// Kotlin
kotlin_version = '1.9.21'
// Koin Versions
koin_version = '3.6.0-alpha1'
koin_version = '3.5.4-RC-1'
koin_android_version = koin_version
koin_compose_version = "1.1.3-RC-1"

coroutines_version = "1.7.3"
ktor_version = "2.3.7"
// Compose
Expand Down
5 changes: 3 additions & 2 deletions examples/sample-desktop-compose/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,20 @@ plugins {

repositories {
mavenCentral()
mavenLocal()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
google()
}

val koin_version : String by project
val koin_compose_version : String by project

dependencies {
// Note, if you develop a library, you should use compose.desktop.common.
// compose.desktop.currentOs should be used in launcher-sourceSet
// (in a separate module for demo project and in testMain).
// With compose.desktop.common you will also lose @Preview functionality
implementation(compose.desktop.currentOs)
implementation("io.insert-koin:koin-compose:$koin_version")
implementation("io.insert-koin:koin-compose:$koin_compose_version")
}

compose.desktop {
Expand Down
Loading

0 comments on commit 765c311

Please sign in to comment.