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
| Principle | Common Android violation | Fix |
|---|---|---|
| SRP | ViewModel doing fetch + format + analytics | Delegate to use cases and formatters |
| OCP | when statements over type strings | Polymorphic dispatch via interface |
| LSP | MutableList backed by read-only data | Match type to actual contract |
| ISP | One giant Repository interface | Separate reader/writer/deleter interfaces |
| DIP | ViewModel creating RepositoryImpl directly | Constructor injection of interfaces |