Skip to main content
Version: 4.2

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 TypeLifetimeSurvives Config ChangeUse CaseDSL
ApplicationEntire app✅ YesSingletons, repositories, managerssingle { }
ActivityActivity lifecycle❌ NoActivity-specific state, shared across fragmentsactivityScope { }
Activity RetainedUntil Activity finish✅ Yes (ViewModel-backed)State that survives rotationactivityRetainedScope { }
FragmentFragment lifecycle❌ NoFragment-specific statefragmentScope { }
ViewModelViewModel lifecycle✅ YesViewModel dependenciesviewModelScope { } or scope<MyViewModel> { }
CustomManual controlDepends on implSession, user statescope(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)
info

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 the by inject() or get() 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() }
}
}
note

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()
}
note

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
}
}
note

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.

info

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.1.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.

danger

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 need to bridge a dependency from outside a ViewModel scope, you can use "injected parameters" to pass some objects to your ViewModel: viewModel { p -> }

Declare your ViewModel scope as follows, tied to your ViewModel class or using the viewModelScope DSL section:

module {
viewModelOf(::MyScopeViewModel)
// scope for MyScopeViewModel only
scope<MyScopeViewModel> {
scopedOf(::Session)
}
// ViewModel Archetype scope - Scope for all ViewModel
viewModelScope {
scopedOf(::Session)
}
}

Once you have declared your ViewModel and your scoped components, you can choose between:

  • Manual API - Manually using the KoinScopeComponent and the viewModelScope function. This will handle the creation and destruction of your created ViewModel scope. But you will have to inject your scoped definitions by field, as you need to rely on scope property to inject your scoped definition:
class MyScopeViewModel : ViewModel(), KoinScopeComponent {

// create ViewModel scope
override val scope: Scope = viewModelScope()

// uses scope above to inject session
val session: Session by inject()
}
  • Automatic Scope Creation
    • Activate the viewModelScopeFactory option (see Koin Options) to automatically create a ViewModel scope on the fly.
    • This allows to use of constructor injection
// activate ViewModel Scope factory
startKoin {
options(
viewModelScopeFactory()
)
}

// Scope being created at factory level, automatically before injection
class MyScopeViewModel(val session: Session) : ViewModel()

Now just call your ViewModel from your Activity or Fragment:

class MyActivity : AppCompatActivity() {

// create MyScopeViewModel instance, and allocate MyScopeViewModel's scope
val vieModel: MyScopeViewModel by viewModel()
}

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()
}
}
danger

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

EventActivity ScopeActivity Retained Scope
onCreate()✅ Created✅ Created
Rotation starts❌ DestroyedSurvives
New Activity onCreate()✅ New scope createdSame 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() }
}
}
info

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)
}
}