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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Common Design Patterns and App Architectures for Android
30 mins
- Getting Started
- Creational Patterns
- Builder
- Dependency Injection
- Singleton
- Factory
- Structural Patterns
- Adapter
- Facade
- Decorator
- Composite
- Behavioral Patterns
- Command
- Observer
- Strategy
- State
- App Architectures
- Types of App Architectures
- Model View Controller
- Model View ViewModel
- Clean Architecture
- Where to Go From Here?
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:
- A
Salad
interface helps with knowing the ingredients. - Every salad needs a base. This base is
Arugula & Lettuce
thus,PlainSalad
. - A
SaladDecorator
helps add more toppings to thePlainSalad
. -
Cumcumber
inherts fromSaladDecorator
. -
Carrot
inherts fromSaladDecorator
.
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:
-
Component
, an interfaceEntity
in Composite pattern. - A
Team
class implements an Entity. It’s aLeaf
. -
Raywenderlich
also implements an Entity interface. It’s aComposite
.
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:
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:
- A
TransportTypeStrategy
interface has a common type for other strategies so it can be interchanged at runtime. - All the concrete classes conform to
TransportTypeStrategy
. -
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