Three classic patterns that appear constantly in Android architecture: Factory for decoupled object creation, Builder for complex optional configuration, and Strategy for swappable algorithms.
Factory Pattern
Decouple the caller from knowing which concrete class to instantiate.
// Interface
interface ImageLoader {
fun load(url: String, imageView: ImageView)
}
// Concrete implementations
class GlideImageLoader : ImageLoader {
override fun load(url: String, imageView: ImageView) {
Glide.with(imageView).load(url).into(imageView)
}
}
class CoilImageLoader : ImageLoader {
override fun load(url: String, imageView: ImageView) {
imageView.load(url)
}
}
// Factory
object ImageLoaderFactory {
fun create(context: Context): ImageLoader {
return if (BuildConfig.USE_COIL) CoilImageLoader() else GlideImageLoader()
}
}
// Caller
val loader = ImageLoaderFactory.create(context)
loader.load("https://...", imageView)
In Android: ViewModel.Factory is the canonical Android example:
class ArticleViewModel(private val id: String, private val repo: ArticleRepository) : ViewModel() {
class Factory(private val id: String, private val repo: ArticleRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")
return ArticleViewModel(id, repo) as T
}
}
}
Builder Pattern
Build complex objects step-by-step with optional configuration, avoiding constructors with too many parameters.
// Complex object with many optional fields
data class ApiRequest(
val url: String,
val method: String,
val headers: Map<String, String>,
val body: String?,
val timeout: Long,
val retryCount: Int,
val cacheEnabled: Boolean
)
class ApiRequestBuilder(private val url: String) {
private var method = "GET"
private val headers = mutableMapOf<String, String>()
private var body: String? = null
private var timeout = 30_000L
private var retryCount = 3
private var cacheEnabled = true
fun method(method: String) = apply { this.method = method }
fun header(key: String, value: String) = apply { headers[key] = value }
fun body(body: String) = apply { this.body = body; this.method = "POST" }
fun timeout(ms: Long) = apply { this.timeout = ms }
fun retries(count: Int) = apply { this.retryCount = count }
fun noCache() = apply { this.cacheEnabled = false }
fun build() = ApiRequest(url, method, headers.toMap(), body, timeout, retryCount, cacheEnabled)
}
// Usage — reads like English
val request = ApiRequestBuilder("https://api.example.com/articles")
.header("Authorization", "Bearer $token")
.timeout(5000)
.noCache()
.build()
In Android: OkHttp's Request.Builder, Notification's NotificationCompat.Builder, and Glide's RequestBuilder are all canonical Builder examples.
Strategy Pattern
Define a family of algorithms and make them interchangeable.
// Strategy interface
interface SortStrategy<T> {
fun sort(items: List<T>): List<T>
}
// Concrete strategies
class ChronologicalSort : SortStrategy<Article> {
override fun sort(items: List<Article>) = items.sortedByDescending { it.publishedAt }
}
class PopularitySort : SortStrategy<Article> {
override fun sort(items: List<Article>) = items.sortedByDescending { it.viewCount }
}
class PersonalizedSort(private val userModel: UserModel) : SortStrategy<Article> {
override fun sort(items: List<Article>) = items.sortedByDescending { userModel.scoreFor(it) }
}
// Context that uses the strategy
class FeedSorter(private var strategy: SortStrategy<Article> = ChronologicalSort()) {
fun setStrategy(strategy: SortStrategy<Article>) { this.strategy = strategy }
fun sort(articles: List<Article>) = strategy.sort(articles)
}
// In ViewModel:
class FeedViewModel(private val flags: FeatureFlags) : ViewModel() {
private val sorter = FeedSorter(
if (flags.isPersonalizedFeedEnabled) PersonalizedSort(userModel) else ChronologicalSort()
)
}
Kotlin idiom: Strategies are often just lambdas in Kotlin:
typealias SortStrategy<T> = (List<T>) -> List<T>
val chronological: SortStrategy<Article> = { it.sortedByDescending { a -> a.publishedAt } }
val byPopularity: SortStrategy<Article> = { it.sortedByDescending { a -> a.viewCount } }
class FeedSorter(var strategy: SortStrategy<Article> = chronological) {
fun sort(articles: List<Article>) = strategy(articles)
}
When to Use Each
| Pattern | Use when |
|---|---|
| Factory | You want to hide which concrete class is created; swapping implementations |
| Builder | Object has many optional parameters; construction order matters |
| Strategy | You have multiple algorithms for the same operation; algorithm should be swappable at runtime |