Android SharedPreferences Tutorial and Example

Implementing dependency injection with Dagger

Dependency injection gives an Android class its collaborators instead of making it construct them. Dagger turns those relationships into a compile-time checked object graph, keeping activities, repositories and workers focused on their own work.

Manual factories work for small apps, but growing projects need a network client, SQLite helper and scheduler with different lifetimes. Explicit dependencies reduce hidden coupling and make test doubles straightforward to substitute.

The examples use Kotlin and Dagger 2 for an app loading transit departures. For Australian readers, imagine an app showing Opal journeys in Sydney alongside Myki trips in Melbourne: a repository retrieves data and a screen displays it through a view model.

An Australian release also means pricing in Australian dollars for the local Google Play market, where consumer prices commonly include 10% GST, and coordinating maintenance across several Australian time zones.

Why dependency injection helps Android projects

Dependency injection applies inversion of control: an object receives what it needs instead of creating or locating it. Constructor injection makes required dependencies explicit in Kotlin, and tests can pass fakes directly.

Dagger uses annotations and generated code. An @Inject constructor tells it how to create a class, modules supply bindings for types needing custom construction, and a component assembles the graph. A missing binding fails the build rather than crashing at runtime.

Setting up Dagger in an Android project

Add Dagger and its annotation processor to the app module, keeping both versions aligned. Enable the Kotlin KAPT plugin with the same version as your Kotlin plugin; the versions below are examples for a compatible toolchain.

plugins {
    id("org.jetbrains.kotlin.kapt")
}

dependencies {
    implementation("com.google.dagger:dagger:2.52")
    kapt("com.google.dagger:dagger-compiler:2.52")
}

KSP is suitable when your selected Dagger release supports your Kotlin version. Rebuild after changing annotations. For further practice, combine this setup with hands-on Android learning.

Building the application component

Start with the dependency contract, not the Android framework. The repository exposes departures, while a data source can later use Retrofit, a SQLite cache or SharedPreferences.

interface DepartureRepository {
    suspend fun departures(): List<String>
}

class DefaultDepartureRepository @Inject constructor(
    private val source: DepartureDataSource
) : DepartureRepository {
    override suspend fun departures() = source.departures()
}

class DepartureDataSource @Inject constructor() {
    suspend fun departures(): List<String> = emptyList()
}

@Module
abstract class RepositoryModule {
    @Binds
    @Singleton
    abstract fun bindRepository(
        implementation: DefaultDepartureRepository
    ): DepartureRepository
}

@Singleton
@Component(modules = [RepositoryModule::class])
interface AppComponent {
    fun inject(activity: MainActivity)

    @Component.Factory
    interface Factory {
        fun create(): AppComponent
    }
}

The @Binds method connects the interface to its implementation, and @Singleton allows reuse within the component. Store the component built by DaggerAppComponent.factory().create() in your Application class. Activities retrieve that owner and call inject(this) before using injected fields. Replace the placeholder data source with a Retrofit-backed implementation later; the contract remains unchanged.

Matching dependency lifetimes with scopes

A scope determines how long Dagger keeps a binding. Choose it from the object's real lifetime, not convenience: a repository may span the application, whereas a screen-specific helper should disappear with that screen.

Dagger Android supports activity-retained and fragment scopes through child components. These require extra setup, so begin with an application component and add finer scopes only where a measured need exists.

Scope Lifetime Suitable use Main caution
@Singleton One instance per application component Repository or API client Holds references for the process lifetime
@ActivityRetainedScoped Survives activity recreation Retained controller Needs Dagger Android child components
@ActivityScoped One activity instance Activity-specific helper Rebuilt with the activity
@FragmentScoped One fragment instance Fragment-owned collaborator Must match the fragment lifetime
Unscoped New instance per request Small stateless utility Can create duplicates
Custom scope A boundary you define Feature session Requires consistent annotations

Scopes are permissions, not automatic lifecycle guards. Dagger will retain a singleton that holds an Activity reference, so application-scoped bindings should contain only application-safe objects.

Injecting activities, fragments and view models

Android creates activities and fragments, so field injection is practical for their framework-managed dependencies. Call inject(this) in an activity's onCreate, or in a fragment's onAttach before onCreate, and never access an injected field earlier.

class TransitApplication : Application() {
    val appComponent: AppComponent by lazy {
        DaggerAppComponent.factory().create()
    }
}

class MainActivity : AppCompatActivity() {
    @Inject lateinit var repository: DepartureRepository

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        (application as TransitApplication).appComponent.inject(this)
    }
}

For a view model, supply the repository through a ViewModelProvider.Factory that constructs it with constructor injection. For WorkManager jobs, pass collaborators through a custom WorkerFactory rather than reaching into an Activity or a static locator.

Keep a screen's state in its view model, and keep the view model free of Activity references. A worker that writes to SQLite can receive a repository over the database, making its dependencies and lifetime explicit.

Testing graph boundaries and collaborators

Unit tests do not need a full Dagger graph. A repository with constructor injection can be created with a fake and exercised directly, giving fast feedback without launching Android.

class FakeDepartureRepository : DepartureRepository {
    override suspend fun departures(): List<String> =
        listOf("Central to Bondi Junction")
}

@Test
fun fakeRepositoryReturnsDepartures() = runTest {
    val repository = FakeDepartureRepository()
    assertEquals(
        "Central to Bondi Junction",
        repository.departures().first()
    )
}

For integration tests, create a separate test component and expose the relevant bindings. Use ActivityScenario to launch an activity, inject it and verify the rendered state. Keep test doubles near the tests rather than mixing them into production modules.

Prefer constructor injection for plain Kotlin classes because it leaves no dependency unset. Reserve field injection for Android-created objects, and inspect generated component code only when a binding error needs deeper investigation.

Practical habits for maintainable Dagger code

Dagger can make wiring disappear from daily development, yet the graph still needs deliberate maintenance. Small habits prevent an application component from becoming an unreadable collection of bindings.

The following practices keep construction, scope and test boundaries visible as the app changes.

  • Start with constructor injection for repositories and data sources; use field injection only where Android creates the instance.
  • Give each component a clear owner, such as the Application class, and keep activity or fragment child components with their hosts.
  • Use @Binds for interface-to-implementation mappings and @Provides only when construction needs custom logic.
  • Never place an Activity, Fragment or View inside a @Singleton binding because the screen can leak.
  • Substitute network, clock and storage dependencies with deterministic fakes in tests.
  • Run assembleDebug and unit tests on every pull request so missing bindings fail before Google Play deployment.