androidengineers.Book a session

Code Organization

Applying SOLID in Android

article25 minMedium

SOLID principles aren't abstract theory — they show up as concrete decisions in everyday Android code. Here's what each principle means in an Android context with real examples.

S — Single Responsibility

Each class has one reason to change.

// ❌ ViewModel doing data fetching AND formatting AND analytics
class ArticleViewModel : ViewModel() {
    fun load(id: String) {
        val article = api.getArticle(id)            // fetching
        val formatted = DateFormat.format(article)  // formatting
        analytics.track("viewed", id)               // analytics
        _state.value = UiState(article, formatted)
    }
}

// ✅ Each class has one job
class ArticleViewModel(
    private val getArticle: GetArticleUseCase,
    private val analytics: AnalyticsTracker
) : ViewModel() {
    fun load(id: String) {
        viewModelScope.launch {
            val article = getArticle(id)  // delegated
            analytics.track("article_viewed", id)
            _state.value = UiState.from(article)  // formatting delegated to UiState
        }
    }
}

O — Open/Closed

Open for extension, closed for modification. Add new behavior by adding code, not changing existing code.

// ❌ Adding a new payment method requires modifying existing code
fun processPayment(method: String, amount: Double) {
    when (method) {
        "card" -> processCard(amount)
        "paypal" -> processPaypal(amount)
        // Adding "crypto" requires editing this function
    }
}

// ✅ Each payment method is an extension; the processor doesn't change
interface PaymentMethod {
    fun process(amount: Double): PaymentResult
}

class CardPayment : PaymentMethod { override fun process(amount: Double) = ... }
class PaypalPayment : PaymentMethod { override fun process(amount: Double) = ... }
// Adding CryptoPayment: just create a new class — PaymentProcessor unchanged

class PaymentProcessor {
    fun processPayment(method: PaymentMethod, amount: Double) = method.process(amount)
}

L — Liskov Substitution

Subtypes must be usable wherever the base type is expected — without breaking the caller.

// ❌ Violates LSP: ReadOnlyList throws on mutating operations
class ReadOnlyList<T>(private val items: List<T>) : MutableList<T> by mutableListOf() {
    override fun add(element: T): Boolean = throw UnsupportedOperationException()
}
// Caller expecting MutableList<T> behavior breaks

// ✅ Use the correct type
fun displayItems(items: List<Article>) { /* read only */ }  // accepts both List and MutableList

In Android: if a ViewModel subclass needs to suppress an inherited method, it's a sign the inheritance is wrong — prefer composition.

I — Interface Segregation

Many small, specific interfaces beat one large general one.

// ❌ Fat interface — most implementors only need part of it
interface DataSource {
    fun getArticle(id: String): Article
    fun saveArticle(article: Article)
    fun deleteArticle(id: String)
    fun getUser(id: String): User
    fun saveUser(user: User)
}

// ✅ Separate interfaces
interface ArticleReader { fun getArticle(id: String): Article }
interface ArticleWriter { fun saveArticle(article: Article); fun deleteArticle(id: String) }
interface UserReader { fun getUser(id: String): User }

// Repositories implement only what they need
class RemoteArticleSource : ArticleReader {
    override fun getArticle(id: String) = api.getArticle(id)
    // No saveArticle — read-only remote source
}

D — Dependency Inversion

High-level modules depend on abstractions, not concrete implementations.

// ❌ ViewModel creates its own dependencies — hard to test
class ArticleViewModel : ViewModel() {
    private val repository = ArticleRepositoryImpl(  // concrete class
        ArticleApi.create(),
        ArticleDao.create()
    )
}

// ✅ Dependencies injected via constructor — easy to swap for tests
class ArticleViewModel(
    private val repository: ArticleRepository  // interface, not impl
) : ViewModel()

// In tests:
val viewModel = ArticleViewModel(FakeArticleRepository())

Hilt/Dagger provide the concrete implementation at DI time; the ViewModel never knows.

SOLID in Practice: A Quick Audit

Ask these questions of each class:

  • Does it have more than one reason to change? → Split (SRP)
  • Adding new behavior requires modifying this class? → Extract interface (OCP)
  • Does any subclass override methods with unexpected behavior? → Prefer composition (LSP)
  • Does an interface have methods callers don't need? → Split the interface (ISP)
  • Does this class create its own collaborators? → Inject them (DIP)

Key Takeaways

PrincipleCommon Android violationFix
SRPViewModel doing fetch + format + analyticsDelegate to use cases and formatters
OCPwhen statements over type stringsPolymorphic dispatch via interface
LSPMutableList backed by read-only dataMatch type to actual contract
ISPOne giant Repository interfaceSeparate reader/writer/deleter interfaces
DIPViewModel creating RepositoryImpl directlyConstructor injection of interfaces

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
Applying SOLID in Android | Android System Design | Android Engineers