Introduction to Kotlin Object-Oriented Programming

May 22 2024 · Kotlin 1.9.21, Android 14, Kotlin Playground & Android Studio Hedgehog | 2023.1.1

Lesson 01: Basics of Object-Oriented Programming

Demo

Episode complete

Play next episode

Next
Transcript

In this demo, you’ll take a quick look at the difference between procedural programming and object-oriented programming. You’ll be working with the Kotlin playground which is an interactive environment to run Kotlin code on the web.

You can find this at “play.kotlinlang.org” To get the starter code, open up the starter folder for this lesson. Copy the content of the file and use it to replace the code in the Kotlin playground.

This Playground has one import statement:

import java.util.UUID;

This library is used inside the TravelMode class to generate a unique random ID for a travel mode. Its a Java class and you won’t actually be using it but this is just to demonstrate that Kotlin is interoperable with Java.

These are the two locations you’ll be using for this demo:

val lagos = Pair(6.465422, 3.406448)
val london = Pair(51.509865, -0.118092)

These are pairs that you’ll use to represent the coordinates. The first argument of the pair is the latitude, while the second argument is the longitude.

First up is an example of a procedural programming function that computes the travel time between two location values. I haven’t written the actual code, but it would compute the point-to-point distance between the from and to values and use some average speed, probably assuming the travel mode is driving. For now, the function just returns a double value of 42.0.

Scroll down to the main function. Uncomment the call to the computeTravelTime function. Then update it to the following:

fun main() {
    println(computeTravelTime(from = lagos, to = london)) // Updated code
}

Here, you pass in the locations to the from and to arguments of the computeTravelTime function. For this demo, you’ll be using the println() function when calling functions and methods so you can see their results in the Playground’s console.

Go ahead and run the code. You can see the result in the console.

Now, suppose the programmer decides or is told, to modify the function so it’s more accurate for other travel modes, like walking, cycling or public transport.

You could add some parameters for average speed and actual distance like so:

fun computeTravelTime(
    from: Pair<Double, Double>,
    to: Pair<Double, Double>,
    averageSpeed: Double,
    actualDistance: Double
): Double {
    return actualDistance/averageSpeed
}

Making this change breaks every call to this function in the program and wherever it’s called. You now need to pass the calculation of actualDistance and averageSpeed arguments to the call. You probably need to include some branching code to set these new parameter values for the different travel modes.

Later, you might want to fine-tune the driving calculation to allow for traffic level and the average time to find parking. But these values aren’t relevant to the other travel modes, so you’d need to call a different function.

Okay, comment out the call to computeTravelTime().

Now, look at the TravelMode class. TravelMode is a very general concept that covers walking, cycling, driving, and public transport.

