How I Fixed My Messy Android Code with 5 Simple SOLID Principles (Using Kotlin)
If your Android app is hard to manage, crashes often, or feels like a mess — these 5 simple ideas can help make it clean, stable, and easier to work with.
The Problem I Faced
When I first started building Android apps, I wrote everything inside one file.
My MainActivity
had code for:
-
Showing the UI
-
Sending network requests
-
Saving data
-
And even handling user login
It worked… until it didn’t.
Small changes started breaking things. It became hard to test. Bugs popped up everywhere. The app felt fragile.
The Solution: SOLID Principles
Then I found something called SOLID — 5 simple ideas that help you write better, easier-to-understand code.
They aren’t complicated. In fact, they made my life easier.
Let me explain each one, with real and simple examples in Kotlin, so you can start using them too.
What is SOLID?
SOLID is a group of five principles:
Letter | Name | Meaning |
---|---|---|
S | Single Responsibility | One class = one job |
O | Open/Closed | Add features without changing old code |
L | Liskov Substitution | Subclasses should work like parents |
I | Interface Segregation | Use smaller, focused interfaces |
D | Dependency Inversion | Depend on interfaces, not direct classes |
Now let’s look at them with clear examples.
1. S — Single Responsibility Principle
"Each class should do only one job."
Bad Example:
class LoginActivity {
fun login() {
// Validate user
// Call server
// Save login info
// Show home screen
}
}
This class is doing too much. If anything changes, we have to update this file again and again.
Better Example:
class Validator {
fun isValid(email: String, password: String): Boolean = email.isNotEmpty() && password.length >= 6
}
class AuthService {
fun login(email: String, password: String): Boolean = true // Simulate login
}
class SessionManager {
fun save(email: String) = println("Saving user: $email")
}
Now each class does one thing. Easy to test, easy to change.
2. O — Open/Closed Principle
"Your code should be easy to extend, not change."
Example:
Let’s say you want to support different login methods: email, Google, Facebook.
Without SOLID:
class AuthService {
fun login(type: String) {
if (type == "email") { /* email login */ }
else if (type == "google") { /* google login */ }
else if (type == "facebook") { /* facebook login */ }
}
}
Every time you add a new method, you have to change this class. Risky!
With SOLID:
interface LoginMethod {
fun login(): Boolean
}
class EmailLogin : LoginMethod {
override fun login() = true
}
class GoogleLogin : LoginMethod {
override fun login() = true
}
class AuthManager(private val method: LoginMethod) {
fun doLogin() = method.login()
}
Want to add Facebook? Just create a new class. No need to touch the old ones!
3. L — Liskov Substitution Principle
"Subclasses should work like their parent class."
Problem:
open class Notification {
open fun send() = println("Sending notification")
}
class SilentNotification : Notification() {
override fun send() {
throw Exception("Can't send") // Bad!
}
}
If we use SilentNotification
where we expect a Notification
, the app crashes.
Fix:
interface Notifier {
fun send()
}
class EmailNotifier : Notifier {
override fun send() = println("Email sent")
}
class LogOnlyNotifier : Notifier {
override fun send() = println("Just logging")
}
Now all classes work safely, no surprises.
4. I — Interface Segregation Principle
"Don't force classes to implement things they don't need."
Big, messy interface:
interface UserActions {
fun login()
fun logout()
fun deleteAccount()
}
What if a class only needs login()
? It still has to implement everything.
Small, focused interfaces:
interface CanLogin {
fun login()
}
interface CanDeleteAccount {
fun deleteAccount()
}
Now each class only implements what it needs.
5. D — Dependency Inversion Principle
"Depend on interfaces, not concrete classes."
Tight coupling:
class LoginViewModel {
val authService = FirebaseAuthService()
}
You can’t test this without real Firebase.
Loose coupling:
interface AuthService {
fun login(email: String, password: String): Boolean
}
class FirebaseAuthService : AuthService {
override fun login(email: String, password: String) = true
}
class LoginViewModel(private val auth: AuthService) {
fun login(email: String, password: String) = auth.login(email, password)
}
Now you can pass a mock service in tests. Easy and clean!
What Changed for Me
After using these 5 principles:
1) My app was easier to understand
2) Testing became simple
3) Adding new features was stress-free
4) Bugs were easier to find
5) I stopped dreading changes
Final Thoughts
You don’t need to learn everything at once. Start small:
-
Use Single Responsibility to break big classes into smaller ones
-
Create interfaces instead of calling other classes directly
-
Don’t let one class do all the work
Little by little, you’ll notice your code is easier to work with and your app is more stable.
Found This Helpful?
If this article helped you:
Clap (you can do it up to 50 times!)
Follow me for more simple Android and Kotlin tips
Subscribe if you want more beginner-friendly posts on architecture, clean code, and testing
Got questions or ideas? Leave a comment below — I reply to everyone.
Let’s make Android development easier — one class at a time.
Comments
Post a Comment