Understanding Koin Scopes for Android Dependency Injection
Dependency injection is crucial for modern Android development, and Koin offers a lightweight solution. One of its most powerful but often misunderstood features is scopes - a way to manage dependencies with lifetimes shorter than your app’s lifetime.
The Problem: Sharing Objects with Custom Lifetimes
When building Android apps, we often need to share objects between components (Activities, Fragments, ViewModels) that have lifetimes between a singleton and factory:
- Too broad: Singletons live for the entire app lifetime, causing memory leaks if they hold references to Activities or Fragments
- Too narrow: Factories create new instances every time, preventing object sharing between components
- Just right: Scopes provide lifecycle-bound singletons that can be destroyed when no longer needed
Common scenarios:
- Sharing data between Fragments in the same Activity
- Passing state between screens in a flow (e.g., multi-step form, checkout process)
- Managing feature-specific dependencies that shouldn’t be singletons
Koin Scope Types
Koin provides three ways to create instances:
1. Single - App Lifetime Singleton
1 | module { |
- Created once when first requested
- Lives for the entire app lifetime
- Never destroyed until app is killed
- Use for: Database, API clients, app-wide managers
2. Factory - New Instance Every Time
1 | module { |
- Creates a new instance on every injection
- No caching or sharing
- Use for: Stateless objects, lightweight classes
3. Scoped - Destroyable Singleton
1 | module { |
- Acts like a singleton within the scope lifetime
- Multiple components can share the same instance
- Manually created and destroyed
- Use for: Feature-specific dependencies, flow state management
Key insight: Scoped instances function as singletons with the ability to be destroyed.
Basic Scope Usage
Step 1: Define a Scope in Koin Module
1 | val checkoutModule = module { |
Important details:
named("checkoutScope")creates a scope qualifier- Multiple scoped dependencies can be defined in the same scope
- Dependencies can be injected using
get()as usual
Step 2: Create the Scope
In your Activity or Fragment:
1 | class CheckoutActivity : AppCompatActivity() { |
Key points:
- First parameter: unique scope ID (can be any string)
- Second parameter: scope qualifier from module definition
- Store scope reference to close it later
Step 3: Share Scope Between Components
1 | class ShippingFragment : Fragment() { |
Both components now share the same CheckoutState instance!
Step 4: Close the Scope
Critical: Always close scopes to prevent memory leaks:
1 | class CheckoutActivity : AppCompatActivity() { |
Without closing: Scoped instances behave like singletons and never get garbage collected.
Advanced: Lifecycle-Aware Extension Functions
Manual scope management is error-prone. Create extension functions that automatically handle scope lifecycle:
Fragment Scope Extensions
1 | /** |
Activity Scope Extensions
1 | /** |
Usage with Extensions
Now scope management becomes much cleaner:
1 | class CheckoutActivity : AppCompatActivity() { |
Scope Linking
Link scopes to create dependency hierarchies:
1 | val appModule = module { |
Benefits:
- Fragment can access Activity-scoped dependencies
- Maintains proper lifecycle boundaries
- Enables parent-child dependency relationships
Practical Example: Multi-Step Checkout Flow
1 | // Define modules |
Benefits of this approach:
- All fragments share the same
CheckoutStateinstance - State persists across fragment transactions
- Everything is cleaned up when Activity is destroyed
- No manual scope management needed with extension functions
Common Pitfalls
1. Forgetting to Close Scopes
1 | // ❌ BAD: Memory leak |
2. Creating Multiple Scopes with Same ID
1 | // ❌ BAD: Will throw exception |
3. Accessing Scope After Closing
1 | // ❌ BAD: Will throw exception |
When to Use Scopes
Use scopes when:
- ✅ You need to share state between multiple components
- ✅ Components have the same or nested lifetimes
- ✅ You want automatic cleanup when flow completes
- ✅ Dependencies shouldn’t be app-wide singletons
Don’t use scopes when:
- ❌ Dependency is truly app-wide (use
singleinstead) - ❌ No sharing needed (use
factoryinstead) - ❌ Managing lifecycle is too complex (consider other patterns)
Conclusion
Koin scopes provide a powerful mechanism for managing dependencies with custom lifetimes. Key takeaways:
- Scopes = destroyable singletons - Share instances within a lifecycle boundary
- Manual management required - Must create and close scopes explicitly
- Lifecycle observers help - Use extension functions for automatic cleanup
- Scope linking enables hierarchies - Parent scopes can provide dependencies to children
- Always close scopes - Prevent memory leaks by closing scopes when done
Scopes bridge the gap between singletons (too broad) and factories (too narrow), giving you precise control over dependency lifetimes in your Android applications.