Android Architecture series · Part 6 of 9

DI, Async and Errors: The Plumbing Every Architecture Needs

Khairul Alam·October 2026·~12 min read
HiltKoinCoroutinesRxJavaLifecycle

Whatever architecture you pick, you need the same plumbing: a way to hand objects to classes, run slow work off the main thread, respect the lifecycle, and fail gracefully.

In 60 seconds
  • DI: pass dependencies in; Hilt is the default, Koin is simpler.
  • Async: coroutines and Flow for new code; Rx is fine; AsyncTask is deprecated.
  • Lifecycle: tie work to the screen so nothing leaks.
  • Errors: try/catch inside the coroutine, onError in Rx, one shared error type.
Contents
  1. Dependency injection
  2. Asynchronous code
  3. Lifecycle handling
  4. Error handling

1. Dependency injection (DI)

A class almost always needs other objects: a presenter needs a repository, a repository needs an API. Dependency injection means the class does not build those objects itself. They are handed in from outside, usually through the constructor.

// Hard to test and hard to change: it builds its own dependencies
class ProfilePresenter {
    private val repo = DefaultProfileRepository(RetrofitClient.create())
}

// DI: asks for what it needs, depends on the interface
class ProfilePresenter(private val repo: ProfileRepository)

Restaurant again. A chef should not drive to the farm for tomatoes. Tomatoes arrive at the kitchen door. Which farm they come from is somebody else’s job, and can change without the chef noticing.

You can do DI by hand (a function that builds and passes the objects), but with many classes a library saves effort:

LibraryStyleNotes
HiltCompile-time, annotation-basedGoogle’s recommended choice. Built on Dagger, with Android scopes (@HiltAndroidApp, @HiltViewModel). Errors show at build time.
Dagger 2Compile-timeThe engine under Hilt. Powerful, more setup.
KoinRuntime, Kotlin DSLEasy to read and start with. Missing bindings fail at runtime. (Technically a service locator.)
Kodein, kotlin-injectDSL / compile-timeKotlin-first alternatives, also work outside Android.
@Module
@InstallIn(SingletonComponent::class)
abstract class DataModule {
    @Binds abstract fun profileRepo(impl: DefaultProfileRepository): ProfileRepository
}

@Module
@InstallIn(SingletonComponent::class)
object AppModule {
    @Provides fun dispatchers(): DispatcherProvider = object : DispatcherProvider {
        override val io = Dispatchers.IO
        override val main = Dispatchers.Main
    }
}

In tests you swap the module for a fake one. This is how the Dependency Inversion principle from Part 2 becomes practical.

2. Asynchronous code

Android draws the screen on the main thread. If you block it with a network call for more than a few seconds the system shows “App Not Responding”. So slow work runs in the background and the result comes back to the main thread. The tools changed over the years:

ToolStatusOne-line summary
AsyncTaskDeprecated (API 30). Do not use for new code.Easy for beginners, but leaked Activities and broke on rotation.
Thread, Handler, ExecutorWorks, low levelYou manage everything yourself.
RxJavaMature, still common in big codebasesStreams with operators; subscribeOn/observeOn choose threads.
Kotlin coroutines + FlowRecommended by GoogleWrite async code that reads like normal code; cancellation follows lifecycle.
// Coroutines
viewModelScope.launch {
    val profile = withContext(dispatchers.io) { repo.getProfile(id) }
    _state.value = ProfileUiState.Content(profile)
}

// RxJava (same idea)
disposables += repo.getProfile(id)               // Single<Profile>
    .subscribeOn(schedulers.io())
    .observeOn(schedulers.ui())
    .subscribe({ _state.value = Content(it) }, { _state.value = Error("No connection") })

Both style examples use the Scheduler pattern from Part 2, so tests can run them instantly. Rule of thumb: new code uses coroutines and Flow; keep Rx where it already works and migrate slowly (there are adapters such as kotlinx-coroutines-rx3 to bridge them).

3. Lifecycle handling

An Activity or Fragment is created, shown, hidden and destroyed by the system, sometimes at moments you do not control (rotation, low memory, the user pressing Home). Work that outlives its screen is the root of leaks and crashes.

CreatedonCreateStartedvisibleResumedin focusStoppedhiddenDestroyedonDestroy
Simplified Activity lifecycle: created, started, resumed, then stopped and destroyed.

4. Error handling

Networks fail, disks fill up, servers return surprises. Decide in the data layer what the error means, and let the logic layer turn it into state the user can understand.

Coroutines: try/catch inside the coroutine

viewModelScope.launch {
    try {
        val profile = repo.getProfile(id)
        _state.value = Content(profile)
    } catch (e: CancellationException) {
        throw e                                   // never swallow cancellation
    } catch (e: IOException) {
        _state.value = Error("No connection")
    } catch (e: HttpException) {
        _state.value = Error("Server problem (${e.code()})")
    }
}

Rx: errors are part of the stream

repo.getProfile(id)
    .subscribeOn(schedulers.io())
    .observeOn(schedulers.ui())
    .retry(2)                                          // try again on failure
    .onErrorReturn { cachedProfile }                   // or fall back to a value
    .subscribe(
        { _state.value = Content(it) },
        { _state.value = Error("No connection") }     // ALWAYS provide onError
    )

If you forget the onError callback, Rx throws OnErrorNotImplementedException and the app can crash. Set a global fallback with RxJavaPlugins.setErrorHandler for exceptions that arrive after a stream is disposed.

One shared error model

Do not let HttpException or SQLiteException leak to the screen. Map them once into your own small type:

sealed interface AppError {
    data object NoConnection : AppError
    data class Server(val code: Int) : AppError
    data object Unknown : AppError
}

Now every architecture (presenter, ViewModel, interactor, reducer) handles the same few cases and shows the same friendly messages. That is DRY in action.

← Back to all writing