Classes are the core of Object-oriented programming (OOP). The most common analogy out there is that a class is like a blueprint. It will give you the structure or the details about something and you can then use this blueprint to build things.
I guess another analogy that works would be that a class is like a recipe. It will give you a list of ingredients, a structure, and directions on how to cook/bake something. If you have a cake recipe, it is not an actual cake, right? But you can use the recipe to build several cakes. So the recipe is the Class, and the cakes are the objects.
A fun exercise when you are studying OOP is to create classes for random things around you and see you can define their properties (characteristics/ state) and behaviour (functions).
For example, a class called Dog, or House.
Defining a Class
A class can be defined on its own file or together with other elements. That will depend on the structure of your project. One of the advantages of creating a class in a separate file is that it gives it room to grow as your app scales. It also improves readability.
A class is often declared in a file of the same name, although it’s not enforced. You can define multiple classes in the same file, and if you do so, you might want to use a name for the file that encompasses all the classes you are defining there, so the file name would be more generic.
To define a class all you need is the keyword class
and the name of your class:
class Dog
The code above won’t do anything, it’s pretty pointless, but you can already create dog objects from this class.
You can create a dog object and assign it to a variable like this:
fun main() {
val dog1 = Dog()
println(dog1)
}
class Dog
The output will be:
Dog@57829d67
Process finished with exit code 0
When you type val dog1 = Dog()
you are creating a Dog object. You are instantiating the Class Dog and calling its constructor by adding the () after the class name while assigning the instance of that class, the object, to a variable called dog1.
If you print the object you just created it will return the class name (Dog) plus the @ (at) symbol plus the memory location where the object is stored.
In Kotlin, every class definition created a corresponding type, so your dog1 variable is of type Dog.
The constructor does exactly what the name says: it constructs. It Builds an instance of that class and prepares it to be used.
Class Functions
BUT, classes are supposed to be useful, right? And this one is not doing anything. So let’s make it a bit more useful. Let’s give this class a body, so it’s not just a floating head. We can do that by adding a pair of curly braces { } after the class name and within the curly braces we can add our definitions.
Class definitions can specify two types of contents: behaviour, via functions, and data, via properties. So let’s define our dogs’ behaviour. Let’s start with a function called eat
because dogs need to eat.
fun main() {
val dog1 = Dog()
println(dog1.eat("raw meat"))
}
class Dog {
fun eat(food: String): String {
return "The dog is eating $food"
}
}
The class function (or member function – functions defined within a Kotlin class) eat
takes a parameter food of type string and returns a string that says what the dog is eating when you pass the type of food as an argument.
Then you can call the dog object you created (dog1) and call the eat function on that object passing the argument raw meat for the food parameter.
dog1 is happy 😀
When you add a behaviour to a class with a function (and/ or data) you are building a description of what that’s class can do and be and that description is visible to anyone with an instance of that class.
By default, all functions and properties without a visibility modifier are public, which means they are accessible from any file or function in your application. In some cases, you might want a function or a property to be accessible by everyone everywhere but as your application scales, so does your codebase complexity and you will probably have some class functions and properties you don’t want to be accessed from another location in your codebase. Hiding the implementation details that do not need to be visible from other parts of your code and exposing only what needs to be exposed will help ensure that the logic of your code is clear, concise and safer. This is called encapsulation, one of the four pillars of OOP.
While a public class function can be invoked anywhere in the application, a private class function can only be invoked within the class where it is defined.
A private class is that type of parents who share their kid’s pictures with family only and ask them not to share with anyone else. The public class is the type of parent who posts pics of their kids on Instagram in their public profile.
The visibility modifiers are very useful to determine who can see or access what.
Name | Description |
---|---|
Public | Accessible by code outside the class |
Private | Accessible only within the same class |
Protected | Accessible within the same class and its subclasses |
Internal | Accessible within the same module. |
Class Properties
While functions determine the behaviour of a class, properties define the data which are attributes required to represent the specific state or characteristics of a class.
If we continue our journey to create dogs, because the world needs more dogs, we could add a class property to give our dogs a name, for example. So we could have:
class Dog {
val name = "Snow"
fun eat(food: String): String {
return "The dog is eating $food"
}
}
We can do that either as a val
or a var depending on the usage. But, unlike regular variables, a class property must have a value when you declare it. So you can’t declare it like this: val name: String
. You will get an error saying the property needs to either be initialized or be abstract

Now that we have added a property called name to our Dog class we can access it:
fun main() {
val dog1 = Dog()
println("What's the dog's name?")
println(dog1.name)
println(dog1.eat("raw meat"))
}
class Dog {
val name = "Snow"
fun eat(food: String): String {
return "$name is eating $food"
}
}
We could also rename our dog, right? Nope, because we have set the property name as a val
, but if we change it to a var we could do that.

But

