Android Best Practices
This guide covers Android-specific best practices for memory management, security, and migration from Hilt.
info
For general module concepts, see Modules. For scoping, see Scopes and Android Scopes.
Memory Management
Avoid Activity/Fragment Leaks
// ❌ Bad - Activity leak
module {
single { SomeService(get<Activity>()) } // Activity reference in singleton!
}
// ✅ Good - Use Application context
module {
single { SomeService(androidContext()) } // Application context, safe
}
// ✅ Good - Use activity scope
module {
activityScope {
scoped { SomeService(/* activity-scoped dependencies */) }
}
}
Close Scopes Properly
// ✅ Good - Automatic scope management
class MyActivity : ScopeActivity() {
override val scope: Scope by activityScope()
// Scope automatically closed in onDestroy
}
// ❌ Bad - Manual scope without cleanup
class MyActivity : AppCompatActivity() {
private val myScope = createScope<MyActivity>()
// Scope never closed - memory leak!
}
// ✅ Good - Manual scope with cleanup
class MyActivity : AppCompatActivity() {
private lateinit var myScope: Scope
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
myScope = createScope<MyActivity>()
}
override fun onDestroy() {
myScope.close()
super.onDestroy()
}
}
Clear References in Long-Lived Objects
// ❌ Bad - Holding references to UI
class UserRepository {
private val listeners = mutableListOf<UserUpdateListener>() // Might hold Activity refs
fun addListener(listener: UserUpdateListener) {
listeners.add(listener)
}
}
// ✅ Good - Weak references or manual cleanup
class UserRepository {
private val listeners = mutableListOf<WeakReference<UserUpdateListener>>()
fun addListener(listener: UserUpdateListener) {
listeners.add(WeakReference(listener))
}
fun removeListener(listener: UserUpdateListener) {
listeners.removeAll { it.get() == listener || it.get() == null }
}
}
Android Debugging
Enable Android Logger
startKoin {
androidLogger(Level.DEBUG) // See all Koin operations
androidContext(this@MyApplication)
modules(appModules)
}
Verify Modules in Debug Builds
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this@MyApplication)
modules(allModules)
}
// Use verify() in unit tests instead
// appModule.verify()
}
}
Scope Callbacks for Debugging
class DebugActivity : ScopeActivity() {
override val scope: Scope by activityScope()
init {
scope.registerCallback(object : ScopeCallback {
override fun onScopeClose(scope: Scope) {
Log.d("Koin", "Scope ${scope.id} closing")
}
})
}
}
Security Best Practices
Don't Store Secrets in Modules
// ❌ Bad - Hardcoded secrets
module {
single {
Retrofit.Builder()
.addInterceptor { chain ->
chain.proceed(
chain.request().newBuilder()
.header("API-Key", "super-secret-key") // NO!
.build()
)
}
.build()
}
}
// ✅ Good - Secrets from secure storage
module {
single {
val securePrefs = get<SecurePreferences>()
Retrofit.Builder()
.addInterceptor(AuthInterceptor(securePrefs))
.build()
}
}
Migration from Dagger/Hilt
info
Koin supports JSR-330 annotations (@Singleton, @Inject, @Named) from jakarta.inject. You can keep using familiar annotations. See JSR-330 Compatibility.
Annotation Mapping
| Hilt | Koin Annotations |
|---|---|
@Singleton | @Singleton (JSR-330 compatible) |
@Provides | @Factory |
@Binds | @Singleton ... bind Interface::class |
@Inject | @Inject (JSR-330 compatible) |
@HiltViewModel | @KoinViewModel |
@InstallIn(SingletonComponent) | @Module + @ComponentScan |
@InstallIn(ActivityComponent) | @Scope(ActivityScope::class) |
Example Migration
// Before (Hilt)
@HiltViewModel
class HomeViewModel @Inject constructor(
private val repository: UserRepository
) : ViewModel()
@Singleton
class UserRepositoryImpl @Inject constructor(
private val api: ApiService
) : UserRepository
// After (Koin) - minimal changes!
@KoinViewModel
class HomeViewModel(
private val repository: UserRepository
) : ViewModel()
@Singleton // Keep using JSR-330
class UserRepositoryImpl(
private val api: ApiService
) : UserRepository
Module Migration
// Before (Hilt)
@InstallIn(SingletonComponent::class)
@Module
object NetworkModule {
@Provides
@Singleton
fun provideRetrofit(): Retrofit = Retrofit.Builder().build()
}
// After (Koin Annotations)
@Module
class NetworkModule {
@Singleton
fun provideRetrofit(): Retrofit = Retrofit.Builder().build()
}
Gradual Migration
// Step 1: Add Koin alongside Hilt for new features
@KoinViewModel
class NewFeatureViewModel(
private val repository: NewFeatureRepository
) : ViewModel()
@Singleton
class NewFeatureRepository(private val api: ApiService)
// Step 2: Migrate existing features one by one
@Singleton
class MigratedRepository(private val api: ApiService) : UserRepository
// Step 3: Remove Hilt when migration complete
See Also
- Scopes - Core scoping concepts
- Android Scopes - Android lifecycle scopes
- Testing - Android testing guide
- Multi-Module Apps - Organizing Android modules