Android Scopes
This guide covers Android-specific scope implementations and patterns. For core scope concepts (scope lifecycle, scope linking, KoinScopeComponent), see Scopes.
Overview
Scopes in Koin allow you to manage the lifecycle of your dependencies to match Android component lifecycles. This prevents memory leaks and ensures proper resource management.
Scope Hierarchy & Lifetime
| Scope Type | Lifetime | Survives Config Change | Use Case | DSL |
|---|---|---|---|---|
| Application | Entire app | ✅ Yes | Singletons, repositories, managers | single { } |
| Activity | Activity lifecycle | ❌ No | Activity-specific state, shared across fragments | activityScope { } |
| Activity Retained | Until Activity finish | ✅ Yes (ViewModel-backed) | State that survives rotation | activityRetainedScope { } |
| Fragment | Fragment lifecycle | ❌ No | Fragment-specific state | fragmentScope { } |
| ViewModel | ViewModel lifecycle | ✅ Yes | ViewModel dependencies | viewModelScope { } or scope<MyViewModel> { } |
| Custom | Manual control | Depends on impl | Session, user state | scope(named("name")) { } |
Scope Relationships
Application Scope (single { })
└── Activity Retained Scope (survives rotation)
└── Activity Scope
├── Fragment Scope 1
├── Fragment Scope 2
└── Fragment Scope 3
└── ViewModel Scope (can't access Activity/Fragment scope)
Key Principle: Child scopes can access parent scope definitions, but not vice versa. This prevents memory leaks and ensures proper lifecycle management.
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() }
}
// or
activityScope {
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 handling Scope closing
You can run code before Koin Scope is destroyed, by overriding the 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, the scope will already be closed.
Scope Archetypes (4.1.0)
As a new feature, you can now declare scope by archetype: you don't need to define a scope against a specific type, but for an "archetype" (a kind of scope class). You can declare a scope for "Activity", "Fragment", or "ViewModel". You can now use the following DSL sections:
module {
activityScope {
// scoped instances for an activity
}
activityRetainedScope {
// scoped instances for an activity, retained scope
}
fragmentScope {
// scoped instances for Fragment
}
viewModelScope {
// scoped instances for ViewModel
}
}
This allows for better reuse of definitions between scopes easily. No need to use a specific type like scope<>{ }, apart from if you need scope on a precise object.
See Android Scope API to see how to use by activityScope(), by activityRetainedScope(), and by fragmentScope() functions to activate your Android scope. Those functions will trigger scope archetypes.
For example, you can easily scope a defintion to an activity like that, with Scope Archetypes:
// declare Class Session in Activity scope
module {
activityScope {
scopedOf(::Session)
}
}
// Inject the scoped Session object to the activity:
class MyActivity : AppCompatActivity(), AndroidScopeComponent {
// create Activity's scope
val scope: Scope by activityScope()
// inject from scope above
val session: Session by inject()
}
ViewModel Scope (updated in 4.2.0)
ViewModel is only created against the root scope to avoid any leaking (leaking Activity or Fragment ...). This guards for the visibility problem, where the ViewModel could have access to incompatible scopes.
ViewModel can't access Activity or Fragment scope. Why? Because ViewModel outlives Activity and Fragment, and would leak dependencies outside of proper scopes.
If you need to pass data from Activity/Fragment to ViewModel, use "injected parameters": viewModel { params -> }
When to Use ViewModel Scope
Use ViewModel Scope when your ViewModel needs scoped dependencies that are tied to its lifecycle. Common use cases:
- Session data specific to a ViewModel
- Caches that should be cleared when ViewModel is destroyed
- Coordinators or use cases that should live as long as the ViewModel
Declaring ViewModel Scope
You can declare a ViewModel scope tied to a specific ViewModel class or using the viewModelScope archetype:
module {
// Option 1: Scope for a specific ViewModel class
scope<MyScopeViewModel> {
scopedOf(::Session)
}
// Option 2: ViewModel Archetype scope - shared by all ViewModels
viewModelScope {
scopedOf(::Session)
}
}
Manual Scope API
Use KoinScopeComponent and viewModelScope() function to manually manage the scope. Dependencies must be injected by field:
module {
viewModelOf(::MyScopeViewModel)
scope<MyScopeViewModel> {
scopedOf(::Session)
}
}
class MyScopeViewModel : ViewModel(), KoinScopeComponent {
// Create ViewModel scope - handles creation and destruction
override val scope: Scope = viewModelScope()
// Inject from scope above
val session: Session by inject()
}
Automatic Scope with Constructor Injection (Experimental)
For constructor injection of scoped dependencies, use the viewModelScopeFactory option.
This feature is experimental and requires opt-in via @OptIn(KoinViewModelScopeApi::class).
Important: Your ViewModel must be declared inside the viewModelScope { } block for constructor injection to work.
Step 1: Enable the option
@OptIn(KoinViewModelScopeApi::class)
startKoin {
options(
viewModelScopeFactory()
)
}
Step 2: Declare ViewModel inside viewModelScope block
@OptIn(KoinViewModelScopeApi::class)
module {
// ✅ Correct: ViewModel declared INSIDE viewModelScope
viewModelScope {
viewModel { MyScopeViewModel(get()) } // or viewModelOf(::MyScopeViewModel)
scoped { Session() }
}
}
// ViewModel with constructor injection
class MyScopeViewModel(val session: Session) : ViewModel()
Declaring the ViewModel outside the viewModelScope block will not work for constructor injection:
// ❌ Wrong: ViewModel outside viewModelScope - Session won't be found!
module {
viewModelOf(::MyScopeViewModel) // Declared at root scope
viewModelScope {
scoped { Session() } // Session is in ViewModel scope
}
}
The ViewModel is resolved from root scope and cannot see the scoped Session inside viewModelScope.
Step 3: Inject ViewModel
class MyActivity : AppCompatActivity() {
// Creates MyScopeViewModel and its scope
val viewModel: MyScopeViewModel by viewModel()
}
How It Works
When viewModelScopeFactory() is enabled and a ViewModel is requested:
- Koin creates a dedicated scope for the ViewModel
- Dependencies declared in
viewModelScope { }are resolved from this scope - The scope is automatically closed when the ViewModel is cleared
Root Scope
└── ViewModel Scope (created per ViewModel instance)
└── scoped { Session() } ← Available for constructor injection
Summary: Which Approach to Use?
| Approach | Constructor Injection | Field Injection | Opt-in Required |
|---|---|---|---|
Manual (KoinScopeComponent) | ❌ No | ✅ Yes | No |
Automatic (viewModelScopeFactory) | ✅ Yes | ✅ Yes | Yes (@KoinViewModelScopeApi) |
Scope Links
Scope links allow sharing instances between components with custom scopes. By default, Fragment's scope are linked to parent Activity scope.
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>()
}
}
## Activity Scope - Expanded Examples
### Sharing State Between Fragments
A common pattern is sharing state between multiple fragments in an Activity using Activity scope:
```kotlin
// Shared state for checkout flow
class CheckoutState {
var selectedShippingAddress: Address? = null
var selectedPaymentMethod: PaymentMethod? = null
var orderItems: List<CartItem> = emptyList()
}
// Define in Activity scope
module {
activityScope {
scoped { CheckoutState() }
scoped { CheckoutViewModel(get()) }
}
}
// Activity manages the flow
class CheckoutActivity : AppCompatActivity(), AndroidScopeComponent {
override val scope: Scope by activityScope()
// Shared state accessible by all fragments
private val checkoutState: CheckoutState by inject()
fun navigateToPayment() {
// State is preserved across fragment transactions
supportFragmentManager.commit {
replace(R.id.container, PaymentFragment())
}
}
}
// First fragment in the flow
class ShippingFragment : Fragment() {
// Gets the same CheckoutState instance as Activity
private val checkoutState: CheckoutState by inject()
fun onAddressSelected(address: Address) {
checkoutState.selectedShippingAddress = address
// Navigate to payment - state is preserved
(activity as CheckoutActivity).navigateToPayment()
}
}
// Second fragment can access the same state
class PaymentFragment : Fragment() {
// Same CheckoutState instance shared across fragments
private val checkoutState: CheckoutState by inject()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// Access data from previous fragment
val shippingAddress = checkoutState.selectedShippingAddress
// Continue checkout flow...
}
}
Activity Scope Lifecycle
Activity scopes are automatically created and destroyed with the Activity lifecycle:
class DetailActivity : ScopeActivity() {
override val scope: Scope by activityScope()
private val presenter: DetailPresenter by inject()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// scope is created before onCreate
// presenter is injected from the scope
}
override fun onDestroy() {
// scope.close() is called automatically after onDestroy
super.onDestroy()
}
}
Configuration Changes: Activity scope does NOT survive configuration changes (rotation, theme change, etc.). All scoped instances are destroyed and recreated. Use activityRetainedScope() if you need to preserve state across configuration changes.
Fragment Scope - Expanded Examples
Fragment-Specific State Management
Fragment scopes are linked to their parent Activity scope, allowing access to both fragment-specific and activity-shared dependencies:
// Module with both Activity and Fragment scopes
module {
// Shared across all fragments
activityScope {
scoped { ShoppingCart() }
}
// Specific to each fragment instance
fragmentScope {
scoped { ProductListPresenter(get()) }
scoped { ProductListState() }
}
}
class ProductListFragment : Fragment(), AndroidScopeComponent {
override val scope: Scope by fragmentScope()
// From fragment scope - unique to this fragment
private val presenter: ProductListPresenter by inject()
private val listState: ProductListState by inject()
// From parent activity scope - shared with other fragments
private val shoppingCart: ShoppingCart by inject()
fun onProductClicked(product: Product) {
// Fragment-specific state
listState.lastClickedProduct = product
// Shared state with other fragments
shoppingCart.addItem(product)
}
}
Fragment Scope Hierarchy
class ProductActivity : ScopeActivity() {
override val scope: Scope by activityScope()
}
class ProductListFragment : Fragment(), AndroidScopeComponent {
override val scope: Scope by fragmentScope()
// Can access both fragment scope and parent activity scope
// Resolution order: Fragment scope → Activity scope → Application scope
}
class ProductDetailFragment : Fragment(), AndroidScopeComponent {
override val scope: Scope by fragmentScope()
// Different fragment scope, but same parent activity scope
// Each fragment has its own isolated scope for fragment-specific dependencies
}
Activity Retained Scope - Deep Dive
Activity Retained Scope survives configuration changes (rotation, theme change) using ViewModel lifecycle backing.
How It Works
class MyActivity : RetainedScopeActivity() {
// Scope backed by ViewModel lifecycle - survives rotation
override val scope: Scope by activityRetainedScope()
private val repository: UserRepository by inject()
private val cache: ImageCache by inject()
}
Lifecycle Comparison
| Event | Activity Scope | Activity Retained Scope |
|---|---|---|
| onCreate() | ✅ Created | ✅ Created |
| Rotation starts | ❌ Destroyed | ✅ Survives |
| New Activity onCreate() | ✅ New scope created | ✅ Same scope |
| Activity finish() | ❌ Destroyed | ❌ Destroyed |
When to Use Retained Scope
module {
// Use retained scope for:
activityRetainedScope {
// Network requests that should continue during rotation
scoped { PendingRequestsManager() }
// Cached data that's expensive to reload
scoped { ImageCache() }
// User input state
scoped { FormState() }
}
// Regular activity scope for:
activityScope {
// UI-specific dependencies
scoped { DialogManager(get()) }
// Short-lived presenters
scoped { ScreenPresenter() }
}
}
Under the Hood: activityRetainedScope() creates a scope tied to a ViewModel's lifecycle. Since ViewModels survive configuration changes, so does the scope and all its instances.
View Scope
For custom views that need scoped dependencies:
// Define custom view scope
module {
scope(named("ChartView")) {
scoped { ChartDataProcessor() }
scoped { ChartRenderer() }
}
}
class ChartView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : View(context, attrs), KoinScopeComponent {
override val scope: Scope by lazy {
createScope(this, named("ChartView"))
}
private val dataProcessor: ChartDataProcessor by inject()
private val renderer: ChartRenderer by inject()
fun setData(data: List<DataPoint>) {
val processed = dataProcessor.process(data)
renderer.render(this, processed)
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
scope.close()
}
}
Service Scope
Services can use scoped dependencies tied to the service lifecycle:
// Define service scope
module {
scope<MusicPlayerService> {
scoped { PlaybackEngine() }
scoped { MediaNotificationManager() }
}
}
class MusicPlayerService : Service(), AndroidScopeComponent {
override val scope: Scope by lazy {
createScope<MusicPlayerService>()
}
private val playbackEngine: PlaybackEngine by inject()
private val notificationManager: MediaNotificationManager by inject()
override fun onBind(intent: Intent?): IBinder? {
// Scope is active throughout service lifecycle
return null
}
override fun onDestroy() {
super.onDestroy()
scope.close()
}
}
Best Practices
Choosing the Right Scope
// Decision tree:
// 1. Does it need to survive the entire app lifecycle?
single { Database() } // → Use Application scope
// 2. Does it need to survive configuration changes?
activityRetainedScope {
scoped { NetworkRequestManager() } // → Use Activity Retained Scope
}
// 3. Is it shared between fragments in an Activity?
activityScope {
scoped { SharedActivityState() } // → Use Activity Scope
}
// 4. Is it specific to a single Fragment?
fragmentScope {
scoped { FragmentPresenter() } // → Use Fragment Scope
}
// 5. Does it need custom lifecycle control?
scope(named("session")) {
scoped { UserSession() } // → Use Custom Scope
}
Memory Management
module {
// ✅ Good - Repository doesn't hold Activity reference
activityScope {
scoped { ScreenPresenter(get<UserRepository>()) }
}
single { UserRepository(get()) }
// ❌ Bad - Activity-scoped dependency leaking into singleton
single {
// Don't do this! Activity will leak
LeakyPresenter(get<Activity>())
}
}
Common Pitfalls
1. Forgetting to Close Custom Scopes
// ❌ Bad - scope is never closed, memory leak!
class UserSessionManager {
val sessionScope = getKoin().createScope("session", named("session"))
fun startSession() {
val userSession = sessionScope.get<UserSession>()
// ... scope is never closed
}
}
// ✅ Good - scope is properly closed
class UserSessionManager {
private var sessionScope: Scope? = null
fun startSession() {
sessionScope = getKoin().createScope("session", named("session"))
}
fun endSession() {
sessionScope?.close()
sessionScope = null
}
}
2. Accessing Scope After Closure
// ❌ Bad - accessing scope in onDestroy
class MyActivity : ScopeActivity() {
override val scope: Scope by activityScope()
override fun onDestroy() {
super.onDestroy() // scope.close() called here
// ❌ Error! Scope is already closed
val presenter = get<Presenter>()
}
}
// ✅ Good - use onCloseScope() hook
class MyActivity : ScopeActivity() {
override val scope: Scope by activityScope()
override fun onCloseScope() {
// Called BEFORE scope.close()
val presenter = get<Presenter>()
presenter.cleanup()
}
}
3. Wrong Scope Type for Use Case
// ❌ Bad - ViewModel in Activity scope (lost on rotation)
module {
activityScope {
scoped { MyViewModel() } // ViewModel should survive rotation!
}
}
// ✅ Good - ViewModel in retained scope or ViewModel scope
module {
activityRetainedScope {
scoped { MyViewModel() }
}
// OR
viewModelOf(::MyViewModel)
}
Scope Testing
class MyActivityTest {
@Test
fun `activity scope should close on destroy`() {
val scenario = ActivityScenario.launch(MyActivity::class.java)
var scopeClosed = false
scenario.onActivity { activity ->
activity.scope.registerCallback(object : ScopeCallback {
override fun onScopeClose(scope: Scope) {
scopeClosed = true
}
})
}
scenario.close() // Triggers onDestroy
assertTrue(scopeClosed)
}
}