SOLID Principles for Android

Recently I had an interview experience where I went blank when the interviewer posed an Interesting Question – Explain SOLID Principles and how they are applicable in Android. Like many other blackboxes I use to skip while programming, I never bothered about learning the SOLID Principles. That interview was an eye opener for me and I took a while to understand the SOLID Principles and decided to share my findings and notes with the Android Community so that it might help someone. Let’s dive into knowing them without further delay.

Why to bother about SOLID Principles?

Before discussing about SOLID principles, here is the quote I found interesting and helpful in understanding why we need SOLID Principles.

Subscribe to our Newsletter!

Never miss Upcoming articles, free e-books, weekly updates on latest news from Android and Coding Community.

“The only constant in life is change”-Heraclitus.

As we all know that Software Industry is a fast paced industry with rapid changes, the components which we use today will become outdated only in matter of small time. Our app requirements changes as time passes. Our codebase will be continuously updated.

Let us imagine all our android code written into a single activity or fragment. While the class becomes lengthy, that is not the only case. We have to deal with several problems like modifying large part of the app or rewriting the entire app if your manager tells us to change business logic or switch from one component to another ( eg. sqlite to other db).

What separates from a novice programmer to an experienced programmer is this SOLID principles application and writing the code keeping them always in our mind.

SOLID unites all the best practices of software development over the years to deliver good quality apps. Understanding SOLID Principles will help us write clean and elegant code. It helps us write the code with SOC (Separation of Concerns).

What is the definition of SOLID?

SOLID is an acronym for five basic Object Oriented Principles –

S Stands for Single Responsibility Principle (SRP)

A class should have only one reason to change.

We can understand the above in this way: A class should have only one responsibility. If it is updating the view, it should be concerned about updating the view only. It should not contain the logic of calculations or data conversions. Let us see with an Android example using Kotlin.

override fun onBindViewHolder(postsViewHolder : ViewHolder, position: Int) {
val post = posts[position]
holder.title!!.text = post.title
holder.description!!.text = post.description
var hashtags = post.hashTags
val sb = StringBuilder()
hashtags.forEach {
sb.apply {
append(it)
append(",")
}
}
holder.hashtags.text = sb.toString()
}
view raw PostsAdapter.kt hosted with ❤ by GitHub

In the above example, we see that from line 7, we are iterating through hashtags and appending them to string. This puts the burden on the above method where its single responsibility is to update the view. Hence here it violates the Single Responsibility Principle. Therefore, to solve this, we will delegate that logic to other classes such as Utility classes. Now after modification, our method looks like:

override fun onBindViewHolder(postsViewHolder : ViewHolder, position: Int) {
val post = posts[position]
holder.title!!.text = post.title
holder.description!!.text = post.description
holder.hashtags.text = AppUtils.convertListToStr(post.hashTags)
}
view raw PostsAdapter.kt hosted with ❤ by GitHub

Another example is: using ViewModel which is a part of Lifecycle Library. Separating our app’s UI data from your Activity and Fragment classes lets us better follow the single responsibility principle: Our activities and fragments are responsible for drawing data to the screen, while our ViewModel can take care of holding and processing all the data needed for the UI.

O Stands for Open-Closed Principle

Software entities such as classes, functions, modules should be open for extension but closed for modification.

It means that whenever we are writing a new functionality, we should not modify existing code. We should rather write new code which will be used by existing code.

For example, in Android, we will write a custom adapter class on top of Android’s Adapter class for our implementation.

Let us see another Kotlin example:

package main.leetcode.kotlin
class Mariott {
private val basePrice = 2000
private val tax = 500
fun getPrice(): Int {
return basePrice + tax
}
}
class Taj {
private val basePrice = 3000
fun getPrice(): Int {
return basePrice
}
}
class PriceFactory {
fun calculatePrices(hotels : List<Any>) : Int {
var price = 0
hotels.forEach {
price += when (it) {
is Mariott -> {
it.getPrice()
}
is Taj -> {
it.getPrice()
}
else -> {
throw RuntimeException("Hotel Not Listed.")
}
}
}
return price
}
}
fun main() {
print(PriceFactory().calculatePrices(listOf(Mariott(), Taj())))
}

