Design Patterns in Android Application Development: Use Cases and Examples
Design patterns are critical in software development as they provide time-tested solutions to common problems. In Android application development, understanding and implementing design patterns can significantly enhance the quality, maintainability, and scalability of your code. This blog will explore some widely used design patterns in Android development, along with their use cases and examples.
1. Singleton
Overview
The Singleton pattern ensures that a class has only one instance and provides a global point of access to it.
Use Case
Singletons are commonly used for managing shared resources, such as database instances, network clients, or application-wide settings.
Example in Real-World Apps
The Retrofit library in Android apps often uses a Singleton to create a single instance of its API client. For example, in an e-commerce app like Amazon, a Singleton API client manages all network calls to fetch product details, user data, and transaction history.
object RetrofitClient {
private const val BASE_URL = "https://api.example.com"
private val retrofit: Retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
val apiService: ApiService = retrofit.create(ApiService::class.java)
}
2. Factory Method
Overview
The Factory Method pattern provides an interface for creating objects but allows subclasses to alter the type of objects that will be created.
Use Case
This pattern is useful when the creation logic is complex or when objects need to be instantiated dynamically.
Example in Real-World Apps
In apps like WhatsApp, the Factory Method pattern is used to create different types of message objects (text, image, video) based on the type of content being shared. This ensures flexibility and scalability as new message types are introduced.
sealed class Message {
data class TextMessage(val content: String) : Message()
data class ImageMessage(val url: String) : Message()
data class VideoMessage(val url: String, val duration: Int) : Message()
}
object MessageFactory {
fun createMessage(type: String, data: String): Message = when (type) {
"text" -> Message.TextMessage(data)
"image" -> Message.ImageMessage(data)
"video" -> Message.VideoMessage(data, duration = 120)
else -> throw IllegalArgumentException("Unknown message type")
}
}
3. Observer
Overview
The Observer pattern allows one object (the subject) to notify a list of observers about changes to its state.
Use Case
In Android, the Observer pattern is frequently used in the context of LiveData and ViewModel to update the UI whenever the data changes.
Example in Real-World Apps
Stock trading apps like Robinhood, LiveData, and the Observer pattern are used to update the UI with real-time stock prices and trading activity.
class StockPriceViewModel : ViewModel() {
private val _stockPrice = MutableLiveData<Double>()
val stockPrice: LiveData<Double> get() = _stockPrice
fun updatePrice(price: Double) {
_stockPrice.value = price
}
}
// Activity or Fragment
stockPriceViewModel.stockPrice.observe(this, Observer { price ->
stockPriceTextView.text = "$price"
})
4. Builder
Overview
The Builder pattern simplifies the construction of complex objects by providing a step-by-step approach.
Use Case
It is useful when creating objects with many optional parameters, such as AlertDialogs or Retrofit instances.
Example in Real-World Apps
In food delivery apps like Zomato, the Builder pattern is used to construct complex order objects that include customization options such as toppings, delivery instructions, and payment methods.
val order = Order.Builder()
.addItem("Pizza")
.addTopping("Cheese")
.setDeliveryInstructions("Leave at the door")
.build()
class Order private constructor(
val items: List<String>,
val toppings: List<String>,
val deliveryInstructions: String?
) {
class Builder {
private val items = mutableListOf<String>()
private val toppings = mutableListOf<String>()
private var deliveryInstructions: String? = null
fun addItem(item: String) = apply { items.add(item) }
fun addTopping(topping: String) = apply { toppings.add(topping) }
fun setDeliveryInstructions(instructions: String) = apply { deliveryInstructions = instructions }
fun build() = Order(items, toppings, deliveryInstructions)
}
}
5. Decorator
Overview
The Decorator pattern allows you to dynamically add behavior to objects without modifying their structure.
Use Case
This pattern is commonly used in RecyclerView to add item decorations, such as dividers or margins.
Example in Real-World Apps
In Instagram, the Decorator pattern is used to apply filters to photos and videos. Each filter is a decorator that enhances the original image or video with additional effects.
abstract class PhotoFilter {
abstract fun apply(image: Bitmap): Bitmap
}
class BrightnessFilter : PhotoFilter() {
override fun apply(image: Bitmap): Bitmap {
// Apply brightness adjustment
return image
}
}
class ContrastFilter : PhotoFilter() {
override fun apply(image: Bitmap): Bitmap {
// Apply contrast adjustment
return image
}
}
val originalImage: Bitmap = ...
val brightenedImage = BrightnessFilter().apply(originalImage)
6. Command
Overview
The Command pattern encapsulates a request as an object, allowing you to parameterize clients with queues, requests, and operations.
Use Case
Useful in implementing undo/redo functionality in text editors or drawing apps.
Example in Real-World Apps
In drawing apps like Adobe Photoshop, each user action (e.g., drawing a line) can be encapsulated as a command, making it easy to undo or redo actions.
interface Command {
fun execute()
fun undo()
}
class DrawLineCommand(private val canvas: Canvas, private val line: Line) : Command {
override fun execute() {
canvas.draw(line)
}
override fun undo() {
canvas.remove(line)
}
}
val drawLineCommand = DrawLineCommand(canvas, Line(Point(0, 0), Point(100, 100)))
drawLineCommand.execute()
7. Adapter
Overview
The Adapter pattern converts the interface of a class into another interface that a client expects.
Use Case
In Android, the Adapter pattern is extensively used to bridge data sources and UI components, such as in RecyclerView.
Example in Real-World Apps
In apps like Spotify, an adapter is used to display a list of songs in a RecyclerView, mapping the song data to the UI.
class SongAdapter(private val songs: List<Song>) : RecyclerView.Adapter<SongAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.song_item, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val song = songs[position]
holder.bind(song)
}
override fun getItemCount(): Int = songs.size
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(song: Song) {
itemView.songTitle.text = song.title
itemView.artistName.text = song.artist
}
}
}
8. Prototype
Overview
The Prototype pattern creates new objects by cloning existing instances instead of creating new ones.
Use Case
This pattern is often used when object creation is expensive, such as creating shapes in graphic design tools.
Example in Real-World Apps
In design apps like Canva, cloning existing shapes or templates allows users to quickly replicate elements on their canvas.
abstract class Shape : Cloneable {
var color: String = ""
public override fun clone(): Shape {
return super.clone() as Shape
}
}
class Circle : Shape()
fun main() {
val originalCircle = Circle()
originalCircle.color = "Red"
val clonedCircle = originalCircle.clone()
println(clonedCircle.color) // Outputs: Red
}
9. Composite
Overview
The Composite pattern allows you to compose objects into tree structures to represent part-whole hierarchies.
Use Case
Used when objects need to be treated uniformly, such as in file systems or UI components with nested views.
Example in Real-World Apps
In apps like Google Drive, the Composite pattern represents folder structures where files and subfolders coexist.
interface Component {
fun show()
}
class File(private val name: String) : Component {
override fun show() {
println("File: $name")
}
}
class Folder(private val name: String) : Component {
private val components = mutableListOf<Component>()
fun add(component: Component) {
components.add(component)
}
override fun show() {
println("Folder: $name")
components.forEach { it.show() }
}
}
10. State
Overview
The State pattern allows an object to alter its behavior when its internal state changes.
Use Case
Useful for implementing workflows or UI components that change behavior based on the state, such as media players.
Example in Real-World Apps
In music apps like Spotify, the State pattern is used to handle playback states (e.g., play, pause, stop).
interface PlayerState {
fun play()
fun pause()
}
class PlayingState : PlayerState {
override fun play() {
println("Already playing")
}
override fun pause() {
println("Pausing playback")
}
}
class PausedState : PlayerState {
override fun play() {
println("Resuming playback")
}
override fun pause() {
println("Already paused")
}
}
class MediaPlayer {
private var state: PlayerState = PausedState()
fun setState(newState: PlayerState) {
state = newState
}
fun play() {
state.play()
}
fun pause() {
state.pause()
}
}
fun main() {
val player = MediaPlayer()
player.play() // Resuming playback
player.setState(PlayingState())
player.play() // Already playing
player.pause() // Pausing playback
}
11. Strategy
Overview
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable.
Use Case
Useful for scenarios like dynamic behavior switching, such as payment methods or sorting algorithms.
Example in Real-World Apps
In e-commerce apps like Flipkart, the Strategy pattern is used to implement different payment methods (e.g., credit card, PayPal, UPI).
interface PaymentStrategy {
fun pay(amount: Double)
}
class CreditCardPayment : PaymentStrategy {
override fun pay(amount: Double) {
println("Paid $amount using Credit Card")
}
}
class PayPalPayment : PaymentStrategy {
override fun pay(amount: Double) {
println("Paid $amount using PayPal")
}
}
class UpiPayment : PaymentStrategy {
override fun pay(amount: Double) {
println("Paid $amount using UPI")
}
}
class Checkout(private var paymentStrategy: PaymentStrategy) {
fun setPaymentMethod(strategy: PaymentStrategy) {
paymentStrategy = strategy
}
fun pay(amount: Double) {
paymentStrategy.pay(amount)
}
}
fun main() {
val checkout = Checkout(CreditCardPayment())
checkout.pay(1000.0) // Paid 1000.0 using Credit Card
checkout.setPaymentMethod(PayPalPayment())
checkout.pay(2000.0) // Paid 2000.0 using PayPal
checkout.setPaymentMethod(UpiPayment())
checkout.pay(1500.0) // Paid 1500.0 using UPI
}
12. Template Method
Overview
The Template Method pattern defines the skeleton of an algorithm and lets subclasses fill in the details.
Use Case
Useful when you have a fixed workflow with some steps varying based on the context.
Example in Real-World Apps
In news apps like Google News, the Template Method pattern is used for rendering different types of articles (e.g., text-only, multimedia).
abstract class ArticleRenderer {
fun render() {
fetchData()
renderHeader()
renderBody()
}
protected abstract fun fetchData()
protected abstract fun renderHeader()
protected abstract fun renderBody()
}
class TextArticleRenderer : ArticleRenderer() {
override fun fetchData() {
println("Fetching text article data")
}
override fun renderHeader() {
println("Rendering text article header")
}
override fun renderBody() {
println("Rendering text article body")
}
}
class MultimediaArticleRenderer : ArticleRenderer() {
override fun fetchData() {
println("Fetching multimedia article data")
}
override fun renderHeader() {
println("Rendering multimedia article header")
}
override fun renderBody() {
println("Rendering multimedia article body with images and videos")
}
}
fun main() {
val textRenderer = TextArticleRenderer()
textRenderer.render()
val multimediaRenderer = MultimediaArticleRenderer()
multimediaRenderer.render()
}
“Great! You’ve reached the end of this discussion. Feel free to reach out if you need further insights on any of the design patterns.”