Design Patterns in Android Application Development: Use Cases and Examples

Sumeet Panchal
7 min read5 hours ago

--

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.”

--

--

Sumeet Panchal
Sumeet Panchal

Written by Sumeet Panchal

Programming enthusiast specializing in Android and React Native, passionate about crafting intuitive mobile experiences and exploring innovative solutions.

No responses yet