Object-Oriented Programming Best Practices with Kotlin
Learn how to write better code following Object Oriented Programming Best Practices with Kotlin and SOLID principles by developing a Terminal Android app. By Ivan Kušt.
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
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
Object-Oriented Programming Best Practices with Kotlin
25 mins
- Getting started
- Understanding Object-Oriented Programming?
- Classes and Objects
- Key Principles of OOP
- Understanding Encapsulation and Kotlin Classes
- Abstraction
- Inheritance and Polymorphism
- SOLIDifying your code
- Understanding the Single Responsibility Principle
- Understanding the Open Closed Principle
- Understanding the Liskov Substitution Principle
- Understanding the Interface Segregation Principle
- Understanding the Dependency Inversion Principle
- Kotlin Specific Tips
- Where to Go From Here?
Object-Oriented Programming (OOP) is the most popular computer programming paradigm. Using it properly can make your life, and your coworkers’, lives easier. In this tutorial, you’ll build a terminal app to execute shell commands on Android.
In the process, you’ll learn the following:
- Key principles of Object-Oriented Programming.
- SOLID principles and how they make your code better.
- Some Kotlin specific good-to-knows.
Also, if you’re completely new to Android development, read through our Beginning Android Development tutorials to familiarize yourself with the basics.
Also, if you’re completely new to Android development, read through our Beginning Android Development tutorials to familiarize yourself with the basics.
Getting started
To begin with, download the Kodeco Shell project using the Download Materials button at the top or bottom of this tutorial.
Open the starter project in Android Studio 2022.2.1 or later by selecting Open on the Android Studio welcome screen:
The app consists of a single screen similar to Terminal on Windows/Linux/MacOS. It lets you input commands and show their output and errors. Additionally, there are two actions, one to stop a running command and one to clear the output.
Build and run the project. You should see the main, and only, screen of the app:
Whoa, what’s going on here? As you can see, the app currently refuses to run any commands, it just displays a non-cooperative message. Therefore, your job will be to use OOP Best Practices and fix that! You’ll add the ability to input commands and display their output.
Understanding Object-Oriented Programming?
Before adding any code, you should understand what OOP is.
Object-Oriented Programming is a programming model based on data. Everything is modeled as objects that can perform certain actions and communicate with each other.
For example, if you were to represent a car in object-oriented programming, one of the objects would be a Car. It would contain actions such as:
- Accelerate
- Brake
- Steer left
- Steer right
Classes and Objects
One of the most important distinctions in object-oriented programming is between classes and objects.
Continuing the car analogy, a class would be a concrete car model and make you can buy, for example — Fiat Panda.
A class describes how the car behaves, such as its top speed, how fast it can accelerate, etc. It is like a blueprint for the car.
An object is an instance of a car, if you go to a dealership and get yourself a Fiat Panda, the Panda you’re now driving in is an object.
Let’s take a look at classes in KodecoShell app:
-
MainActivity
class represents the screen shown when you open the app. -
TerminalCommandProcessor
class processes commands that you’ll enter on the screen and takes care of capturing their output and errors. -
Shell
class executes the commands using Android runtime. -
TerminalItem
class represents a chunk of text shown on the screen, a command that was entered, its output or error.
MainActivity
uses TerminalCommandProcessor
to process the commands the user enters. To do so, it first needs to create an object from it, referred to as “creating an object” or “instantiating an object of a class”.
To achieve this in Kotlin, you use:
private val commandProcessor: TerminalCommandProcessor = TerminalCommandProcessor()
Afterward, you could use it by calling its functions, for example:
commandProcessor.init()
Key Principles of OOP
Now that you know the basics, it’s time to move on to the key principles of OOP:
- Encapsulation
- Abstraction
- Inheritance
- Polymorphism
These principles make it possible to build code that is easy to understand and maintain.
Understanding Encapsulation and Kotlin Classes
Data inside a class can be restricted. Make sure other classes can only change the data in expected ways and prevent state inconsistencies.
In short, the outside world doesn’t need to know how a class does something, but what it does.
In Kotlin, you use visibility modifiers to control the visibility of properties and functions inside classes. Two of the most important ones are:
-
private
: property or function is only visible inside the class where it’s defined. -
public
: default visibility modifier if none is specified, property or function is visible everywhere.
Marking the internal data of a class as private
prevents other classes from modifying it unexpectedly and causing errors.
To see this in action, open TerminalCommandProcessor
class and add the following import:
import com.kodeco.android.kodecoshell.processor.shell.Shell
Then, add the following inside the class:
private val shell = Shell(
outputCallback = { outputCallback(TerminalItem(it)) },
errorCallback = { outputCallback(TerminalItem(it)) }
)
You instantiated a Shell
to run shell commands. You can’t access it outside of TerminalCommandProcessor
. You want other classes to use process()
to process commands via TerminalCommandProcessor
.
Note you passed blocks of code for outputCallback
and errorCallback
parameters. Shell
will execute one of them when its process
function is called.
To test this, open MainActivity
and add the following line at the end of the onCreate
function:
commandProcessor.shell.process("ps")
This code tries to use the shell
property you’ve just added to TerminalCommandProcessor
to run the ps
command.
However, Android Studio will show the following error:
Cannot access 'shell': it is private in 'TerminalCommandProcessor'
Delete the line and return to TerminalCommandProcessor
. Now change the init()
function to the following:
fun init() {
shell.process("ps")
}
This code executes when the application starts because MainActivity
calls TerminalViews
‘s LaunchEffect
.
Build and run the app.
As a result, now you should see the output of the ps
command, which is the list of the currently running processes.
Abstraction
This is similar to encapsulation, it allows access to classes through a specific contract. In Kotlin, you can define that contract using interfaces.
Interfaces in Kotlin can contain declarations of functions and properties. But, the main difference between interfaces and classes is that interfaces can’t store state.
In Kotlin, functions in interfaces can have implementations or be abstract. Properties can only be abstract; otherwise, interfaces could store state.
Open TerminalCommandProcessor
and replace class
keyword with interface
.
Note Android Studio’s error for the shell property: Property initializers aren't allowed in interfaces
.
As mentioned, interfaces can’t store state, and you cannot initialize properties.
Delete the shell
property to eliminate the error.
You’ll get the same error for the outputCallback
property. In this case, remove only the initializer:
var outputCallback: (TerminalItem) -> Unit
Now you have an interface with three functions with implementations.
Replace init
function with the following:
fun init()
This is now an abstract function with no implementation. All classes that implement TerminalCommandProcessor
interface must provide the implementation of this function.
Replace process
and stopCurrentCommand
functions with the following:
fun process(command: String)
fun stopCurrentCommand()
Classes in Kotlin can implement one or more interfaces. Each interface a class implements must provide implementations of all its abstract functions and properties.
Create a new class ShellCommandProcessor
implementing TerminalCommandProcessor
in processor/shell
package with the following content:
package com.kodeco.android.kodecoshell.processor.shell
import com.kodeco.android.kodecoshell.processor.TerminalCommandProcessor
import com.kodeco.android.kodecoshell.processor.model.TerminalItem
class ShellCommandProcessor: TerminalCommandProcessor { // 1
// 2
override var outputCallback: (TerminalItem) -> Unit = {}
// 3
private val shell = Shell(
outputCallback = { outputCallback(TerminalItem(it)) },
errorCallback = { outputCallback(TerminalItem(it)) }
)
// 4
override fun init() {
outputCallback(TerminalItem("Welcome to Kodeco shell - enter your command ..."))
}
override fun process(command: String) {
shell.process(command)
}
override fun stopCurrentCommand() {
shell.stopCurrentCommand()
}
}
Let’s go over this step-by-step.
- You implement
TerminalCommandProcessor
interface. - You declare a property named
outputCallback
and use the override keyword to declare that it’s an implementation of property with the same name fromTerminalCommandProcessor
interface. - You create a private property holding a
Shell
object for executing commands. You pass the code blocks that pass the command output and errors tooutputCallback
wrapped inTerminalItem
objects. - Implementations of
init
,process
andstopCurrentCommand
functions call appropriateShell
object functions.
You need one more MainActivity
change to test the new code. So, add the following import:
import com.kodeco.android.kodecoshell.processor.shell.ShellCommandProcessor
Then, replace commandProcessor
property with:
private val commandProcessor: TerminalCommandProcessor = ShellCommandProcessor()
Build and run the app.