Here PriceFactory class is checking if the Hotel is Mariott or whether it is Taj. If it is not, it simply throws “Hotel Not Listed”. Hence every time we add a new Hotel, we need to touch PriceFactory Class to modify its content which violates Open/Closed Principle.

You may also find useful:

Android Interview Questions and Answers

To make it obey the above Principle, we need to modify the above example as shown in below:

package main.leetcode.kotlin
interface Hotel {
fun getPrice(): Int
}
class Mariott : Hotel {
private val basePrice = 2000
private val tax = 500
override fun getPrice(): Int {
return basePrice + tax
}
}
class Taj : Hotel {
private val basePrice = 3000
override fun getPrice(): Int {
return basePrice
}
}
class Hyatt : Hotel {
private val basePrice = 4000
private val parkingFee = 500
private val tax = 1000
override fun getPrice(): Int {
return basePrice + parkingFee + tax
}
}
class PriceFactory {
fun calculatePrices(hotels: List<Hotel>): Int {
var totalPrice = 0
hotels.forEach {
totalPrice += it.getPrice()
}
return totalPrice
}
}
fun main() {
print(PriceFactory().calculatePrices(listOf(Mariott(),Taj(),Hyatt())))
}

L stands for Liskov Substitution Principle (LSP)

The derived class must be usable through the base class interface, without the need for the user to know the difference.

Liskov Substitution Principle suggests that we can replace a Parent Class with a Child class without altering the correctness of the application. It was named of renowned computer scientist – Barbara Liskov. 

Let us take a standard example to understand. Let us examine where LSP fails.

open class Bird{
fun makeSound(){}
fun fly() {}
}
class Eagle : Bird()
class Penguin : Bird() // fails LSP because it cannot fly therefore has different behaviour and cannot call fly() method
view raw LSPFailScenario.kt hosted with ❤ by GitHub

In the above code block, we have a Bird class as open class. Now two birds – Eagle and Penguin are extending Bird feature and therefore inherits all the features of the bird. But we know that Penguin cannot fly. Therefore instead of properties we need to segregate classes by Behaviours. Let us next see how we can fix the above scenario.

open class Bird{
fun makeSound(){}
}
open class FlyingBird : Bird() {
fun fly() {}
}
class Eagle : FlyingBird()
class Penguin : Bird()
view raw LSPPassScenario.kt hosted with ❤ by GitHub

Here we separated the characteristics of a Flying bird and we extended Eagle class with it, thus obeying Liskov Substitution Principle. In other words, a subclass should override the parent class’s methods in a way that does not break functionality.

An elegant example in android is – using repository to fetch either from local or remote. We will only call Repository to fetch the data. We as a dependent on the Repo should not know whether the data is being fetched from local or remote.

I stands for Interface Segregation

Interface Segregation states that:

No client should be forced to depend on methods it does not use.

It means that a class should contain as many minimum methods as possible. Any interface that class inherits should consist of methods which are required by the class.

package main.leetcode.kotlin.solidPrinciples.InterfaceSegregation
enum class TYPE {
FAST_FOOD, DESSERT, INDIAN, CHINESE
}
interface Food {
fun name(): String
fun type(): TYPE
fun boil() : String
fun freeze(): String
}
class IceCream : Food {
override fun name(): String {
return "Vanilla"
}
override fun type(): TYPE {
return TYPE.DESSERT
}
override fun boil(): String {
// not required to call boil on foods of type Dessert. This method is additionally declared. That's the
// violation of Interface segregation principle.
TODO("not implemented")
}
override fun freeze(): String {
return "Freezing"
}
}
class Noodles : Food {
override fun name(): String {
return "Schezwan Chicken Noodles"
}
override fun type(): TYPE {
return TYPE.FAST_FOOD
}
override fun boil(): String {
return "Boiling"
}
override fun freeze(): String {
// not required to call boil on foods of type fast food. This method is additionally declared. That's the
// violation of Interface segregation principle.
TODO("not implemented")
}
}

Now let us fix the above code. We will segregate the interfaces of hot food items and cold food items from food.