class TravelMode(private val mode: String, val averageSpeed: Double) {
  val id = UUID.randomUUID().toString()

It has a primary constructor with two properties: mode and averageSpeed. A constructor is used to initialize a class. This is what you call when creating an object, as you’ll see shortly.

mode could be walking, cycling, driving or public transport. After you create a TravelMode object, the app’s code can’t access the mode outside the class because it is marked as private. You’ll check this for yourself soon.

You also supply the value of averageSpeed when you create a TravelMode object. And the averageSpeed is an estimate that depends on the mode and user, or time-specific factors like how fast the user can walk or cycle, or whether the driving is on city streets or highways.

The extra requirement of the TravelMode class is the id property, which must be a unique value for each instance, and UUID.randomUUID() takes care of this without any fuss. You don’t need to know what its actual value is, it’ll just do its job quietly.

Inside the main function, instantiate TravelMode below the call to computeTravelTime function like so:

val sammy = TravelMode(mode = "walking", averageSpeed = 4.5)

That’s 4.5 km/hr.

Now, see if you can access mode:

println(sammy.mode)

Soon, you see an error message: Cannot access ‘mode’: it is private in ‘TravelMode’. This is an example of abstraction. Other parts of your app can interact with an object only through its public interface, and as you can see here, mode is marked as private in the class definition.

Comment out or delete the line with sammy.mode.

Back in TravelMode, look at the two methods:

fun actualDistance(from: Pair<Double, Double>, to: Pair<Double, Double>): Double {
  // use relevant map info for each specific travel mode
  throw IllegalArgumentException("Implement this method for ${mode}.")
}

fun computeTravelTime(from: Pair<Double, Double>, to: Pair<Double, Double>): Double {
  return actualDistance(from, to)/averageSpeed
}

computeTravelTime() looks just like the original procedural programming function, but it uses the actualDistance() method, which uses data that’s specific to each travel mode, and the averageSpeed that was used to instantiate the TravelMode object. Everything you need for computeTravelTime() is encapsulated in the object, and the app’s code just needs to provide the from and to locations.

actualDistance() is a placeholder function. Having a method like this means you shouldn’t create instances of this class. Instead, you should define a subclass and override this method to suit objects of that type. It’s a kind of back-door way of creating an abstract class. More on abstract classes in a future lesson.

Try calling it with your TravelMode object:

sammy.actualDistance(lagos, london)

Click the run button to execute the Playground.

There’s an error flag, and, in the console, this message appears:

Exception in thread "main" java.lang.IllegalArgumentException: Implement this method for walking.

If you don’t want your class to be “sort of abstract,” you could implement actualDistance() to return the exact point-to-point distance.

For now, comment out or delete this method call.

Leave actualDistance() as a placeholder because you’re about to create some subclasses!

Add in a walking subclass like so:

class Walking(mode: String, averageSpeed: Double): TravelMode(mode, averageSpeed) {

}

Writing :TravelMode after the class name means Walking is a TravelMode. It inherits all the properties and methods of TravelMode. This is inheritance at play. You can see the Walking class has the properties from the TravelMode parent class, and it can also have its additional properties unique to walking. The only thing you need to write is the method that isn’t implemented in TravelMode, and that’s the actualDistance() method.

But before you do that, go ahead and run the Playground. You’ll get this error in the console:

This type is final, so it cannot be inherited from

This is because, in Kotlin, classes are final, which means they can’t be inherited. So, to open it up for inheritance, you have to mark the class and any of its members you wish to override with the open keyword.

Let’s do that now:

open class TravelMode(private val mode: String, val averageSpeed: Double) {
  //...
  open fun actualDistance(from: Pair<Double, Double>, to: Pair<Double, Double>): Double {
    //...
  }
  //...
}

You marked both the class and the actualDistance() method open. You do this for the actualDistance() method because you want to override and implement it in the Walking class. Let’s do that now.

Add in the following code:

override fun actualDistance(from: Pair<Double, Double>, to: Pair<Double, Double>): Double {
  // use map info about walking paths, low-traffic roads, hills
  return 42.0
}

42.0 is only a placeholder value since you always have to return something. Notice you preceded the method’s definition with the override keyword. Failure to do so would lead to an error because this new method would hide the one in the parent class.

Go ahead and instantiate your subclass and print it out to the console:

val tim = Walking(mode = "walking", averageSpeed = 6.0)
println(tim)

Tim is younger and taller than Sammy, so I guess he walks faster.

Then call that stubborn method that refused to run earlier:

println(tim.actualDistance(from = lagos, to = london))

Also call computeTravelTime():

println(tim.computeTravelTime(from = lagos, to = london))

Run the Playground. actualDistance is 42, so travel time is 7 hours.

Also, you can see the walking object printed in the console with some strange values. That’s the hashcode of the object, and a hashcode is just a numeric representation of the contents of an object.

Now, go ahead and create one more subclass for the driving travel mode:

class Driving(mode: String, averageSpeed: Double): TravelMode(mode, averageSpeed) {
  override fun actualDistance(from: Pair<Double, Double>, to: Pair<Double, Double>): Double {
    // use map info about roads, tollways
    return 57.0
  }
}

57.0 is another placeholder value, different from Walking’s 42.0.

Instantiate a Driving object:

val car = Driving(mode = "driving", averageSpeed = 50.0)

50 km/hr is about right for averageSpeed, as much of the distance is on a highway.

And compute its travel time:

val hours = car.computeTravelTime(from = lagos, to = london)
println(car)
println("Hours: "+ hours)

Run the Playground.

The travel time value actually isn’t far off, considering it’s using placeholder values, but it’s different from the Walking object’s travel time, because it’s using its own version of actualDistance() and averageSpeed. That’s polymorphism in action! You could say the actualDistance() behavior exists in different forms depending on the type of travel mode.

So far, Driving works the same as Walking. How do you set it up to use traffic level and parking time?

Instead of overriding computeTravelTime(), you overload it. That is, you add parameters to create a different function signature. A Driving object can call this version of computeTravelTime(), but no other subclass type can.

Let’s add that in:

fun computeTravelTime(from: Pair<Double, Double>,
                      to: Pair<Double, Double>,
                      traffic: Double,
                      parking: Double): Double {
  return actualDistance(from, to)/averageSpeed * traffic + parking
}

You can see it has a different method signature from its parent class. This is called method overloading.

Alright, let’s try it out.

Add the following code under the print statement:

val realHours = car.computeTravelTime(
  from = lagos,
  to = london,
  traffic = 1.2,
  parking = 0.5
)
println("Actual Hours: " + realHours)

Run the Playground.

As you’d expect, travel time is now more than the inherited method’s calculation due to the new factors like traffic and parking time.

Do note that the hours here are in decimal values. So for example, if you multiply the realHours by 60, you’ll get the time in minutes.

Now, just check to see if tim can call this method:

Start typing…

tim.c

Nope, only the TravelMode method appears in the menu.

If you force it, by copy-pasting, then running the Playground, you get two errors that says kotlin can find the traffic and parking parameters in the method’s definition.

Go ahead and delete this call.

That ends this demo. Continue with the lesson for a summary.

See forum comments
Cinema mode Download course materials from Github
Previous: Instruction Next: Conclusion