androidinterview.com

Design Patterns Interview Questions for Android

26 questions

Tier
Difficulty
Level

Showing all 26 questions

SOLID & Principles

What are the SOLID principles?

Tier: EssentialDifficulty: EasyAsked at: meesho

SOLID is five principles for writing object oriented code that's easy to change and test, and each letter names one.

  • Single Responsibility, a class should have exactly one reason to change, one job.
  • Open/Closed, a class should be open to extension but closed to modification, add new behavior without editing existing, working code.
  • Liskov Substitution, a subclass should be usable anywhere its parent type is expected, without breaking anything.
  • Interface Segregation, don't force a class to implement methods it doesn't need, split fat interfaces into smaller, focused ones.
  • Dependency Inversion, depend on abstractions, not concrete implementations, so high level code doesn't know or care which specific class it's talking to.

None of these are Android specific, they're general object oriented design guidance, but Android code leans on them constantly, a ViewModel that only handles UI state and delegates data work to a Repository is Single Responsibility, ViewModelProvider.Factory letting you extend how a ViewModel gets built without touching the framework is Open/Closed, and a Repository interface with an in memory fake for tests versus a real Room backed implementation in production is Dependency Inversion in action. They're guidelines, not laws, the value is in the trade offs they force you to notice, not in following each one rigidly everywhere.

Explain the Dependency Inversion Principle (D in SOLID).

Tier: EssentialDifficulty: Medium

Dependency Inversion says high level code should depend on abstractions, not on concrete implementations, so a ViewModel doesn't need to know or care which exact class is actually fetching its data.

// Violates DIP: ViewModel is welded to one concrete implementation
class UserViewModel(private val api: RetrofitUserApi)

// Follows DIP: ViewModel depends on an interface, not a concrete class
interface UserRepository { suspend fun getUser(id: String): User }
class UserViewModel(private val repository: UserRepository)

class RemoteUserRepository(private val api: RetrofitUserApi) : UserRepository {
    override suspend fun getUser(id: String) = api.fetchUser(id)
}

In the first version, UserViewModel is hard wired to RetrofitUserApi, swapping networking libraries or writing a test means editing the ViewModel itself. In the second, UserViewModel only knows about the UserRepository interface, so a test can hand it an in memory fake, and production can hand it RemoteUserRepository, without the ViewModel changing at all. This is the actual mechanism dependency injection frameworks like Hilt or Koin are built to automate, they don't invent the idea, they just wire up which concrete implementation satisfies which interface at each injection point, so you can keep every class depending on interfaces and let the framework handle supplying the real thing.

Explain the Open/Closed Principle (O in SOLID).

Tier: CommonDifficulty: Easy

Open/Closed says a class should be open to extension but closed to modification, you should be able to add new behavior without editing code that already works.

// Violates OCP: adding a new shape means editing this function again
fun area(shape: Any): Double = when (shape) {
    is Circle -> Math.PI * shape.radius * shape.radius
    is Square -> shape.side * shape.side
    else -> throw IllegalArgumentException()
}

// Follows OCP: adding Triangle means writing a new class, not editing this one
interface Shape { fun area(): Double }
class Circle(val radius: Double) : Shape { override fun area() = Math.PI * radius * radius }
class Square(val side: Double) : Shape { override fun area() = side * side }

The when version has to be reopened and edited every time a new shape shows up, which risks breaking the existing cases every single time. The interface version lets you add Triangle : Shape as a brand new class, the existing Circle and Square never get touched, and anything already calling shape.area() keeps working exactly as before. ViewModelProvider.Factory is the same idea in the framework itself, the ViewModelProvider machinery never changes, but you extend it with your own factory to construct whatever ViewModel you need, new behavior added without modifying the class that does the providing.

Explain the Single Responsibility Principle (S in SOLID).

Tier: CommonDifficulty: Easy

Single Responsibility says a class should have exactly one reason to change, one job it's responsible for, not several unrelated ones bundled together.

