Kotlin Tutorial for Beginners: Build a Null-Safe Study Tracker

Learn core Kotlin by building one console project with nullable values, a data class, small functions and a collection pipeline whose output you can verify.

KnowledgeGate Team

Exam prep & CS education

Updated 14 Sep 20265 min read

Java-aware beginners can usually read braces and types, but Kotlin may introduce String?, primary constructors, expression bodies and collection pipelines too quickly. An Android-free console project stores four study sessions and produces exact, checkable output. Kotlin fundamentals come before framework choices; broader routes are available under Coding & Skills.

Read the smallest Kotlin program before adding abstractions

Start with one Kotlin console file:

kotlin
fun main() {
    val language = "Kotlin"
    val message = if (language.length == 6) "ready" else "check"
    println("$language: $message")
}

The output is Kotlin: ready. The keyword fun declares a function. Kotlin infers that language is a String, while val makes its reference read-only. $language and $message are string templates. Here, if is an expression because it produces the value assigned to message. Semicolons are not mandatory, and object construction later will not need new.

Run the file in a current local Kotlin/JVM project or the official Kotlin Playground. The project models a StudySession, formats it with a function, filters longer sessions through a list pipeline, and totals minutes with an extension.

Model one session with a data class and a nullable property

kotlin
data class StudySession(val topic: String, val minutes: Int, val note: String?)

Kotlin's official null-safety documentation distinguishes String, which cannot hold null, from String?, which can. The primary constructor defines all three properties, and each is a val because a study session is an immutable value.

The official data-class documentation explains that primary-constructor properties participate in generated members including toString(), equals() and hashCode(), component functions and copy():

kotlin
val original = StudySession("Functions", 25, "named arguments")
val revised = original.copy(minutes = 30, note = null)
println(original)
println(revised)

The exact values are:

Code
StudySession(topic=Functions, minutes=25, note=named arguments)
StudySession(topic=Functions, minutes=30, note=null)

copy created a revised value without mutating original. Kotlin's compact form still belongs in the wider class-and-object model explained in Object Oriented Technology Explained. It does not remove the need to understand encapsulation or object identity.

Make the nullable path explicit with safe calls and Elvis

kotlin
fun StudySession.label(prefix: String = "Study"): String =
    "$prefix: $topic - $minutes min - ${note?.uppercase() ?: "NO NOTE"}"

For StudySession("Null safety", 35, "redo Elvis operator"), note?.uppercase() produces REDO ELVIS OPERATOR. The fallback is skipped, so label(prefix = "Today") returns Today: Null safety - 35 min - REDO ELVIS OPERATOR.

For StudySession("Data classes", 50, null), the safe call produces null. The Elvis operator ?: selects NO NOTE, so label() returns Study: Data classes - 50 min - NO NOTE. These safe-call and fallback rules follow the official null-safety documentation.

The expression deliberately avoids note!!.uppercase(). Applying !! to null can produce a NullPointerException. Java-aware readers can compare related string concepts in String Handling in Java: Pool, Immutability, Interview Qs, but Java string behaviour is not evidence for Kotlin's nullable type rules.

Two label traces: a present note prints REDO ELVIS OPERATOR through the safe call, and a null note yields NO NOTE via the Elvis fallback.

Use functions to make intent visible

In StudySession.label, StudySession is the receiver type, label is the function name, and prefix = "Study" is a default parameter. String declares the return type. The expression after = is the entire function body. Kotlin's official functions documentation defines default parameters, named arguments and single-expression functions in these terms.

kotlin
val first = StudySession("Null safety", 35, "redo Elvis operator")
val second = StudySession("Data classes", 50, null)

first.label(prefix = "Today")
second.label()

fun validMinutes(minutes: Int): Boolean = minutes > 0

The first call uses a named argument to replace the default, while the second keeps Study. Function arguments are read-only inside the function. validMinutes(35) is true, and validMinutes(0) is false. Keeping validation separate from formatting gives each function one visible job.

Complete the console project with collections and an extension

Use this program:

kotlin
data class StudySession(val topic: String, val minutes: Int, val note: String?)

fun StudySession.label(prefix: String = "Study"): String =
    "$prefix: $topic - $minutes min - ${note?.uppercase() ?: "NO NOTE"}"

fun validMinutes(minutes: Int): Boolean = minutes > 0

fun List<StudySession>.totalMinutes(): Int = sumOf { it.minutes }

fun main() {
    val sessions = listOf(
        StudySession("Null safety", 35, "redo Elvis operator"),
        StudySession("Data classes", 50, null),
        StudySession("Functions", 25, "named arguments"),
        StudySession("Collections", 40, null)
    )

    val total = sessions.totalMinutes()
    val longTopics = sessions.filter { it.minutes >= 40 }.map { it.topic }
    println(sessions[0].label(prefix = "Today"))
    println(sessions[1].label())
    println("Total: $total min")
    println("Long sessions: $longTopics")
    val pace = if (total >= 120) "on track" else "add a session"
    println("Plan: $pace")
}

Its five output lines are:

Code
Today: Null safety - 35 min - REDO ELVIS OPERATOR
Study: Data classes - 50 min - NO NOTE
Total: 150 min
Long sessions: [Data classes, Collections]
Plan: on track

The read-only list keeps its construction order. The extension wraps sumOf, so 35 + 50 + 25 + 40 = 150. The filter keeps only 50 and 40, then map extracts their topics in order. Since 150 >= 120, the plan is on track. These operations follow Kotlin's official collection and extension documentation.

Data flow: the minutes >= 40 filter yields [Data classes, Collections] and totalMinutes sums to 150, so the plan reads on track.

Change one value and predict the result before running

Replace only the Functions entry with StudySession("Functions", 45, null). Write your prediction before running it. The total rises from 150 to 170, because 20 minutes were added. Long sessions becomes [Data classes, Functions, Collections], and the plan remains on track. The first line stays Today: Null safety - 35 min - REDO ELVIS OPERATOR. The unchanged second session still prints Study: Data classes - 50 min - NO NOTE.

Now isolate the nullable-number idea:

kotlin
val maybeMinutes: Int? = null
val safeMinutes = maybeMinutes ?: 0

safeMinutes is exactly 0. Change maybeMinutes to 45, and predict 45. This confirms that Elvis works with nullable numbers as well as strings. For more trace-and-predict practice, reproduce small programs across languages, but preserve each language's own type and null rules.

Common traps and how assessments probe them

Keep these distinctions precise:

  • String and String? are different types.

  • ?. propagates null, while ?: supplies a fallback.

  • !! can move a null failure to runtime.

  • A data-class copy() creates a new instance, but the copy is shallow when properties refer to mutable objects.

  • A val prevents reference reassignment. It does not make the contents of a mutable collection immutable.

  • map returns a transformed list rather than editing sessions.

Extensions allow member-like call syntax, but they do not modify the class, and their dispatch uses the declared receiver type. Honest assessment exercises can ask you to predict both null-safety labels, identify a declaration that fails to compile, calculate the filtered list and total, or choose the generated data-class members. Use the first-party Kotlin documentation as the authority for language behaviour.

Short version and the next step

  • Use nullable types deliberately.

  • Prefer ?. and ?: to unchecked !!.

  • Model transparent value records with data classes.

  • Keep functions small, using defaults and named calls where useful.

  • Use collection pipelines and extensions when they clarify transformations.

Remember 150 min, [Data classes, Collections], and on track. For adjacent JVM fundamentals, the Java course teaches the Java language in depth, and DSA using Java adds algorithm practice on the same JVM; both are Java courses, not Kotlin ones. Make the 25 -> 45 change and predict all five lines before running.