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?
Singleton
The Singleton pattern specifies that only a single instance of a class should exist with a global access point. This pattern works well when modeling real-world objects with only one instance. For example, if you have an object that makes network or database connections, having more than one instance of the project may cause problems and mix data. That’s why in some scenarios you want to restrict the creation of more than one instance.
The Kotlin object
keyword declares a singleton without needing to specify a static instance like in other languages:
object ExampleSingleton {
fun exampleMethod() {
// ...
}
}
When you need to access members of the singleton object, you make a call like this:
ExampleSingleton.exampleMethod()
Behind the scenes, an INSTANCE
static field backs the Kotlin object. So, if you need to use a Kotlin object from Java code, you modify the call like this:
ExampleSingleton.INSTANCE.exampleMethod();
By using object
, you’ll know you’re using the same instance of that class throughout your app.
The Singleton is probably the most straightforward pattern to understand initially but can be dangerously easy to overuse and abuse. Since it’s accessible from multiple objects, the singleton can undergo unexpected side effects that are difficult to track down, which is exactly what Future You doesn’t want to deal with. It’s important to understand the pattern, but other design patterns may be safer and easier to maintain.
Factory
As the name suggests, Factory takes care of all the object creational logic. In this pattern, a factory class controls which object to instantiate. Factory pattern comes in handy when dealing with many common objects. You can use it where you might not want to specify a concrete class.
Take a look at the code below for a better understanding:
// 1
interface HostingPackageInterface {
fun getServices(): List<String>
}
// 2
enum class HostingPackageType {
STANDARD,
PREMIUM
}
// 3
class StandardHostingPackage : HostingPackageInterface {
override fun getServices(): List<String> {
return ...
}
}
// 4
class PremiumHostingPackage : HostingPackageInterface {
override fun getServices(): List<String> {
return ...
}
}
// 5
object HostingPackageFactory {
// 6
fun getHostingFrom(type: HostingPackageType): HostingPackageInterface {
return when (type) {
HostingPackageType.STANDARD -> {
StandardHostingPackage()
}
HostingPackageType.PREMIUM -> {
PremiumHostingPackage()
}
}
}
}
Here’s a walk through the code:
- This is a basic interface for all the hosting plans.
- This enum specifies all the hosting package types.
-
StandardHostingPackage
conforms to the interface and implements the required method to list all the services. -
PremiumHostingPackage
conforms to the interface and implements the required method to list all the services. -
HostingPackageFactory
is a singleton class with a helper method. -
getHostingFrom
insideHostingPackageFactory
is responsible for creating all the objects.
You can use it like this:
val standardPackage = HostingPackageFactory.getHostingFrom(HostingPackageType.STANDARD)
It helps to keep all object creation in one class. If used inappropriately, a Factory class can get bloated due to excessive objects. Testing can also become difficult as the factory class itself is responsible for all the objects.
Structural Patterns
“So, when I open this class, how will I remember what’s it doing and how it’s put together?” – Future You
Future You will undoubtedly appreciate the Structural Patterns you used to organize the guts of your classes and objects into familiar arrangements that perform typical tasks. Adapter and Facade are two commonly-seen patterns in Android.
Adapter
A famous scene in the movie Apollo 13 features a team of engineers tasked with fitting a square peg into a round hole. This, metaphorically, is the role of an adapter. In software terms, this pattern lets two incompatible classes work together by converting a class’s interface into the interface the client expects.
Consider your app’s business logic. It might be a Product or a User or Tribble. It’s the square peg. Meanwhile, a RecyclerView
is the same basic object across all Android apps. It’s the round hole.
In this situation, you can use a subclass of RecyclerView.Adapter
and implement the required methods to make everything work:
class TribbleAdapter(private val tribbles: List<Tribble>) : RecyclerView.Adapter<TribbleViewHolder>() {
override fun onCreateViewHolder(viewGroup: ViewGroup, i: Int): TribbleViewHolder {
val inflater = LayoutInflater.from(viewGroup.context)
val view = inflater.inflate(R.layout.row_tribble, viewGroup, false)
return TribbleViewHolder(view)
}
override fun onBindViewHolder(viewHolder: TribbleViewHolder, itemIndex: Int) {
viewHolder.bind(tribbles[itemIndex])
}
override fun getItemCount() = tribbles.size
}
RecyclerView
doesn’t know what a Tribble is, as it’s never seen a single episode of Star Trek, not even the new movies. :] Instead, it’s the adapter’s job to handle the data and send the bind
command to the correct ViewHolder
.
Facade
The Facade pattern provides a higher-level interface that makes a set of other interfaces easier to use. The following diagram illustrates this idea in more detail:
If your Activity needs a list of books, it should be able to ask a single object for that list without understanding the inner workings of your local storage, cache and API client. Beyond keeping your Activities and Fragments code clean and concise, this lets Future You make any required changes to the API implementation without impacting the Activity.
Square’s Retrofit is an open-source Android library that helps you implement the Facade pattern. You create an interface to provide API data to client classes like so:
interface BooksApi {
@GET("books")
fun listBooks(): Call<List<Book>>
}
The client needs to call listBooks()
to receive a list of Book
objects in the callback. It’s nice and clean. For all it knows, you could have an army of Tribbles assembling the list and sending it back via transporter beam. :]
This lets you make all types of customizations underneath without affecting the client. For example, you can specify a customized JSON deserializer that the Activity has no clue about:
val retrofit = Retrofit.Builder()
.baseUrl("http://www.myexampleurl.com")
.addConverterFactory(GsonConverterFactory.create())
.build()
val api = retrofit.create<BooksApi>(BooksApi::class.java)
Notice the use of GsonConverterFactory
, working behind the scenes as a JSON deserializer. With Retrofit, you can further customize operations with Interceptor
and OkHttpClient
to control caching and logging behavior without the client knowing what’s going on.
The less each object knows about what’s going on behind the scenes, the easier it’ll be for Future You to manage changes in the app.