Design patterns are proven solutions to recurring problems. In Android, you encounter them constantly — often without realizing it. Understanding them helps you recognize structure in existing code, communicate decisions clearly with teammates, and pick the right tool for each situation.
Repository Pattern
The Repository pattern provides a clean API to data, hiding whether it comes from the network, cache, or database.
interface TaskRepository {
fun observeTasks(): Flow<List<Task>>
suspend fun sync(): Result<Unit>
}
class TaskRepositoryImpl(
private val remote: TaskRemoteSource,
private val local: TaskLocalSource
) : TaskRepository {
override fun observeTasks(): Flow<List<Task>> =
local.observeTasks().map { entities -> entities.map { it.toTask() } }
override suspend fun sync(): Result<Unit> = runCatching {
val remoteTasks = remote.fetchTasks()
local.upsertAll(remoteTasks.map { it.toEntity() })
}
}
The ViewModel never knows whether data came from the network or disk. This also makes it trivial to swap implementations in tests.
Observer Pattern
The Observer pattern lets objects subscribe to changes without being tightly coupled to the producer. Kotlin's Flow and StateFlow are the standard implementation in Android.
class NotificationViewModel(
private val repository: NotificationRepository
) : ViewModel() {
val unreadCount: StateFlow<Int> = repository
.observeUnreadCount()
.stateIn(viewModelScope, SharingStarted.Eagerly, 0)
}
The ViewModel is the observable subject. The composable is the observer. The composable does not need to know when or why the count changes — it just reacts.
Factory Pattern
Factories create objects when construction logic is complex or needs to vary.
sealed interface PaymentMethod {
data class Card(val last4: String) : PaymentMethod
data class Wallet(val provider: String) : PaymentMethod
}
fun createPaymentProcessor(method: PaymentMethod): PaymentProcessor {
return when (method) {
is PaymentMethod.Card -> CardPaymentProcessor(method.last4)
is PaymentMethod.Wallet -> WalletPaymentProcessor(method.provider)
}
}
In Android, ViewModel factories (ViewModelProvider.Factory) are the most visible factory use case. Hilt generates these automatically, but understanding factories explains how ViewModel construction works under the hood.
Strategy Pattern
The Strategy pattern defines a family of algorithms and makes them interchangeable.
interface ImageLoader {
suspend fun load(url: String): Bitmap
}
class CachedImageLoader(private val delegate: ImageLoader) : ImageLoader {
private val cache = LruCache<String, Bitmap>(50)
override suspend fun load(url: String): Bitmap {
return cache[url] ?: delegate.load(url).also { cache.put(url, it) }
}
}
The caller uses ImageLoader and does not need to know whether caching is active. You can wrap or swap the strategy without changing call sites.
Decorator Pattern
The Decorator pattern adds behavior to an object without subclassing. OkHttp interceptors are the most visible Android example.
class AuthInterceptor(
private val tokenProvider: TokenProvider
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request().newBuilder()
.addHeader("Authorization", "Bearer ${tokenProvider.token}")
.build()
return chain.proceed(request)
}
}
Each interceptor adds a layer of behavior (auth, logging, retry) without modifying the Retrofit or OkHttp core.
Mediator Pattern (ViewModel as Mediator)
In complex screens, multiple components need to communicate without knowing about each other. The ViewModel acts as the mediator.
class CheckoutViewModel(
private val cartRepository: CartRepository,
private val paymentRepository: PaymentRepository,
private val analyticsRepository: AnalyticsRepository
) : ViewModel() {
fun onConfirmOrder() {
viewModelScope.launch {
val cart = cartRepository.getCart()
val result = paymentRepository.charge(cart.total)
if (result.isSuccess) {
analyticsRepository.logPurchase(cart)
_state.update { it.copy(isComplete = true) }
}
}
}
}
The checkout screen does not know about the cart, payment, or analytics systems. The ViewModel coordinates them.
When Not to Use a Pattern
Patterns add indirection. Apply them when:
- The problem recurs and the pattern makes the solution clearer
- You need to swap implementations (Strategy, Factory, Repository)
- You need to extend behavior without modifying core code (Decorator)
Do not apply a pattern just because it has a name. A simple if/else is often better than a Factory with two implementations.
Practice
Identify one pattern in your existing codebase that is present but not named. Document why it exists and what problem it solves. Then find one place where a pattern would genuinely reduce duplication and apply it.
Summary
Repository centralizes data access. Observer decouples producers from consumers. Factory abstracts complex construction. Strategy makes algorithms swappable. Decorator adds behavior without subclassing. ViewModel is a natural mediator. Apply patterns to solve real problems, not to demonstrate knowledge.