Techbase Academy
Android · Lesson 11

Classes and Objects in Kotlin

9 phasesSchool Staff and Students RegisterAndroid Studio
Phase 1 of 9
Lesson Introduction

Welcome! Until now your programs have used loose variables and functions. That works for tiny programs, but real apps model real things: a student, a bank account, a bus route, a shopping basket. A phone app may handle thousands of such things at once. Kotlin gives you a tool to describe them neatly: the class.

Think of a class as the architect's plan for a building. The plan says "every house has a door, three windows and a roof". The actual houses built from the plan are called objects (or instances). One plan, many houses. In this lesson you will learn to write the plans, build the houses, and organise related plans into families.

By the end of this lesson you will be able to:

  • Define a class with properties and functions, and create objects from it.
  • Write primary and secondary constructors, default values and init blocks.
  • Use custom getters and setters, and control access with private, protected, internal and public.
  • Build class hierarchies with open, override and super.
  • Declare abstract classes and interfaces, and know when to choose which.
  • Use data class for classes that just hold information.
  • Use enum class for fixed sets of choices, and meet sealed class for closed hierarchies.
  • Use object and companion object for single shared instances.

How this fits the bigger picture: This lesson matches the Use classes and objects in Kotlin codelab in Unit 2 of Google's official Android Basics with Compose course. Everything on Android, from an Activity to a ViewModel to a Room database, is a class, so this lesson is the foundation for the rest of the course.

✏️ Your Task
Practise what you just learned about Lesson Introduction. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 2 of 9
Prerequisite Concepts

1. Variables, val and var (lesson 07)

val cannot be reassigned, var can. Both are used for properties inside classes too.

2. Functions (lesson 08)

A function inside a class is called a method (Kotlin people often still just say function). It works exactly like the functions you have written, except it belongs to an object and can use the object's data.

3. Conditionals (lesson 09)

You will use if and when inside methods and getters.

4. Nullability (lesson 10)

Properties can be nullable (String?). The ?. and ?: operators work on properties just as they do on variables.

✏️ Your Task
Practise what you just learned about Prerequisite Concepts. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 3 of 9
Conceptual Understanding: Classes, Objects and Why We Need Them

The plan and the houses

text
CLASS (the plan)                OBJECTS (the houses built from it)

 class Student                    Student("Ada", 70)
 -------------                    Student("Bola", 55)
 name   : String       ---->      Student("Chidi", 90)
 score  : Int
 describe()                       Each has its OWN name and score,
                                  but they all share the same plan.

A class bundles two things together:

Part Kotlin word Meaning
Data the object remembers property name, score, balance
Things the object can do method (member function) describe(), deposit()

Why is bundling useful? Compare keeping three students in loose variables (name1, score1, name2, score2, ...) with one Student plan you reuse. The class approach scales to a thousand students, groups related things so you cannot mix them up, and lets you add behaviour (such as isPassing()) in one place.

The four big ideas (in plain words)

  1. Encapsulation: an object keeps its data safe and only lets the outside world touch it in controlled ways, like a bank that lets you deposit but not simply type a bigger number in your balance.
  2. Inheritance: a new class can be built on an existing one and reuse its features, like a Dog being a kind of Animal.
  3. Abstraction: you can describe what something must do (an interface) without saying how, like "anything payable must be able to produce a fee".
  4. Polymorphism: code written for the general type works with every specific type, like a conductor collecting fares from every passenger however they pay.

You will see all four in action below.

✏️ Your Task
Practise what you just learned about Conceptual Understanding: Classes, Objects and Why We Need Them. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 4 of 9
Worked Examples: Writing Classes

Run these in the Kotlin Playground (play.kotlinlang.org) as in lesson 06.

Example 1: Your first class

kotlin
class Phone {
 var brand = "Tecno"
 var batteryPercent = 100
 fun call(number: String) {
 println("Calling $number on $brand")
 }
}
fun main() {
 val myPhone = Phone()
 myPhone.call("0803 000 1234")
 myPhone.batteryPercent = 80
 println("Battery: ${myPhone.batteryPercent}%")
}