// Violates SRP: fetching, parsing, and persisting all live in one class
class UserManager {
    fun fetchUserFromNetwork(id: String): User { /* ... */ }
    fun parseUserJson(json: String): User { /* ... */ }
    fun saveUserToDatabase(user: User) { /* ... */ }
}

// Follows SRP: each class owns one concern
class UserApi { fun fetchUser(id: String): User { /* ... */ } }
class UserDao { fun save(user: User) { /* ... */ } }
class UserRepository(private val api: UserApi, private val dao: UserDao) { /* coordinates the two */ }

The version with one UserManager class has three separate reasons to change, the network format, the parsing logic, and the storage schema, and a change to any one of them risks breaking the others because they're tangled in the same file. Splitting them means a change to how you talk to the API doesn't touch the database code at all. This is exactly why Android architecture guidance separates a ViewModel, which only holds and exposes UI state, from a Repository, which only coordinates data sources, from a Dao, which only talks to the database, each layer has one job, and each can be tested and changed independently of the others.

Explain the Interface Segregation Principle (I in SOLID).

Tier: CommonDifficulty: Medium

Interface Segregation says don't force a class to implement methods it doesn't actually need, break a large, do everything interface into smaller, focused ones instead.

// Violates ISP: a read-only repository still has to implement write()
interface DataSource {
    fun read(id: String): Data
    fun write(data: Data)
    fun delete(id: String)
}

// Follows ISP: implement only what you actually support
interface Readable { fun read(id: String): Data }
interface Writable { fun write(data: Data) }

class RemoteConfigSource : Readable {
    override fun read(id: String): Data { /* ... */ }
    // no write() or delete() to fake or throw from
}

With one fat DataSource interface, a read only source like remote config still has to provide a write() method, and the only honest thing it can do there is throw an exception or silently no-op, both of which are landmines for whoever calls it later expecting a working method. Splitting into Readable and Writable means RemoteConfigSource only implements what it genuinely supports, and the compiler itself enforces that a caller expecting only reads can't accidentally call a write that was never really there. This is the same instinct behind Kotlin's smaller collection interfaces, List doesn't force you to implement add() the way a single mutable collection interface would, read only and mutable capabilities are kept separate on purpose.

Explain the Liskov Substitution Principle (L in SOLID).

Tier: CommonDifficulty: Medium

Liskov Substitution says a subclass should be usable anywhere its parent type is expected, without the caller noticing anything is wrong or breaking.

open class Bird { open fun fly() { /* ... */ } }
class Sparrow : Bird()
class Penguin : Bird() {
    override fun fly() {
        throw UnsupportedOperationException("Penguins can't fly") // violates LSP
    }
}

fun letItFly(bird: Bird) = bird.fly() // silently breaks for a Penguin

Penguin compiles fine and satisfies the type system, but it breaks the actual contract, any code that calls bird.fly() on a Bird reasonably expects that to work, and it explodes at runtime the moment the bird happens to be a Penguin. That's Liskov being violated even though the inheritance is syntactically legal. The fix is usually to rethink the hierarchy, fly() doesn't belong on Bird at all if not every bird can do it, it belongs on a narrower FlyingBird interface that only flying birds implement. This is the principle that most directly explains why favoring interfaces over deep inheritance chains tends to age better, a small, honest interface is much harder to violate than a broad base class making promises some subclasses can't keep.

Creational Patterns

Explain the Factory pattern.

Tier: EssentialDifficulty: Easy

Factory hides object construction behind a method, so the caller asks for what it needs without knowing, or caring, which concrete class actually gets built.

interface Notification { fun send(message: String) }
class EmailNotification : Notification { override fun send(message: String) { /* ... */ } }
class PushNotification : Notification { override fun send(message: String) { /* ... */ } }

class NotificationFactory {
    fun create(type: String): Notification = when (type) {
        "email" -> EmailNotification()
        else -> PushNotification()
    }
}

The caller works entirely against the Notification interface, it never sees EmailNotification or PushNotification directly, so adding a new notification type later means adding a new class and a new when branch, not touching every place that creates one. The Android framework example most people already know is ViewModelProvider.Factory, it exists purely because ViewModel construction sometimes needs arguments, a repository, a saved state handle, that the framework itself can't provide, so you hand it a factory that knows how to build your specific ViewModel. LayoutInflater is another one, it takes an XML resource ID and returns you a fully constructed View tree without you ever calling a View constructor yourself.

Explain the Singleton pattern.

Tier: EssentialDifficulty: Easy

Singleton guarantees a class has exactly one instance for the whole application, and gives you one global point to access it.

public class Database {
    private static Database instance;
    private Database() { } // private, so no one else can construct it

    public static synchronized Database getInstance() {
        if (instance == null) {
            instance = new Database();
        }
        return instance;
    }
}

The private constructor is what actually enforces the guarantee, nothing outside the class can call new Database(), the only way in is getInstance(). You reach for it when creating a second instance would be wasteful or actively wrong, a database connection, a shared cache, a network client, anything expensive to construct or that needs one consistent piece of state across the whole app. Room's generated database class works exactly this way, so does a single OkHttpClient shared across your networking layer. In Kotlin you almost never hand write this, the object keyword compiles down to the same private constructor and static instance pattern for you.

Explain the Builder pattern (and the Builder pattern in Kotlin).

Tier: EssentialDifficulty: Medium

Builder constructs a complex object step by step through chained calls, instead of forcing one giant constructor with a dozen parameters, most of them optional.

AlertDialog dialog = new AlertDialog.Builder(context)
    .setTitle("Delete item")
    .setMessage("This can't be undone")
    .setPositiveButton("Delete", (d, w) -> deleteItem())
    .show();

AlertDialog.Builder and OkHttpClient.Builder are the two examples every Android developer has already used without necessarily naming the pattern, both configure an object with mostly optional settings and only build the real object at the end, when .build() or .show() is called. Each setter returns this, which is what makes the chaining work.

data class NetworkConfig(
    val baseUrl: String,
    val timeoutSeconds: Int = 30,
    val retries: Int = 3
)

NetworkConfig(baseUrl = "https://api.example.com", retries = 5)

Kotlin mostly makes the classic Builder unnecessary. Default parameter values plus named arguments give you the same readable, optional heavy construction without a separate builder class at all, you can skip any parameter that has a default and label the ones you do pass. The classic Builder still earns its place in Kotlin when construction genuinely needs multiple steps or validation between them, like Retrofit's Retrofit.Builder, which has to accumulate a base URL, a client, and converters before it can validate the whole configuration and produce a working instance.

How do you make a Singleton pattern thread-safe?

Tier: CommonDifficulty: Medium

The naive singleton breaks under concurrency because two threads can both see instance == null at the same time and each construct their own copy, and there are a few standard fixes, each with a different cost.

  • Synchronize the whole method. Simple and correct, but every call pays the locking cost forever, even after the instance already exists.
  • Double-checked locking. Check instance == null before and after taking the lock, so only the first, contended call actually synchronizes. Requires the field to be volatile, or another thread can see a partially constructed object.
  • Initialize eagerly. Create the instance at class load time instead of lazily. The JVM's class loading is already thread safe, so this needs no locking at all, at the cost of paying construction cost even if the singleton is never used.
  • Use an enum. A single element enum is thread safe, serialization safe, and reflection safe by construction, courtesy of the JVM's own class loading guarantees.
public enum Database {
    INSTANCE;
    public void query(String sql) { /* ... */ }
}

Most real code reaches for double-checked locking when construction genuinely needs to be lazy and expensive, or eager initialization when the cost of always constructing it is acceptable. The enum form is the one many effective Java style guides actually recommend as the safest default, since it closes off edge cases like reflection based attacks or accidental deserialization creating a second instance, though it's rare to see it used for anything beyond simple cases in real Android code.

Kotlin optional parameters vs the Builder pattern: which and when?

Tier: CommonDifficulty: Medium

Default for optional parameters with named arguments, and only reach for a Builder when construction needs multiple steps, validation between them, or has to be called comfortably from Java.

data class HttpRequest(
    val url: String,
    val method: String = "GET",
    val headers: Map<String, String> = emptyMap(),
    val timeoutSeconds: Int = 30
)

HttpRequest(url = "https://api.example.com", method = "POST")

This gives you everything the Builder pattern exists to provide, readable construction, sensible defaults, skip whatever you don't need, without a separate builder class and its boilerplate. It works because Kotlin has named arguments and default values built into the language, which Java never did, that gap is exactly why Java projects lean on Builder so heavily.

A Builder still earns its place when construction genuinely has steps that depend on each other, like Retrofit's Retrofit.Builder, which needs a base URL and a client accumulated before .build() can validate the whole configuration together, a single data class constructor can't express that kind of staged validation. It's also the right call when the API needs to be called cleanly from Java, since Java has no named or default arguments, a Kotlin data class with five optional parameters forces Java callers to pass all five positionally every time.

Structural Patterns

Explain the Repository pattern.

Tier: CommonDifficulty: Easy

Repository sits between your ViewModel and your actual data sources, network, database, cache, and gives the rest of the app one clean API to fetch and save data through, without caring where that data actually comes from.

class ArticleRepository(
    private val api: ArticleApi,
    private val dao: ArticleDao
) {
    fun getArticles(): Flow<List<Article>> = dao.getArticles()
        .onStart { emit(dao.getArticles().first()) }

    suspend fun refresh() {
        val fresh = api.fetchArticles()
        dao.insertAll(fresh)
    }
}

The ViewModel calls repository.getArticles() and has no idea whether that's coming from Room, a network call, or both, and that's exactly the point, the decision of local first versus network first, or how caching and refresh work, lives in one place instead of being scattered across every screen that needs the data. It also makes testing straightforward, a ViewModel test can hand in a fake repository instead of a real database and a real Retrofit client. It's one of the official recommended layers in Android's architecture guidance, sitting directly below the ViewModel and above your Room DAOs and Retrofit services.

Explain the Adapter pattern (and implement it).

Tier: CommonDifficulty: Medium

Adapter converts the interface of one class into another interface a client expects, so two things that weren't built to work together can, without changing either one.

class MyAdapter(private val items: List<String>) : RecyclerView.Adapter<MyAdapter.ViewHolder>() {
    class ViewHolder(val view: TextView) : RecyclerView.ViewHolder(view)

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = TextView(parent.context)
        return ViewHolder(view)
    }
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.view.text = items[position]
    }
    override fun getItemCount() = items.size
}

RecyclerView.Adapter is the example every Android developer already knows, RecyclerView only knows how to talk to a fixed interface, getItemCount(), onCreateViewHolder(), onBindViewHolder(), it has no idea your data is a List<String>, a Room query result, or a network response. Your adapter subclass is the translation layer, it adapts your specific data shape into the interface RecyclerView expects, and RecyclerView never needs to change to support a new kind of data. That's the pattern in general, you write a thin wrapper that speaks the target interface on one side and delegates to the incompatible thing on the other, rather than rewriting either side to match.

Behavioral Patterns

Explain the Strategy pattern.

Tier: CommonDifficulty: Easy

Strategy lets you swap an algorithm in and out at runtime by defining a family of interchangeable implementations behind one common interface, so the code that uses the algorithm never has to know which one it's actually running.

interface SortStrategy { fun sort(items: MutableList<Int>) }

class QuickSort : SortStrategy { override fun sort(items: MutableList<Int>) { /* ... */ } }
class MergeSort : SortStrategy { override fun sort(items: MutableList<Int>) { /* ... */ } }

