๐Ÿ›๏ธ Android Architecture and Clean Code

SOLID Principles in Android Development

SOLID is often taught with shapes and animals. On Android, the same five principles show up as ViewModels that do too much, repositories that are hard to fake, and features that need edits in ten places. Here's each principle with real Android Kotlin code.

โœ๏ธ Yunok Tech ๐Ÿ“… Updated โฑ 3 min read ๐ŸŽฏ Intermediate
On this page
  1. S: Single Responsibility
  2. O: Open/Closed
  3. L: Liskov Substitution
  4. I: Interface Segregation
  5. D: Dependency Inversion
  6. When SOLID becomes over-engineering
  7. Where it pays off on Android
  8. Frequently asked questions
  9. Conclusion

SOLID is a set of five design principles, popularised by Robert C. Martin, for code that's easier to change. They're not rules to apply mechanically. They're questions to ask when code starts to hurt. Here's what each one looks like in an Android codebase.

The five SOLID principles S: single responsibility, one reason to change. O: open/closed, extend without modifying. L: Liskov substitution, implementations honour contracts. I: interface segregation, small focused interfaces. D: dependency inversion, depend on abstractions. SSingleresponsibility OOpen for extension,closed for change LLiskovsubstitution IInterfacesegregation DDependencyinversion
Five questions to ask when code becomes hard to change.

S: Single Responsibility

A class should have one reason to change. On Android, the classic violation is a ViewModel or Activity that formats, validates, fetches, caches and tracks analytics.

Splitting responsibilitiesKotlin
// โŒ One class changes for UI tweaks, API changes, caching policy AND analytics events
class ProfileViewModel(/* api, dao, prefs, analytics */) : ViewModel() { /* 600 lines */ }

// โœ… Each concern has an owner
class ProfileRepository(/* api, dao */)            // data access + caching
class ProfileValidator                              // business rules for edits
class ProfileViewModel(                             // screen state + coordination
    private val repository: ProfileRepository,
    private val validator: ProfileValidator,
    private val analytics: Analytics,
) : ViewModel()

O: Open/Closed

Extend behaviour without modifying existing code. Example: adding a payment method shouldn't require editing a giant when in five places.

Pluggable payment methodsKotlin
interface PaymentMethodHandler {
    val id: String
    suspend fun pay(orderId: String, amountPaise: Long): PaymentResult
}

class UpiHandler @Inject constructor(/* ... */) : PaymentMethodHandler { override val id = "upi" /* ... */ }
class CardHandler @Inject constructor(/* ... */) : PaymentMethodHandler { override val id = "card" /* ... */ }

class PaymentProcessor @Inject constructor(
    private val handlers: Set<@JvmSuppressWildcards PaymentMethodHandler>,   // Hilt multibinding
) {
    suspend fun pay(methodId: String, orderId: String, amount: Long) =
        handlers.first { it.id == methodId }.pay(orderId, amount)
}
// Adding Wallet = new class + @IntoSet binding; PaymentProcessor never changes.
Sealed types are the exception

For a closed set of states, such as UI state, a sealed type with an exhaustive when is better, because the compiler forces you to handle new cases. Use open extension for plug-in style variation.

L: Liskov Substitution

Implementations must honour the contract of their abstraction. If ArticleRepository.observeArticles() promises cached data even when offline, a "network-only" implementation that throws offline breaks every caller that relied on that promise.

Document and test the contractKotlin
interface ArticleRepository {
    /** Emits cached articles immediately (possibly empty); never throws for network failures. */
    fun observeArticles(): Flow<List<Article>>
}

Shared contract tests that run against every implementation, including fakes, catch Liskov violations early.

I: Interface Segregation

Clients shouldn't depend on methods they don't use.

Split a fat interfaceKotlin
// โŒ The bookmarks screen depends on admin operations it never calls
interface ArticleService {
    fun observe(): Flow<List<Article>>
    suspend fun bookmark(id: String)
    suspend fun publish(article: Article)
    suspend fun delete(id: String)
}

// โœ… Focused interfaces; one class can implement several
interface ArticleReader { fun observe(): Flow<List<Article>> }
interface BookmarkWriter { suspend fun bookmark(id: String) }
interface ArticleAdmin { suspend fun publish(article: Article); suspend fun delete(id: String) }

D: Dependency Inversion

High-level policy shouldn't depend on low-level details. Both depend on abstractions. Your ViewModel depends on WeatherRepository (an interface), not on Retrofit. Hilt supplies the implementation. This is what makes testing with fakes trivial.

Inverted dependencyKotlin
class WeatherViewModel @Inject constructor(private val repo: WeatherRepository) : ViewModel()
// Production: DefaultWeatherRepository(Retrofit, Room). Tests: FakeWeatherRepository.

When SOLID becomes over-engineering

  • An interface for every class "just in case", which adds navigation cost and no flexibility.
  • Splitting simple classes into many tiny ones that are always used together.
  • Plugin architectures for variation that will never happen.

Apply SOLID where change actually happens: areas that change often, are tested heavily, or are shared across features.

Where it pays off on Android

Analytics

An Analytics interface (DIP) with a Firebase implementation, plus a no-op for tests and debug builds.

Feature flags

A small FeatureFlags reader interface (ISP), so features don't see the remote-config SDK.

Notifications

Handlers per notification type (OCP) registered in a set, with a dispatcher routing by type.

Frequently asked questions

Is SOLID still relevant with Kotlin and Compose?

Yes. The principles are language-agnostic. Kotlin features such as interfaces, sealed types and extension functions make them lighter to apply.

Which principle matters most in Android apps?

Single responsibility and dependency inversion usually give the largest practical benefit: smaller ViewModels and testable code.

How do I explain SOLID in interviews?

Use a concrete Android example for each principle, like the ones above, and mention when you'd choose not to apply one.

Conclusion

SOLID on Android means focused classes, extension points where variation is real, honest contracts, small interfaces and dependencies on abstractions, applied where code actually changes, not everywhere.

Next step: see recurring solutions in Design Patterns Every Android Developer Should Know.

Official references

๐Ÿ“ฑ Apps built by Yunok Tech

Real Android apps on Google Play, free to download.

See all our apps โ†’

Ready to build real Android applications?

Architecture makes the most sense when you apply it to a real project and see why each boundary exists. Learn how maintainable Android applications are designed by building one.

Move beyond theory and strengthen your Android development skills with Yunoktech's Android course. Learn core concepts, practical implementation, modern Android development techniques, and the reasoning behind building maintainable applications.

Explore the Android course and take your next step as an Android developer.

Share: LinkedIn X / Twitter WhatsApp