Expected output:

Calling 0803 000 1234 on Tecno
Battery: 80%

Explanation:

  • class Phone { ... } is the plan. It has two properties and one method.
  • Phone() builds an object. Notice there is no new keyword in Kotlin.
  • myPhone.call(...) runs a method, and myPhone.batteryPercent = 80 changes a property. The val myPhone label cannot be pointed at a different phone, but the phone object's own var properties can still change.

If you print a plain object with println(myPhone), you get something like Phone@1b6d3586: the class name and a code that varies from run to run. That is rarely useful, and later we will see how data class fixes it.

Example 2: Primary constructors

Most classes need their values supplied when they are created. Kotlin lets you write the constructor right in the class header. Putting val or var in front of a parameter turns it into a property at the same time:

kotlin
class Student(val name: String, var score: Int = 0) {
 fun describe(): String = "$name has $score points"
}
fun main() {
 val ada = Student("Ada", 70)
 val bola = Student("Bola")
 bola.score += 15
 println(ada.describe())
 println(bola.describe())
}

Expected output:

Ada has 70 points
Bola has 15 points

Explanation:

  • val name makes a read-only property: you can read ada.name but ada.name = "X" will not compile.
  • var score makes a changeable property with a default value of 0, so Student("Bola") is allowed.
  • bola.score += 15 takes 0 and adds 15.
  • Named arguments from lesson 08 work with constructors too: Student(name = "Chidi", score = 90).

A parameter without val or var (such as class A(x: Int)) is only a constructor input. It is not stored as a property, though it can be used in init blocks and property initialisers.

Example 3: init blocks and secondary constructors

An init block runs right after the primary constructor, as the object is being built. A secondary constructor is an extra way to create the object. It must call the primary constructor with this(...).

kotlin
class Counter(val start: Int) {
 var value = start
 init {
 println("Counter created at $start")
 }
 constructor() : this(0) {
 println("Default counter")
 }
}
fun main() {
 val c1 = Counter(5)
 val c2 = Counter()
 println(c2.value)
}

Expected output:

Counter created at 5
Counter created at 0
Default counter
0

Explanation: Counter(5) runs the primary constructor, then the init block, giving the first line. Counter() calls the secondary constructor, which immediately delegates to this(0), so the primary constructor and init run first (line two), and only then does the secondary constructor's own body print "Default counter". Finally c2.value is 0. In everyday Kotlin, default parameter values usually replace secondary constructors, so you will not write many.

Example 4: Getters, setters and private set

A property is more than a variable: it comes with a hidden getter and setter. You can customise them.

kotlin
class Account(val owner: String) {
 var balance: Int = 0
 private set
 val isRich: Boolean
 get() = balance > 100000
 var nickname: String = ""
 set(value) {
 field = value.trim()
 }
 fun deposit(amount: Int) {
 if (amount > 0) {
 balance += amount
 }
 }
}
fun main() {
 val acc = Account("Ada")
 acc.deposit(50000)
 acc.deposit(-5)
 println("Balance: ${acc.balance}, rich: ${acc.isRich}")
 acc.deposit(60000)
 println("Balance: ${acc.balance}, rich: ${acc.isRich}")
 acc.nickname = "   Tee  "
 println("[${acc.nickname}]")
}

Expected output:

Balance: 50000, rich: false
Balance: 110000, rich: true
[Tee]

Explanation:

  • private set means code outside the class can read balance but cannot assign to it. Only deposit can change it. That is encapsulation: the bank's rule "no negative deposits" cannot be bypassed.
  • isRich has a custom getter and no stored value at all. It is recalculated every time it is read, so it can never be out of date.
  • In the nickname setter, field is the hidden storage behind the property. Writing field = ... stores the value. Writing nickname = ... inside the setter would call the setter again forever. value is the name for the incoming value.
  • The -5 deposit is silently ignored by the if, so the first line shows 50000.

Visibility modifiers

You decide who may see each property or function.