Ready, Get, Set, GO!
Properties model the characteristics of each instance of a class. They also provide a way for other things to interact with that data and this happens through getters and setters.
Every time you create a property, Kotlin generates (magically, behind the scenes) up to three components: a field, a getter (for vals
and vars
) and a setter (for vars
only). The field is where the data for that property is stored. A getter is how you read the data and a setter is how you change the data (hence it being generated only for vars since you cannot change vals
).
Think about this like a restaurant. You look at the menu and order spaghetti Marinara. You stay at your table while the chef prepares your dish and the waiter brings it to you on a nice plate, with cheese and basil.
You do not have access to the kitchen, and the waiter handles everything behind the scenes for you. You are like the caller, and the waiter is the getter.
That said, even though getters and setters are automagically generated by Kotlin so you don’t have to, you can if you want (or need) modify their behaviour to change how the data will be read or written.
So, for example, let’s say you are asking for user input for the dog’s name and you want to make sure the first letter of the name will always be capitalized. You could do something like this:
fun main() {
val dog1 = Dog()
println("What's the dog's name?")
dog1.name = readln()
println(dog1.eat("raw meat"))
}
class Dog {
var name = "Snow"
get() = field.replaceFirstChar { it.uppercase() }
fun eat(food: String): String {
return "$name is eating $food"
}
}
And you will have:

When you define a custom getter for a property, you change how the property works when it is accessed. Because name
contains a proper noun, you always want it to be capitalized when you reference it. This custom getter makes sure of that.
The field
identifier here points to the backing field that Kotlin manages for your property automatically. The backing field is the data that the getters and setters use to read and write the data in/from the memory that represents the property. Fields cannot be declared directly. So, when a property needs a backing field, Kotlin provides it automatically.
When the capitalized version of name
is returned, the backing field is not modified. If the value assigned to name
is not capitalized, as it is above, it remains lowercase after the getter does its work.
A setter, on the other hand, does modify the backing field of the property on which it is declared.
fun main() {
val dog1 = Dog()
println("What's the dog's name?")
dog1.name = readln()
println(dog1.name + "The Dog")
}
...
var name = "Snow"
get() = field.replaceFirstChar { it.uppercase() }
set(value) {field = value.trim()}
...

There you can see both the custom getter and setter in action. Note that even though I added a lot of white spaces after the name Rex they were trimmed because of the custom setter and there were no spaces between the name Rex and the “The Dog” text I added in the println function.
Property Visibility
Properties are different from variables defined locally within a function. When a property is defined, it is defined at the class level. As such, it may be accessible to other classes, if its visibility allows it. Over-permissive visibility can cause problems: If other classes have access to a class’s data, then any class in your application could make changes to that instance at will.
The visibility of a property matches the visibility of the class, which, by default, is public. So, if you don’t set any modifiers to the class or the properties they will be public by default (both getters and setters, regardless if they are custom or not).
What if I want access to a getter to be exposed but not the setter?
You can define the visibility separately like this:

As you can see, you are still able to access the getter and print the dog’s name provided in the class, but if you try to change it (via a setter) you get a red squiggly error line.

📌 Quick notes:
- You can set the visibility modifier like I did above, without settings a custom behaviour or you can do both like
private set(value) {field = value.trim()}
- I removed the getter from the code just to make it cleaner, but you can set both, or one, custom, not custom, mix and match 🧪
- A setter’s visibility cannot be more permissive than the property it is defined on. So, if the property is private, the setter cannot be public. If the property is protected the setter can either be protected too or private and so on.
- As for getter visibility, you cannot use visibility modifiers to make a getter’s visibility different from the property’s. You CAN set the visibility but it has to match the property’s so it’s kind of pointless 🤷🏼♀️
Now, let’s make things a bit more confusing, shall we? 😏
Computed Properties
Why?
Because life’s not easy!
So, when you define a property, a field is generated to store the value the property encapsulates. That is true…
Except when you have a computed property 😈
Let’s say you would like to add a title after the dog’s name based on some characteristics of its name.
You could add a function to do that. Fair enough. That said, in the world of classes, functions determine behaviour and properties determine data. And the title is data, so it really belongs in a property. BUT it needs to respond to changes based on the name. Using a computed property will allow you to keep this value in a property and ensure it is always up to date.
So, instead of having something like this:
private fun createTitle(name: String): String {
return when {
name.count() > 4 -> "The Fluffiest"
else -> "The master of dogs"
}
}
You can have this:
val title: String
get() = when {
name.count() > 4 -> "The Fluffiest"
else -> "The master of dogs"
}
And then you can use it like this:
fun main() {
val dog1 = Dog()
println("What's the dog's name?")
dog1.name = readln()
println("Please meet ${dog1.name}, the ${dog1.title}")
}

The value of title
is computed each time the property is accessed. It has no initial or default value – and no backing field to hold a value. If the dog’s name has changed, the value will automatically update so the title stays in sync with the dog’s name.