Functions & Classes
Now that you're comfortable with variables and control flow, it's time to look at how Kotlin organizes code into functions and classes — the two building blocks you'll use to structure every TrailLog feature for the rest of this track.
Function Syntax
A Kotlin function starts with the fun keyword, followed by its name, a parenthesized
parameter list (each parameter written as name: Type), an optional return type, and a
body:
fun greet(name: String): String {
return "Hello, $name"
}
When a function's entire body is a single expression, Kotlin lets you drop the curly braces,
the return keyword, and often the return type too — this is called a
single-expression function:
fun greet(name: String) = "Hello, $name"
// return type "String" is inferred from the expression after "="
You'll see this shorthand constantly in idiomatic Kotlin code — small helper functions in particular tend to be written this way once you're used to reading it.
Default Parameters and Named Arguments
Kotlin functions can give parameters a default value, and callers can then omit that argument entirely. Combined with named arguments — passing arguments by parameter name instead of position — this genuinely reduces how often you need method overloading compared to Java, where achieving the same flexibility usually means writing several overloaded constructors or methods.
fun createTrail(
name: String,
notes: String = "",
distanceKm: Double = 0.0,
photoUri: String? = null
): Trail {
return Trail(name = name, notes = notes, distanceKm = distanceKm, photoUri = photoUri)
}
// called positionally, relying on defaults for the rest
val quickTrail = createTrail("Cedar Falls Trail")
// called with named arguments — order doesn't matter, and it's clear
// at the call site exactly what each value means
val detailedTrail = createTrail(
name = "Summit Pass",
distanceKm = 12.4,
notes = "Steep final mile, bring extra water"
)
Notice that detailedTrail supplies distanceKm and notes but
skips photoUri entirely, relying on its default of null — and it supplies
distanceKm before notes, which would be a compile error with plain
positional arguments, but works fine here because they're named.
Classes and Primary Constructors
In Java, a class's fields and its constructor are two separate pieces you write out fully. Kotlin lets you declare a class's primary constructor directly in the class header, collapsing field declarations and constructor parameters into one line:
class Trail(val name: String, val distanceKm: Double)
That single line declares a class with two read-only properties, name and
distanceKm, both automatically assigned from the constructor's arguments. The
equivalent in Java requires a field declaration, a constructor, and (if you want them accessible)
getter methods — three separate places to keep in sync instead of one.
You can still add a full body to a Kotlin class for additional logic, initialization code, or extra properties beyond the primary constructor:
class Trail(val name: String, val distanceKm: Double) {
val isLongHike: Boolean
get() = distanceKm > 15.0 // a custom getter — computed each time it's read
fun summary(): String = "$name ($distanceKm km)"
}
isLongHike here is a property with a custom getter instead of a
stored value — every time you read trail.isLongHike, Kotlin re-evaluates the
expression against the current distanceKm. Properties can have custom setters the same
way, using the set(value) { ... } syntax, useful when you need to validate or transform
a value before storing it.
Inheritance: Classes Are Final by Default
Here's a genuine design difference worth internalizing: in Java, any class can be subclassed unless
you explicitly mark it final. Kotlin inverts that default — every class is
final unless you mark it open. This is a deliberate choice: a class that
wasn't designed with subclassing in mind (with careful thought about which methods are safe to
override) can cause subtle bugs if extended anyway, so Kotlin makes you opt in.
open class Trail(val name: String, val distanceKm: Double) {
open fun summary(): String = "$name ($distanceKm km)"
}
class WaterfallTrail(name: String, distanceKm: Double) : Trail(name, distanceKm) {
override fun summary(): String = "${super.summary()} — features a waterfall"
}
Both the class itself (open class Trail) and the specific method being overridden
(open fun summary()) need the open keyword — a class being open doesn't
automatically make its members overridable. The subclass then uses override
explicitly, so it's always clear at a glance which functions are replacing inherited behavior.
Interfaces
Kotlin interfaces work much like Java's — they declare functions (and properties) that implementing classes must provide, and a class can implement multiple interfaces even though it can only extend one class.
interface Describable {
fun describe(): String
}
class Trail(val name: String, val distanceKm: Double) : Describable {
override fun describe(): String = "$name is a $distanceKm km trail"
}
Putting It Together
Here's a small, self-contained Trail class with a couple of methods, instantiated and
used — combining the primary constructor, a computed property, and a regular function:
class Trail(
val name: String,
val distanceKm: Double,
val notes: String = ""
) {
val difficulty: String
get() = if (distanceKm < 5.0) "Easy" else "Hard"
fun summary(): String = "$name — $distanceKm km ($difficulty)"
}
// instantiate and use it — note there's no "new" keyword in Kotlin
val trail = Trail(name = "Summit Pass", distanceKm = 12.4)
println(trail.summary()) // "Summit Pass — 12.4 km (Hard)"
new keyword when creating an
instance — you just call the class name like a function, as shown above.