Modifier Who can use it
public (default) Everyone, everywhere
private Only code inside the same class (or the same file, for top-level items)
protected The class and its subclasses
internal Anything in the same module (for example, your app module)

Rule of thumb: make things as private as you can, and open them up only when something else needs them. A small public surface is easier to understand and change.

kotlin
class Safe(private val code: String) {
 fun open(attempt: String): Boolean = attempt == code
}
fun main() {
 val safe = Safe("1234")
 println(safe.open("0000"))
 println(safe.open("1234"))
 // println(safe.code)   // error: cannot access 'code': it is private
}

Expected output:

false
true

✏️ Your Task
Practise what you just learned about Worked Examples: Writing Classes. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 5 of 9
Worked Examples: Inheritance, Abstract Classes and Interfaces

Example 5: open, override and super

In Kotlin classes are closed by default: nobody can inherit from them unless you write open. The same goes for methods you want subclasses to change.

kotlin
open class Animal(val name: String) {
 open fun sound(): String = "..."
 fun introduce(): String = "$name says ${sound()}"
}
open class Dog(name: String) : Animal(name) {
 override fun sound(): String = "Woof"
}
class Puppy(name: String) : Dog(name) {
 override fun sound(): String = super.sound() + "!"
}
class Fish(name: String) : Animal(name)
fun main() {
 println(Dog("Rex").introduce())
 println(Puppy("Bit").introduce())
 println(Fish("Nemo").introduce())
}

Expected output:

Rex says Woof
Bit says Woof!
Nemo says ...

Explanation:

  • class Dog(name: String) : Animal(name) reads "Dog is an Animal, and passes name up to Animal's constructor".
  • override fun sound() replaces the parent's version. super.sound() calls the parent's version, so Puppy says "Woof" plus an exclamation mark.
  • Fish does not override sound, so it inherits the default "...".
  • introduce() is not open, so it cannot be overridden; it always calls whichever sound() belongs to the actual object. That is polymorphism: the same line of code in introduce behaves differently for a Dog and a Fish.

Example 6: Abstract classes

Sometimes the parent class is too general to exist on its own. "A shape" has no area until you say which shape. An abstract class can hold shared code and also declare functions that subclasses must supply. You cannot create an object of an abstract class directly.

kotlin
abstract class Shape(val label: String) {
 abstract fun area(): Double
 fun show() {
 println("$label area = ${area()}")
 }
}
class Rectangle(private val w: Double, private val h: Double) : Shape("Rectangle") {
 override fun area(): Double = w * h
}
class Triangle(private val base: Double, private val height: Double) : Shape("Triangle") {
 override fun area(): Double = 0.5 * base * height
}
fun main() {
 val shapes = listOf(Rectangle(4.0, 2.5), Triangle(6.0, 3.0))
 for (shape in shapes) {
 shape.show()
 }
 // val s = Shape("Blob")   // error: cannot create an instance of an abstract class
}

Expected output:

Rectangle area = 10.0
Triangle area = 9.0

Explanation: Shape forces every subclass to write area(). Members marked abstract are automatically open, so you do not write open as well. 4.0 times 2.5 is 10.0, and half of 6.0 times 3.0 is 9.0. The loop treats both objects as a Shape, which is polymorphism again.

Example 7: Interfaces

An interface is a contract: "anything that claims to be Payable can do these things". A class can extend only one parent class, but it can implement many interfaces. Interfaces cannot store state (no backing fields), but they can contain default implementations.

kotlin
interface Payable {
 val fee: Int
 fun pay(): String = "Paid ₦$fee"
}
class Tuition : Payable {
 override val fee = 45000
}
class Uniform : Payable {
 override val fee = 8500
 override fun pay(): String = "Paid ₦$fee for uniform"
}
fun main() {
 val bills = listOf<Payable>(Tuition(), Uniform())
 for (bill in bills) {
 println(bill.pay())
 }
}

Expected output:

Paid ₦45000
Paid ₦8500 for uniform