package main.leetcode.kotlin.solidPrinciples.InterfaceSegregation
enum class TYPE {
FAST_FOOD, DESSERT, INDIAN, CHINESE
}
interface Food {
fun name(): String
fun type(): TYPE
}
interface ColdFood: Food {
fun freeze() : String
}
interface HotFood: Food {
fun boil() : String
}
class IceCream : ColdFood {
override fun name(): String {
return "Vanilla"
}
override fun type(): TYPE {
return TYPE.DESSERT
}
override fun freeze(): String {
return "Freezing"
}
}
class Noodles : HotFood {
override fun name(): String {
return "Schezwan Chicken Noodles"
}
override fun type(): TYPE {
return TYPE.FAST_FOOD
}
override fun boil(): String {
return "Boiling"
}
}

Here we can see that we introduced two additional interfaces which will help us reduce the methods which are not required. This helps us understanding Interface Segregation Principle.

D stands for Dependency Inversion

According to Robert C. Martin, Dependency Inversion Principle consists of two

1. High-level modules should not depend on low-level modules. Both should depend on abstractions.
2. Abstractions should not depend on details. Details should depend on abstractions.

To understand this example, let us take a common scenario from Android Development. User has an option to enable dark mode and if he clicked on it, we need to store his preference for future purpose. Currently our business asked us to store it in local. Hence we opted to store in shared preferences.

Below is the code snippet for it:

import android.content.Context
import android.os.Bundle
import android.widget.Button
import androidx.appcompat.app.AppCompatActivity
import com.coderefer.newyorktimesapp.R
class PrefActivity : AppCompatActivity() {
private val btnDarkMode: Button = Button(this)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_pref)
btnDarkMode.setOnClickListener {
val sharedPreferences = getSharedPreferences("darkModePref", Context.MODE_PRIVATE)
val editor = sharedPreferences.edit()
editor.putBoolean("darkMode", true)
editor.apply()
}
}
}
view raw PrefActivity.kt hosted with ❤ by GitHub

Now our business came with a new plan to change the logic to server. This will impact our existing code as we need to modify it.

To tackle this problem, let us divide our code base into 2 modules – UI module and Repository Module. This is a perfect example of Separation of Concerns (SOC). Now if we see our code, UI module depends upon Repository module.

Now lets alter the code to fit them into respective modules as follows. Our UI Module consists of Activity and ViewModel and the code is refactored as follows:

class PrefActivity : AppCompatActivity() {
lateinit var viewModelFactory: PrefViewModelFactory
private val btnDarkMode: Button = Button(this)
private lateinit var viewmodel: PrefViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_pref)
viewModelFactory = PrefViewModelFactory((application as DIExampleApp).repository)
viewmodel= ViewModelProvider(this, viewModelFactory).get(PrefViewModel::class.java)
btnDarkMode.setOnClickListener {
viewmodel.updateDarkMode(true)
}
}
}
view raw PrefActivity.kt hosted with ❤ by GitHub

class PrefViewModel(private val prefRepo: PrefRepo): ViewModel() {
fun updateDarkMode(enabled: Boolean) {
prefRepo.updateDarkMode(enabled)
}
}
view raw PrefViewModel.kt hosted with ❤ by GitHub

Now we moved our data model into PrefRepo as follows:

interface PrefRepo {
fun updateDarkMode(enabled: Boolean)
}
view raw PrefRepo.kt hosted with ❤ by GitHub

The implementation of it for our local data source is as follows:

class PrefLocalDataSourceImpl(val context: Context) : PrefRepo {
override fun updateDarkMode(enabled: Boolean) {
val sharedPreferences = context.getSharedPreferences("darkModePref", Context.MODE_PRIVATE)
val editor = sharedPreferences.edit()
editor.putBoolean("darkMode", true)
editor.apply()
}
}

If we carefully observe the above implementation, we can clearly see that our dependencies are inverted – our Repository Layer now depends on UI layer. Thus we created abstraction wherever we think the code changes are frequent thus making our code more maintainable.

The Dependency Inversion project is available in my GitHub repository. Drop a star if you find it useful.


Though SOLID Principles seems a bit overwhelming at the first time, learning them and using them as a Software developer makes our code more robust and maintainable protecting us from future changes. 

 

5 2 votes
Article Rating

Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments