Common Design Patterns and App Architectures for Android

Discover how to make your Android code cleaner and easier to understand with these common design patterns for Android apps. “Future You” will appreciate it! By Aaqib Hussain.

4.4 (10) · 1 Review

Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Decorator

The Decorator pattern dynamically attaches additional responsibilities to an object to extended its functionality at runtime. Take a look at the example below:

//1
interface Salad {
  fun getIngredient(): String
}

//2
class PlainSalad : Salad {
  override fun getIngredient(): String {
    return "Arugula & Lettuce"
  }
}

//3
open class SaladDecorator(protected var salad: Salad) : Salad {
  override fun getIngredient(): String {
    return salad.getIngredient()
  }
}

//4
class Cucumber(salad: Salad) : SaladDecorator(salad) {
  override fun getIngredient(): String {
    return salad.getIngredient() + ", Cucumber"
  }
}

//5
class Carrot(salad: Salad) : SaladDecorator(salad) {
  override fun getIngredient(): String {
    return salad.getIngredient() + ", Carrot"
  }
}

Here’s what the above code defines:

  1. A Salad interface helps with knowing the ingredients.
  2. Every salad needs a base. This base is Arugula & Lettuce thus, PlainSalad.
  3. A SaladDecorator helps add more toppings to the PlainSalad.
  4. Cumcumber inherts from SaladDecorator.
  5. Carrot inherts from SaladDecorator.

By using the SaladDecorator class, you can extend your salad easily without having to change PlainSalad. You can also remove or add any salad decorator on runtime. Here’s how you use it:

val cucumberSalad = Cucumber(Carrot(PlainSalad()))
print(cucumberSalad.getIngredient()) // Arugula & Lettuce, Carrot, Cucumber
val carrotSalad = Carrot(PlainSalad())
print(carrotSalad.getIngredient()) // Arugula & Lettuce, Carrot

Composite

You use the Composite pattern when you want to represent a tree-like structure consisting of uniform objects. A Composite pattern can have two types of objects: composite and leaf. A composite object can have further objects, whereas a leaf object is the last object.

Take a look at the following code to understand it better:

//1
interface Entity {
  fun getEntityName(): String
}

//2
class Team(private val name: String) : Entity {
  override fun getEntityName(): String {
    return name
  }
}

//3
class Raywenderlich(private val name: String) : Entity {
  private val teamList = arrayListOf<Entity>()

  override fun getEntityName(): String {
    return name + ", " + teamList.map { it.getEntityName() }.joinToString(", ")
  }

  fun addTeamMember(member: Entity) {
    teamList.add(member)
  }
}

In the code above you have:

  1. Component, an interface Entity in Composite pattern.
  2. A Team class implements an Entity. It’s a Leaf.
  3. Raywenderlich also implements an Entity interface. It’s a Composite.

Logically and technically the organization, in this case Raywenderlich, adds an Entity to the Team. Here’s how you use it:

val composite = Raywenderlich("Ray")
val ericTeamComposite = Raywenderlich("Eric")
val aaqib = Team("Aaqib")
val vijay = Team("Vijay")
ericTeamComposite.addTeamMember(aaqib)
ericTeamComposite.addTeamMember(vijay)
composite.addTeamMember(ericTeamComposite)
print(composite.getEntityName()) // Ray, Eric, Aaqib, Vijay

Behavioral Patterns

“So… how do I tell which class is responsible for what?” – Future You

Behavioral Patterns let you assign responsibility for different app functions. Future You can use them to navigate the project’s structure and architecture.

These patterns can vary in scope, from the relationship between two objects to your app’s entire architecture. Often, developers use several behavioral patterns together in the same app.

Command

When you order some Saag Paneer at an Indian restaurant, you don’t know which cook will prepare your dish. You just give your order to the waiter, who posts the order in the kitchen for the next available cook.

Similarly, the Command pattern lets you issue requests without knowing the receiver. You encapsulate a request as an object and send it off. Deciding how to complete the request is an entirely separate mechanism.

Greenrobot’s EventBus is a popular Android framework that supports this pattern in the following manner:

Flow chart that starts with a square block of a publisher, then points to the right to an Event Bus shown in a circle, the arrow connecting both has an Event square and a Post() method. The Event bus, has two arrows coming out and pointing to two subscribers,each labeled with a box, and the arrows also have events on them. The subscriber boxes trigger an OnEvent() method

An Event is a command-style object that’s triggered by user input, server data or pretty much anything else in your app. You can create specific subclasses which carry data as well:

class MySpecificEvent { /* Additional fields if needed */ }

After defining your event, you obtain an instance of EventBus and register an object as a subscriber. For example, if you register an Activity you’ll have:

override fun onStart() {
  super.onStart()
  EventBus.getDefault().register(this)
}

override fun onStop() {
  super.onStop()
  EventBus.getDefault().unregister(this)
}

Now that the object is a subscriber, tell it what type of event to subscribe to and what it should do when it receives one:

@Subscribe(threadMode = ThreadMode.MAIN)
fun onEvent(event: MySpecificEvent?) {
  /* Do something */
}

Finally, create and post one of those events based on your criteria:

EventBus.getDefault().post(MySpecificEvent())

Since so much of this pattern works its magic at run-time, Future You might have a little trouble tracing this pattern unless you have good test coverage. Still, a well-designed flow of commands balances out the readability and should be easy to follow later.

Observer

The Observer pattern defines a one-to-many dependency between objects. When one object changes state, its dependents get a notification and updates automatically.

This pattern is versatile. You can use it for operations of indeterminate time, such as API calls. You can also use it to respond to user input.

It was originally popularized by the RxAndroid framework, also known as Reactive Android. This library lets you implement this pattern throughout your app:

apiService.getData(someData)
  .subscribeOn(Schedulers.io())
  .observeOn(AndroidSchedulers.mainThread())
  .subscribe (/* an Observer */)

In short, you define Observable objects that will emit values. These values can emit all at once, as a continuous stream or at any rate and duration.

Subscriber objects will listen for these values and react to them as they arrive. For example, you can open a subscription when you make an API call, listen to the server’s response and react accordingly.

More recently Android also introduced a native way to implement this pattern through LiveData. You can learn more about this topic here.

Strategy

You use a Strategy pattern when you have multiple objects of the same nature with different functionalities. For a better understanding, take a look at the following code:

// 1
interface TransportTypeStrategy {
  fun travelMode(): String
}

// 2
class Car : TransportTypeStrategy {
  override fun travelMode(): String {
    return "Road"
  }
}

class Ship : TransportTypeStrategy {
  override fun travelMode(): String {
    return "Sea"
  }
}

class Aeroplane : TransportTypeStrategy {
  override fun travelMode(): String {
    return "Air"
  }
}

// 3
class TravellingClient(var strategy: TransportTypeStrategy) {
  fun update(strategy: TransportTypeStrategy) {
    this.strategy = strategy
  }

  fun howToTravel(): String {
    return "Travel by ${strategy.travelMode()}"
  }
}

Here’s a code breakdown:

  1. A TransportTypeStrategy interface has a common type for other strategies so it can be interchanged at runtime.
  2. All the concrete classes conform to TransportTypeStrategy.
  3. TravellingClient composes strategy and uses its functionalities inside the functions exposed to the client side.

Here’s how you use it:

val travelClient = TravellingClient(Aeroplane())
print(travelClient.howToTravel()) // Travel by Air
// Change the Strategy to Ship
travelClient.update(Ship())
print(travelClient.howToTravel()) // Travel by Sea