Explanation: Payable demands a fee property and gives pay() a default body. Tuition supplies only the fee and inherits pay(). Uniform supplies the fee and replaces pay(). Writing listOf<Payable>(...) states that the list holds anything payable. (The angle brackets are generics; lesson 13 covers them.)

Abstract class Interface
Can hold state (stored properties) Yes No
Constructor Yes No
A class can extend / implement Only one Many
Best for "Is a kind of..." with shared code "Can do..." capabilities
✏️ Your Task
Practise what you just learned about Worked Examples: Inheritance, Abstract Classes and Interfaces. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 6 of 9
Worked Examples: Data, Enums, Sealed Classes and Objects

Example 8: Data classes

Many classes exist only to carry information: a book, a user, a product. Write data before class and Kotlin generates useful members for you: toString(), equals(), hashCode(), copy() and destructuring.

kotlin
data class Book(val title: String, val pages: Int)
fun main() {
 val a = Book("Learn Kotlin", 120)
 val b = Book("Learn Kotlin", 120)
 println(a)
 println(a == b)
 println(a === b)
 val longer = a.copy(pages = 300)
 println(longer)
 val (title, pages) = a
 println("$title has $pages pages")
}

Expected output:

Book(title=Learn Kotlin, pages=120)
true
false
Book(title=Learn Kotlin, pages=300)
Learn Kotlin has 120 pages

Explanation:

  • println(a) prints a readable description instead of Book@....
  • == compares contents, so two books with the same title and pages are equal. === compares identity (are they the very same object in memory?), which is false here because we built two objects.
  • copy(pages = 300) makes a new book, changing only what you name. Because data classes are usually built from val properties, copy is how you "change" something without mutating the original. You will use this pattern constantly when handling state in Compose.
  • val (title, pages) = a is destructuring: it pulls out the properties in declaration order.

A data class needs at least one parameter in its primary constructor, and every parameter must be val or var.

Example 9: Enum classes

An enum class is for a fixed set of options: days, directions, order status. Each option is a constant object.

kotlin
enum class Day(val shortName: String, val isWeekend: Boolean) {
 SATURDAY("Sat", true),
 SUNDAY("Sun", true),
 MONDAY("Mon", false)
}
fun mood(day: Day): String = when (day) {
 Day.SATURDAY -> "Party time"
 Day.SUNDAY -> "Rest and church"
 Day.MONDAY -> "Back to work"
}
fun main() {
 for (day in Day.entries) {
 println("${day.name} (${day.shortName}) weekend=${day.isWeekend} position=${day.ordinal}")
 }
 println(mood(Day.MONDAY))
}

Expected output:

SATURDAY (Sat) weekend=true position=0
SUNDAY (Sun) weekend=true position=1
MONDAY (Mon) weekend=false position=2
Back to work

Explanation: Each constant is created with its constructor values. name is the constant's own name, ordinal is its position starting from zero, and Day.entries lists all constants in order (older code uses Day.values()). When you use when on an enum and cover every constant, the compiler knows the when is exhaustive, so no else is needed, and if you later add a new day the compiler tells you every when that must be updated. That safety net is the main reason to prefer an enum over loose strings like "sat".

Example 10: Sealed classes (an introduction)

An enum constant has one fixed shape. What if each option needs different data? A loading screen has no data, a success carries the result, and a failure carries an error message. A sealed class is a closed family of related classes: all subclasses are known when the code is compiled.

kotlin
sealed class LoadState {
 object Loading : LoadState()
 class Success(val data: String) : LoadState()
 class Failure(val reason: String) : LoadState()
}
fun message(state: LoadState): String = when (state) {
 is LoadState.Loading -> "Loading..."
 is LoadState.Success -> "Got: ${state.data}"
 is LoadState.Failure -> "Error: ${state.reason}"
}
fun main() {
 println(message(LoadState.Loading))
 println(message(LoadState.Success("12 products")))
 println(message(LoadState.Failure("No network")))
}

Expected output:

Loading...
Got: 12 products
Error: No network

Explanation: is LoadState.Success -> checks the type, and inside that branch state is smart-cast (lesson 10) so you can read state.data. Because the hierarchy is sealed, when is exhaustive without an else. You will use exactly this pattern for screen state in lessons 24 and 29. Sealed classes are for "one of several different shapes"; enums are for "one of several identical-shaped constants".

Example 11: object and companion object

Sometimes you need exactly one of something: app settings, a logger. An object declaration creates a class and its single instance in one step. A companion object attaches shared members to a class itself instead of to each instance (like static in Java).

kotlin
object AppConfig {
 const val NAME = "Techbase"
 var visits = 0
}
class Ticket(val seat: Int) {
 companion object {
 const val MAX_SEATS = 14
 fun first(): Ticket = Ticket(1)
 }
}
fun main() {
 AppConfig.visits++
 AppConfig.visits++
 println("${AppConfig.NAME} visits: ${AppConfig.visits}")
 val t = Ticket.first()
 println("Seat ${t.seat} of ${Ticket.MAX_SEATS}")
}

Expected output:

Techbase visits: 2
Seat 1 of 14

Explanation: You never write AppConfig(); you use the name directly, and everyone shares the same single object. const val is for compile-time constants (plain text or numbers) and is written in UPPER_SNAKE_CASE by convention. Ticket.first() and Ticket.MAX_SEATS are reached through the class name. Lesson 13 digs deeper into both forms of object.

✏️ Your Task
Practise what you just learned about Worked Examples: Data, Enums, Sealed Classes and Objects. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 7 of 9
Guided Practice Exercises
🎯 Your Challenge

Exercise 1: A Rectangle with computed properties

Write a class Rect(val width: Int, val height: Int) with two read-only computed properties, area and perimeter, written with custom getters. Create Rect(3, 4) and print its area and perimeter.

kotlin
class Rect(val width: Int, val height: Int) {
 val area: Int
 get() = width * height
 val perimeter: Int
 get() = 2 * (width + height)
}
fun main() {
 val r = Rect(3, 4)
 println("Area: ${r.area}")
 println("Perimeter: ${r.perimeter}")
}

Expected output:

Area: 12
Perimeter: 14

Explanation: 3 times 4 is 12, and twice (3 plus 4) is 14. Neither property has stored data, so they always match the current width and height.

🎯 Your Challenge

Exercise 2: Fix the inheritance

This program does not compile. Find the two missing keywords and fix it so that it prints Driving.

kotlin
class Vehicle {
 fun move(): String = "Moving"
}
class Car : Vehicle() {
 override fun move(): String = "Driving"
}
fun main() {
 println(Car().move())
}

Classes and functions are closed by default. Vehicle must be open so it can be inherited, and move must be open so it can be overridden.

kotlin
open class Vehicle {
 open fun move(): String = "Moving"
}
class Car : Vehicle() {
 override fun move(): String = "Driving"
}
fun main() {
 println(Car().move())
}

Expected output:

Driving

🎯 Your Challenge

Exercise 3: Traffic lights

Create an enum class Light(val action: String) with RED("Stop"), YELLOW("Get ready") and GREEN("Go"). Write fun next(light: Light): Light using when so that RED goes to GREEN, GREEN to YELLOW and YELLOW to RED. Print one line per light in the form RED: Stop, next is GREEN.

kotlin
enum class Light(val action: String) {
 RED("Stop"),
 YELLOW("Get ready"),
 GREEN("Go")
}
fun next(light: Light): Light = when (light) {
 Light.RED -> Light.GREEN
 Light.GREEN -> Light.YELLOW
 Light.YELLOW -> Light.RED
}
fun main() {
 for (light in Light.entries) {
 println("${light.name}: ${light.action}, next is ${next(light).name}")
 }
}

Expected output:

RED: Stop, next is GREEN
YELLOW: Get ready, next is RED
GREEN: Go, next is YELLOW

Explanation: entries follows declaration order (RED, YELLOW, GREEN). The when covers every constant, so no else is needed.

✏️ Your Task

