Managing Android Scopes
Working with the Android lifecycle
Android components are mainly managed by their lifecycle: we can't directly instantiate an Activity nor a Fragment. The system make all creation and management for us, and make callbacks on methods: onCreate, onStart...
That's why we can't describe our Activity/Fragment/Service in a Koin module. We need then to inject dependencies into properties and also respect the lifecycle: Components related to the UI parts must be released on soon as we don't need them anymore.
Then we have:
- long live components (Services, Data Repository ...) - used by several screens, never dropped
- medium live components (user sessions ...) - used by several screens, must be dropped after an amount of time
- short live components (views) - used by only one screen & must be dropped at the end of the screen
Long live components can be easily described as single
definitions. For medium and short live components we can have several approaches.
In the case of MVP architecture style, the Presenter
is a short live component to help/support the UI. The presenter must be created each time the screen is showing,
and dropped once the screen is gone.
A new Presenter is created each time
class DetailActivity : AppCompatActivity() {
// injected Presenter
override val presenter : Presenter by inject()
We can describe it in a module:
- as
factory
- to produce a new instance each time theby inject()
orget()
is called
val androidModule = module {
// Factory instance of Presenter
factory { Presenter() }
}
- as
scope
- to produce an instance tied to a scope
val androidModule = module {
scope<DetailActivity> {
scoped { Presenter() }
}
}
Most of Android memory leaks come from referencing a UI/Android component from a non Android component. The system keeps a reference on it and can't totally drop it via garbage collection.
Scope for Android Components (since 3.2.1)
Declare an Android Scope
To scope dependencies on an Android component, you have to declare a scope section with the scope
block like follow:
class MyPresenter()
class MyAdapter(val presenter : MyPresenter)
module {
// Declare scope for MyActivity
scope<MyActivity> {
// get MyPresenter instance from current scope
scoped { MyAdapter(get()) }
scoped { MyPresenter() }
}
}
Android Scope Classes
Koin offers ScopeActivity
, RetainedScopeActivity
and ScopeFragment
classes to let you use directly a declared scope for Activity or Fragment:
class MyActivity : ScopeActivity() {
// MyPresenter is resolved from MyActivity's scope
val presenter : MyPresenter by inject()
}
Under the hood, Android scopes needs to be used with AndroidScopeComponent
interface to implement scope
field like this:
abstract class ScopeActivity(
@LayoutRes contentLayoutId: Int = 0,
) : AppCompatActivity(contentLayoutId), AndroidScopeComponent {
override val scope: Scope by activityScope()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
checkNotNull(scope)
}
}
We need to use the AndroidScopeComponent
interface and implement the scope
property. This will setting up the default scope used by your class.
Android Scope API
To create a Koin scope bound to an Android component, just use the following functions:
createActivityScope()
- Create Scope for current Activity (scope section must be declared)createActivityRetainedScope()
- Create a retained Scope (backed by ViewModel lifecycle) for current Activity (scope section must be declared)createFragmentScope()
- Create Scope for current Fragment and link to parent Activity scope
Those functions are available as delegate, to implement different kind of scope:
activityScope()
- Create Scope for current Activity (scope section must be declared)activityRetainedScope()
- Create a retained Scope (backed by ViewModel lifecycle) for current Activity (scope section must be declared)fragmentScope()
- Create Scope for current Fragment and link to parent Activity scope
class MyActivity() : AppCompatActivity(contentLayoutId), AndroidScopeComponent {
override val scope: Scope by activityScope()
}
We can also to setting up a retained scope (backed by a ViewModel lifecycle) with the following:
class MyActivity() : AppCompatActivity(contentLayoutId), AndroidScopeComponent {
override val scope: Scope by activityRetainedScope()
}
If you don't want to use Android Scope classes, you can work with your own and use AndroidScopeComponent
with the Scope creation API
AndroidScopeComponent and Scope closing handling
You can run some code before Koin Scope is being destroyed, by overriding onCloseScope
function from AndroidScopeComponent
:
class MyActivity() : AppCompatActivity(contentLayoutId), AndroidScopeComponent {
override val scope: Scope by activityScope()
override fun onCloseScope() {
// Called before closing the Scope
}
}
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.
:::warn ViewModel can't access to Activity or Fragment scope. Why? Because ViewModel is lasting long than Activity and Fragment, and then it would leak dependencies outside of proper scopes. :::
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: viewModel { p -> }
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()
:
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.
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:
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:
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.
In a more extended usage, you can use a Scope
instance across components. For example, if we need to share a UserSession
instance.
First declare a scope definition:
module {
// Shared user session data
scope(named("session")) {
scoped { UserSession() }
}
}
When needed to begin use a UserSession
instance, create a scope for it:
val ourSession = getKoin().createScope("ourSession",named("session"))
// link ourSession scope to current `scope`, from ScopeActivity or ScopeFragment
scope.linkTo(ourSession)
Then use it anywhere you need it:
class MyActivity1 : ScopeActivity() {
fun reuseSession(){
val ourSession = getKoin().createScope("ourSession",named("session"))
// link ourSession scope to current `scope`, from ScopeActivity or ScopeFragment
scope.linkTo(ourSession)
// will look at MyActivity1's Scope + ourSession scope to resolve
val userSession = get<UserSession>()
}
}
class MyActivity2 : ScopeActivity() {
fun reuseSession(){
val ourSession = getKoin().createScope("ourSession",named("session"))
// link ourSession scope to current `scope`, from ScopeActivity or ScopeFragment
scope.linkTo(ourSession)
// will look at MyActivity2's Scope + ourSession scope to resolve
val userSession = get<UserSession>()
}
}