class Sorter(private var strategy: SortStrategy) {
    fun setStrategy(strategy: SortStrategy) { this.strategy = strategy }
    fun sort(items: MutableList<Int>) = strategy.sort(items)
}

Sorter doesn't have a giant if/else picking between algorithms, it just delegates to whatever SortStrategy it currently holds, and that can be swapped at runtime without touching Sorter at all. Glide's DiskCacheStrategy is a real example already in your dependency graph, you pass in ALL, NONE, DATA, or RESOURCE and Glide plugs in a different caching algorithm behind the same loading pipeline without changing how you call it. It's also the pattern behind Android's own Comparator interface, you can hand a Collections.sort() call any comparison strategy you want, and the sort itself doesn't care which one it got.

Give examples of the Observer pattern used in Android.

Tier: CommonDifficulty: Easy

Android is full of Observer, once you know to look for it, it's the pattern underneath most of the framework's reactive and event driven APIs.

  • LiveData, an Activity or Fragment calls observe() and gets notified whenever the held value changes, and it's lifecycle aware, it stops delivering to a destroyed observer automatically.
  • Kotlin Flow, a collect {} block subscribes to a stream of emissions from upstream, the coroutine based successor to LiveData for most new code.
  • RecyclerView.Adapter, calling notifyDataSetChanged() or notifyItemChanged() notifies the RecyclerView that its backing data changed, so it knows to re-render.
  • BroadcastReceiver, an app registers to be notified when a system wide event happens, like the battery level changing or connectivity dropping.
  • View.OnClickListener and friends, the view is the subject, your listener is the observer, notified when a tap happens.
  • SharedPreferences.OnSharedPreferenceChangeListener, notified whenever a stored preference value changes.

In every one of these, the subject holds a list of listeners it knows nothing about beyond a shared interface, and just loops through and calls them when something happens. That's Observer's whole value, the subject and its observers can be written, tested, and changed independently of each other.

Explain the Observer pattern (and implement it).

Tier: CommonDifficulty: Medium

Observer lets one object, the subject, notify a list of dependents, the observers, whenever its state changes, without the subject needing to know anything about who's listening or what they'll do about it.

interface Observer { fun onUpdate(value: Int) }

class CounterSubject {
    private val observers = mutableListOf<Observer>()
    var count = 0
        set(value) {
            field = value
            observers.forEach { it.onUpdate(value) }
        }
    fun subscribe(observer: Observer) { observers.add(observer) }
}

Any number of observers can subscribe, and the subject just loops through them and calls onUpdate(), it never checks what type of observer it's talking to or what the callback actually does. That decoupling is the whole point, you can add a new observer, a logger, an analytics tracker, a UI update, without touching the subject at all.

This is the pattern behind most of Android's reactive plumbing. LiveData is a subject, an Activity or Fragment observing it via observe() is the observer, and it's lifecycle aware on top, it stops delivering updates to a destroyed observer automatically. Kotlin Flow is the same idea built on coroutines, a collect {} block is an observer subscribing to a stream of emissions. BroadcastReceiver is an older, system level version of the same pattern, and RecyclerView.Adapter's notifyDataSetChanged() is a subject telling its observer, the RecyclerView itself, that its data changed.

Patterns in Android Libraries

This section is the one worth reading last and remembering first, because naming the pattern inside a library you already use is what makes the rest of the answers sound like experience rather than revision.

What design pattern is used in the Retrofit library source code?

Tier: CommonDifficulty: Medium

Retrofit is a small showcase of design patterns working together, at least four are doing real work in its source.

  • Builder. Retrofit.Builder() accumulates a base URL, an OkHttpClient, and converter factories across chained calls, and only validates and constructs the real Retrofit instance when .build() runs.
  • Factory. CallAdapter.Factory and Converter.Factory are how Retrofit stays extensible, they're how RxJava or coroutine support gets plugged in without touching Retrofit's core, each factory knows how to produce the adapter or converter for a given type.
  • Proxy. retrofit.create(ApiService::class.java) doesn't hand you a real implementation of your @GET/@POST interface at all, it hands you a dynamic proxy that intercepts every call, builds the actual HTTP request from your annotations, and only then talks to OkHttpClient.
  • Adapter. CallAdapter converts Retrofit's own Call<T> type into whatever return type you actually asked for, an RxJava Observable, a coroutine suspend function's result, and so on.