Create a data class Product(val name: String, val price: Int). Make one product, print it, make a copy with a different price using copy, print both, and check whether they are equal with ==. Predict each line before running.

Phase 8 of 9
Common Beginner Mistakes (and How to Fix Them)

Mistake #1: Forgetting open. The wrong code: class Animal then class Dog : Animal(). The error: "This type is final, so it cannot be inherited from". The fix: write open class Animal, and open fun for any method a subclass should override.

Mistake #2: Forgetting override. The wrong code: declaring fun sound() in a subclass when the parent has open fun sound(). The problem: the compiler insists you say your intention, to protect you from accidental name clashes. The fix: write override fun sound().

Mistake #3: Trying to create an abstract class or an interface directly. The wrong code: val s = Shape("x"). The fix: create one of its concrete subclasses, such as Rectangle(...).

Mistake #4: Expecting val to freeze the whole object. The wrong code idea: val acc = Account("Ada") and then being surprised that acc.deposit(100) works. The explanation: val stops you pointing acc at a different object. It does not stop the object's own var properties from changing. To make an object truly unchangeable, give every property val.

Mistake #5: A setter that calls itself. The wrong code:

kotlin
var name: String = ""
 set(value) {
 name = value.trim() // calls the setter again, forever
 }

The fix: assign to field, not to the property name: field = value.trim().

Mistake #6: Using == on ordinary classes and expecting content comparison. The problem: for a normal class == compares identity unless you write equals. Two Student("Ada", 70) objects are not equal. The fix: make the class a data class when "same contents" should mean "equal".

Mistake #7: Writing data class with no parameters, or with plain parameters. The fix: every primary-constructor parameter of a data class must be marked val or var, and there must be at least one.

Mistake #8: Making everything public. The problem: any code can break your object's rules. The fix: use private by default, and private set for properties that others may read but only the class may change.

✏️ Your Task
Practise what you just learned about Common Beginner Mistakes (and How to Fix Them). Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 9 of 9
Knowledge Check

Question 1. What is the difference between a class and an object?

A class is the plan or template that describes properties and behaviour. An object (instance) is one actual thing built from that plan, with its own values for the properties. One class can produce many objects.

Question 2. What does the private set in var balance: Int = 0 followed by private set achieve?

Code outside the class can read balance but cannot assign to it. Only code inside the class (such as a deposit method) may change it. This is encapsulation: the class controls how its data changes.

Question 3. Why does the following fail to compile, and how do you fix it?

kotlin
class Bird {
 fun fly() = "Flap"
}
class Eagle : Bird()

Kotlin classes are final by default, so Bird cannot be inherited. Mark it open class Bird. (If Eagle also needed to change fly(), fly would need to be open as well.)

Question 4. Name two things a data class gives you for free, and one rule its constructor must follow.

Any two of: a readable toString(), content-based equals() and hashCode(), the copy() function, and destructuring declarations. The primary constructor must have at least one parameter, and every parameter must be marked val or var.

Question 5. When would you choose a sealed class over an enum class?

Choose a sealed class when the alternatives need to carry different data (for example Loading, Success(data) and Failure(reason)). Choose an enum when the alternatives are a fixed set of constants that all have the same shape. Both give you an exhaustive when without else.

Go deeper: official resources

✏️ Your Task
Practise what you just learned about Knowledge Check. Open Android Studio, type the examples by hand, change a value, and observe what happens.
🏗️ Build It — Mini Project
School Staff and Students Register

Project goal: Model a small school with a class hierarchy, an interface, an enum, a data class and a companion object, and print a tidy report.

Requirements:

  1. Create enum class Role(val title: String) with at least TEACHER, BURSAR and PRINCIPAL.
  2. Create interface Payable with val monthlyPay: Int.
  3. Create abstract class Person(val name: String) with an abstract fun describe(): String.
  4. Create class Staff that extends Person and implements Payable, with a role, and a describe() that returns text such as Teacher Mrs Ade earns ₦120000.
  5. Create class Student(val name: String, var score: Int) whose companion object keeps a count of how many students have been created (hint: use an init block). Only the class itself may change count (private set).
  6. Create data class Score(val subject: String, val mark: Int), then print one score and a copy of it with a changed mark.
  7. In main, create two staff members and two students, print every staff description, the total monthly pay, the student count and the scores.

