Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Apply SOLID in Android by giving each layer a clear responsibility and keeping dependencies pointed toward stable contracts: UI components render and handle user actions, ViewModels coordinate UI state, repositories manage data access, and use cases hold distinct business operations when they add clarity or reuse. These are design recommendations, not rules that require a fixed number of classes or modules.

How the five principles map to Android

Principle Android design question
Single Responsibility Does this class have one coherent reason to change?
Open/Closed Can a new data strategy be added without rewriting its consumers?
Liskov Substitution Can another implementation honor the behavior callers rely on?
Interface Segregation Does each client depend only on operations it needs?
Dependency Inversion Do higher-level policies avoid depending directly on technical details?

Google’s Android architecture guidance emphasizes separation of concerns and describes its recommendations as adaptable rather than a mandatory architecture. Treat SOLID as a way to evaluate boundaries, not a compliance checklist.

S — Give each class one coherent responsibility

The Single Responsibility Principle is about having one coherent reason to change, not limiting a class to one method. In a typical Android feature, a ViewModel coordinates UI state and user events, a repository coordinates data sources and any needed mapping, and a use case performs one business action.

For example, a refresh operation may validate a request and ask a repository to refresh articles. The repository can decide how to coordinate a network source and a local database. The ViewModel can start that operation in response to a user action and expose the resulting state to the UI. Google’s domain-layer guidance describes each use case as responsible for a single functionality.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Keep mutable state out of use-case classes; pass inputs as parameters. A use case is not automatically useful just because a function exists. If it only forwards one call, adds no policy, and is neither reused nor helpful to test or understand, placing that operation directly in its caller may be simpler.

O — Add variation behind stable contracts

The Open/Closed Principle means consumers should not need repeated edits whenever an implementation changes. Define a repository contract that expresses what a feature needs, then provide an implementation appropriate to the current data strategy.

For a news feature, a NewsRepository contract could be implemented by an offline-first repository, an in-memory repository, or a remote-backed repository. A ViewModel or use case that depends on the contract can continue to work when the application selects a different implementation. Google’s Android architecture examples include offline-first and in-memory repository implementations.

This does not mean every class needs an interface or that a speculative future backend deserves an abstraction now. Introduce a boundary when it reflects a real variation, such as production versus test data, or when it helps keep data-source details out of a client.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

L — Make replacement implementations honor the contract

The Liskov Substitution Principle is about behavior, not just matching method signatures. If callers rely on a repository to emit updates, report failures consistently, and respect coroutine cancellation, a fake or offline implementation should preserve those expectations.

Write down the behavior clients can rely on: for example, whether observing articles emits current local data before a refresh completes, how failures are surfaced, and what happens when a caller cancels a request. Test those expectations against both production and test implementations. Without a documented contract, an interface can look substitutable while implementations behave differently in ways that break callers.

I — Keep interfaces focused on their clients

The Interface Segregation Principle says a client should not depend on operations it does not use. If a broad repository interface exposes observation, refresh, deletion, synchronization, and administrative operations, a screen that only displays articles should not need to know about all of them.

Split the surface where it improves the boundary. For instance, a feature might use focused contracts such as ObserveArticles, RefreshArticles, and SaveArticle, or keep read-only flows separate from mutations. The useful design is the one that gives each client the operations it needs without creating needless layers. Android recommends that UI components and ViewModels get application data through repositories rather than contacting raw data sources directly.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

D — Keep policy independent of technical details

The Dependency Inversion Principle says high-level policy should rely on abstractions rather than directly on low-level details such as a network client, database implementation, or Android framework object. Supply those details from outside the policy code, commonly through constructor injection.

A ViewModel can receive the operations it needs in its constructor; a use case can receive a repository contract; and a repository implementation can receive a DAO and network data source. The place that assembles the application chooses the concrete implementations. Android recommends dependency injection and constructor injection when possible. Manual injection can be sufficient in a small application; Hilt is not required to apply dependency inversion. Android recommends Hilt for projects with multiple screens and ViewModels, WorkManager, or navigation-back-stack-scoped ViewModels.

Avoid passing an Activity, Context, or Resources deep into business logic. Keep platform-specific needs at a boundary, and expose application models from repositories when that gives the UI a more stable contract than database entities.

What a practical feature can look like

Consider an article screen. Its ViewModel can depend on an observation operation and a refresh operation. The observation operation reads through a repository; the repository coordinates a Room DAO and a network source. The refresh operation validates its request and asks the repository to refresh. The UI observes one state flow rather than reaching into storage or networking directly.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
interface ArticleRepository {
    fun observeArticles(): Flow<List<Article>>
    suspend fun refresh()
}

class ObserveArticles(
    private val repository: ArticleRepository
) {
    operator fun invoke(): Flow<List<Article>> = repository.observeArticles()
}

class RefreshArticles(
    private val repository: ArticleRepository
) {
    suspend operator fun invoke() {
        // Validate any feature-specific request here.
        repository.refresh()
    }
}

class ArticleViewModel(
    observeArticles: ObserveArticles,
    private val refreshArticles: RefreshArticles
) : ViewModel() {
    val articles = observeArticles()

    fun refresh() {
        viewModelScope.launch { refreshArticles() }
    }
}

This is an illustrative structure, not a requirement to create a separate use-case class for every call. A test can inject a fake or in-memory repository without constructing Android framework objects. In a real screen, the ViewModel would commonly expose a presentation-oriented uiState that includes loading and error information as well as article data.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

How to choose the right amount of architecture

SOLID boundaries have costs as well as benefits. Compare a proposed design against the actual needs of the feature:

  • Responsibility: Does each class have a focused reason to change, or is a class collecting unrelated work?
  • Dependency direction: Can business and UI policy be tested without constructing network, database, or framework details?
  • Substitutability: Do fake, offline, and production implementations honor the same caller-visible behavior?
  • Interface size: Are clients forced to depend on operations they do not use?
  • Test setup: Does the boundary make useful tests easier, or add indirection without reducing setup?
  • Lifecycle and coroutines: Are work and cancellation handled appropriately by the caller, and do implementations respect cancellation?
  • Variation: Does the abstraction represent an actual testing need, backend variation, or reuse case?

A small application can keep UI, domain, and data code in one module while still maintaining clear class boundaries. A larger application may separate those layers into modules when independent ownership, build structure, or dependency control makes that worthwhile. Module count is not a measure of SOLID compliance.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.