interface ApiService {
    @GET("users/{id}")
    suspend fun getUser(@Path("id") id: String): User
}

val retrofit = Retrofit.Builder()          // Builder
    .baseUrl("https://api.example.com")
    .addConverterFactory(GsonConverterFactory.create()) // Factory
    .build()

val api = retrofit.create(ApiService::class.java) // Proxy, not a real implementation

The Proxy piece is the one that surprises people most, you never actually write a class that implements ApiService, Retrofit generates a stand in at runtime using Java's dynamic proxy mechanism, and every method call on it gets intercepted and turned into an HTTP request based on the annotations.

What design patterns are used in Android, and which ones do you know about?

Tier: CommonDifficulty: Medium

Android's own framework and the libraries built on top of it use most of the classic patterns, and having a concrete anchor for each is what actually makes this answer land in an interview.

  • Builder, AlertDialog.Builder, NotificationCompat.Builder, OkHttpClient.Builder, Retrofit.Builder, all construct a complex object through chained, mostly optional calls.
  • Singleton, a Room database instance, and Glide's default shared instance per application.
  • Factory, ViewModelProvider.Factory for constructing ViewModels with dependencies, LayoutInflater for turning an XML resource into a View tree.
  • Observer, LiveData and Flow, RecyclerView.Adapter's notifyDataSetChanged(), BroadcastReceiver.
  • Adapter, RecyclerView.Adapter translating your data into what RecyclerView expects.
  • Facade, the Repository pattern hiding network, database, and cache behind one API.
  • Proxy, Retrofit generating a dynamic implementation of your API interface at runtime.
  • Strategy, Glide's DiskCacheStrategy swapping caching behavior behind one loading pipeline.

The pattern behind naming these well isn't memorizing a textbook definition for each, it's being able to point at the actual class in a library you've used and say what problem the pattern solved there. ViewModelProvider.Factory sticks in an interview because it's something most Android developers have written themselves, a made up example rarely lands the same way.

Less common, worth knowing

These come up less often. Skim them once you are comfortable with everything above.

Creational Patterns

What is a creational pattern?

Tier: Less commonDifficulty: Easy

A creational pattern deals with how objects get created, hiding the details of instantiation so calling code depends on what an object does rather than how it's built.

  • Singleton, guarantees exactly one instance exists, and gives one global point to reach it. A Room database instance is the standard Android example.
  • Factory, hides construction behind a method so the caller doesn't need to know which concrete class it's getting back. ViewModelProvider.Factory is the standard Android example.
  • Builder, constructs a complex object step by step through chained calls instead of one huge constructor. AlertDialog.Builder and OkHttpClient.Builder are both this.

The common thread across all three is decoupling, the code that uses an object shouldn't need to know the details of how that object came into existence, only that it did. That separation is what lets you change how something gets built, add caching, swap an implementation, add validation, without touching every place it's used.

Create a Singleton pattern without using Kotlin's default object implementation.

Tier: Less commonDifficulty: Medium

You hand write it the way Java always did, a private constructor plus a companion object holding the single instance, which is close to what object compiles down to anyway.

class AppDatabase private constructor(context: Context) {
    companion object {
        @Volatile private var instance: AppDatabase? = null

        fun getInstance(context: Context): AppDatabase =
            instance ?: synchronized(this) {
                instance ?: AppDatabase(context).also { instance = it }
            }
    }
}