Stretch goal: Add a method isPassing() to Student (a pass is 50 or more) and print each student's name with PASS or FAIL.

Main.kt
enum class Role(val title: String) {
 TEACHER("Teacher"),
 BURSAR("Bursar"),
 PRINCIPAL("Principal")
}
interface Payable {
 val monthlyPay: Int
}
abstract class Person(val name: String) {
 abstract fun describe(): String
}
class Staff(
 name: String,
 val role: Role,
 override val monthlyPay: Int
) : Person(name), Payable {
 override fun describe(): String = "${role.title} $name earns ₦$monthlyPay"
}
class Student(val name: String, var score: Int) {
 init {
 count++
 }
 fun isPassing(): Boolean = score >= 50
 companion object {
 var count = 0
 private set
 }
}
data class Score(val subject: String, val mark: Int)
fun main() {
 val staff = listOf(
 Staff("Mrs Ade", Role.TEACHER, 120000),
 Staff("Mr Bello", Role.BURSAR, 150000)
 )
 val ada = Student("Ada", 72)
 val bola = Student("Bola", 41)
 var total = 0
 for (member in staff) {
 println(member.describe())
 total += member.monthlyPay
 }
 println("Total monthly pay: ₦$total")
 println("Students enrolled: ${Student.count}")
 for (s in listOf(ada, bola)) {
 println("${s.name}: ${if (s.isPassing()) "PASS" else "FAIL"}")
 }
 val maths = Score("Maths", 65)
 println(maths)
 println(maths.copy(mark = 70))
}

Expected output:

Teacher Mrs Ade earns ₦120000
Bursar Mr Bello earns ₦150000
Total monthly pay: ₦270000
Students enrolled: 2
Ada: PASS
Bola: FAIL
Score(subject=Maths, mark=65)
Score(subject=Maths, mark=70)

Why it works: Staff inherits from Person (so it must implement describe) and implements Payable (so it must supply monthlyPay, which the constructor does with override val). The total is 120000 plus 150000, which is 270000. Each Student creation runs init, which bumps the shared count in the companion object, so after two students it is 2. Ada's 72 is at least 50 and Bola's 41 is not. The data class prints in the generated Score(subject=..., mark=...) format, and copy changes only the mark.

1

Open your finished project in Android Studio. Check that the .gitignore file the wizard created is in the project root. It keeps local.properties, build folders and keystores out of Git. Never commit passwords or signing keys.

2

Choose Git > Create Git Repository… (or VCS > Enable Version Control Integration in older versions), select the project folder, then open Git > Commit, tick every file, and write the message “Add Android lesson 11 project”.

3

On github.com click New repository, name it android-lesson-11-classes-and-objects-in-kotlin, set it to Public, leave Add a README unticked, and click Create repository.

4

Back in Android Studio choose Git > Manage Remotes…, add the repository URL as origin, then choose Git > Push… and push your main branch. Prefer the terminal? Run git remote add origin <your-url> then git push -u origin main.

5

Add a README.md with a screenshot of your app running and a short description. Employers and clients read READMEs first. Your project is now part of your public portfolio. 🎉

Lesson 11 complete! 🎉

You covered:

  • Lesson Introduction
  • Prerequisite Concepts
  • Conceptual Understanding: Classes, Objects and Why We Need Them
  • Worked Examples: Writing Classes
  • Worked Examples: Inheritance, Abstract Classes and Interfaces
  • Worked Examples: Data, Enums, Sealed Classes and Objects
  • Guided Practice Exercises
  • Common Beginner Mistakes (and How to Fix Them)
  • Knowledge Check
All 72 lessons in Google Android Studio ·Techbase Academy ·© 2025–2026 Techbase Consultant Services, Ibadan
Esc
↑ ↓ to moveEnter to openEsc to close