Skip to main content
Version: 4.2

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

HiltKoin 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