The private constructor blocks anyone outside the class from creating a second instance directly. companion object gives you a class level home for getInstance() without needing a real instance to call it on. The synchronized block plus the double check on instance is the classic double-checked locking pattern, it avoids taking a lock on every single call, only the first, contended call pays that cost, and @Volatile makes sure a half constructed instance is never visible to another thread. Interviewers ask for this specifically because it forces you to reason about the exact mechanics object normally hides, thread safety, lazy construction, and why the private constructor matters, this is essentially the constructor-argument singleton pattern for something like a database instance that object alone can't express since object can't take constructor parameters.

Structural Patterns

Explain the Facade pattern.

Tier: Less commonDifficulty: Easy

Facade puts one simple interface in front of a complicated subsystem, so callers don't have to know or care how many moving parts are behind it.

class UserRepository(
    private val api: UserApi,
    private val dao: UserDao,
    private val cache: MemoryCache
) {
    suspend fun getUser(id: String): User {
        cache.get(id)?.let { return it }
        val local = dao.getUser(id)
        if (local != null) return local
        val remote = api.fetchUser(id)
        dao.insert(remote)
        cache.put(id, remote)
        return remote
    }
}

A ViewModel calling repository.getUser(id) doesn't know or need to know that behind that one call sits a memory cache check, a database lookup, and a network fallback, all coordinated in a specific order. That's exactly what makes the Repository pattern in Android a Facade, it's one clean entry point hiding a genuinely complicated subsystem, several data sources and the rules for choosing between them. WorkManager is another example, it presents one simple API for scheduling background work, but underneath it juggles JobScheduler, AlarmManager, and a GreedyScheduler, picking whichever the device actually supports, and you never see any of that complexity from the calling side.

What is a structural pattern?

Tier: Less commonDifficulty: Easy

A structural pattern deals with how classes and objects are composed into larger structures, keeping those structures flexible and easy to change without rewriting the pieces underneath.

  • Adapter, converts one interface into another the client expects, so incompatible pieces can work together. RecyclerView.Adapter translating your data into what RecyclerView needs is the standard Android example.
  • Facade, puts one simple interface in front of a complicated subsystem. The Repository pattern hiding network, database, and cache behind one getUser() call is the standard Android example.
  • Repository, a specific, widely used facade over your data sources, sitting between the ViewModel and Room or Retrofit.

Where a creational pattern is about how one object gets built, a structural pattern is about how several objects, often ones you didn't write yourself, get wired together into something the rest of the app can use cleanly. They're the patterns you reach for most often when integrating a third party library or a legacy piece of code into an app that expects a different shape.

Behavioral Patterns

What is a behavioral pattern?

Tier: Less commonDifficulty: Easy

A behavioral pattern deals with how objects communicate and share responsibility, defining the rules for how a request or a change flows between them without hard wiring every object to know about every other.

  • Observer, one object notifies a list of dependents when its state changes, without knowing who they are or what they'll do about it. LiveData and Flow are the standard Android examples.
  • Strategy, an interchangeable algorithm behind one common interface, swappable at runtime. Glide's DiskCacheStrategy is a real example already in most Android dependency graphs.

Where creational patterns are about building objects and structural patterns are about composing them, behavioral patterns are about runtime communication, who calls whom, in what order, and how a change in one place propagates to everywhere it needs to be heard. That's why so much of Android's reactive UI plumbing, LiveData, Flow, click listeners, is really behavioral pattern in disguise, the whole job is getting an event from where it happens to everywhere that cares.

Patterns in Android Libraries

What design pattern is used in Glide (image loading library)?

Tier: Less commonDifficulty: Medium

Glide leans on several patterns at once, and the fluent API most people call it by, Glide.with(context).load(url).into(imageView), is the most visible one.

  • Builder. That chained with().load().into() call is building up a request step by step before Glide actually fires it off.
  • Singleton. Glide keeps one shared instance per application by default, holding the memory cache and the bitmap pool that every load request draws from.
  • Factory. ModelLoaders and ResourceDecoders are produced by factories internally, which is exactly how Glide supports loading from a URL, a file, a resource ID, or a custom model type through the same load() call.
  • Strategy. DiskCacheStrategy lets you swap the caching algorithm, ALL, NONE, DATA, RESOURCE, behind the same loading pipeline, without Glide's core logic changing at all.
  • Observer. LifecycleListener hooks Glide's request into the Activity or Fragment lifecycle, so an image load automatically pauses, resumes, or cancels itself as the screen it belongs to changes state, which is exactly why you don't leak image loads when a Fragment is destroyed mid load.
Glide.with(context)                      // Singleton instance
    .load(imageUrl)
    .diskCacheStrategy(DiskCacheStrategy.ALL) // Strategy
    .into(imageView)

The lifecycle awareness is worth calling out specifically, it's the same idea LiveData uses, an Observer that Glide attaches internally by inspecting the Activity or Fragment you passed to with(), so requests tied to a destroyed screen just stop, without you writing any cleanup code yourself.

Explain how Android Architecture Components (ViewModel, LiveData, etc.) use design patterns behind the scenes.

Tier: Less commonDifficulty: Hard

Jetpack's architecture components aren't inventing new ideas, they're the classic patterns applied directly to the problems of surviving configuration changes and keeping UI state in sync.

  • ViewModel is a Singleton, scoped to a lifecycle owner instead of the whole app. The framework's internal ViewModelStore holds exactly one instance per screen, and hands you back the same one across a rotation instead of constructing a new one, that's the same guarantee a classic Singleton makes, just scoped narrower.
  • ViewModelProvider.Factory is a Factory. Whenever a ViewModel needs constructor arguments the framework can't supply on its own, a repository, a SavedStateHandle, you hand it a factory that knows how to build that specific ViewModel, and the framework calls it instead of calling new itself.
  • LiveData and Flow are Observer. A ViewModel holds the state, an Activity or Fragment subscribes with observe() or collect {}, and gets notified whenever that state changes, without the ViewModel knowing or caring who's listening.
  • Repository is a Facade, and often built with Dependency Inversion. It hides network, database, and cache behind one clean API, and the ViewModel depends on a Repository interface rather than a concrete Retrofit service, so the actual data source can be swapped or faked in tests.

Put together, a screen's data flows through a Repository (Facade) into a ViewModel (Singleton, scoped) that exposes it as LiveData or Flow (Observer), and the ViewModel itself was constructed by a Factory. None of these are Android inventions, they're standard object oriented patterns, Jetpack just packages them so you don't have to hand write the plumbing yourself.

What design patterns are used in AOSP (Android Open Source Project)?

Tier: Less commonDifficulty: Hard

AOSP itself, the platform code underneath the SDK you actually call, is built on the same patterns as the libraries on top of it, just one layer deeper.

  • Singleton, ActivityManagerService, PackageManagerService, and most of the system services running inside system_server exist as exactly one instance for the whole device, reachable through a Binder interface.
  • Proxy, all inter-process communication in Android runs through Binder, your app talks to a local proxy object, and the real work happens in a completely different process, the proxy marshals the call across the process boundary transparently.
  • Factory, LayoutInflater builds a View hierarchy from XML without your code ever calling a View constructor directly, and Instrumentation is what constructs your Activity instances behind the scenes.
  • Observer, ContentObserver watches a ContentProvider for changes, and the whole Intent/BroadcastReceiver system is a system wide publish-subscribe mechanism built on the same idea.
  • Command, an Intent itself works like a Command object, it bundles up an action and its data into one object that gets handed off and executed later, by whatever component the system resolves it to.

The Binder Proxy is worth knowing in more depth than the rest, since it's the mechanism that makes the whole platform work, every system service you call, getSystemService(Context.LOCATION_SERVICE) and the like, hands you back a local proxy stub, and calling a method on it silently serializes the call, sends it across a process boundary to the real service, and deserializes the result back, all without your code doing anything different than calling a normal method.