Open menu with table of contents Kotlin Introduction
Logo of Stuttgart Media University for light theme Logo of Stuttgart Media University for dark theme
Android Development

Kotlin Introduction

Kotlin for Java developers: as much as you need for this course

Stuttgart Media University

1 Agenda

  • Basic about Kotlin
  • Where Kotlin differs from Java
  • Basics: packages, fun main, val/var, string templates
  • if and when as expressions, ranges and for
  • Null safety
  • Classes: properties, data class, object, enum, sealed
  • Functions: default/named arguments, extension functions
  • Lambdas, function types and the trailing lambda syntax (= Compose)
  • Collections
  • Scope functions
  • Property delegation with by
  • suspend functions: a first look at coroutines
  • Interop with Java

All code examples are taken from or modelled on the MovieTracker app of the assignments.

2 Basic About Kotlin

  • First appeared: July 22, 2011 ("Project Kotlin"), Kotlin 1.0 released February 15, 2016
  • Designed and developed by JetBrains, open source (Apache 2.0)
  • Statically typed, compiles to JVM bytecode (also to JavaScript and native code via Kotlin Multiplatform)
  • Concise, null safe and 100 % interoperable with Java: Kotlin code calls Java libraries and vice versa
  • Google I/O, May 2017: first-class support for Kotlin on Android, shipped with Android Studio 3.0 (October 2017)
  • May 7, 2019: Google announces "Kotlin first", new Jetpack APIs are designed for Kotlin
  • Today Kotlin is the default language of new Android projects, Jetpack Compose is Kotlin only
  • This course uses Kotlin 2.4 (K2 compiler); the Android Gradle Plugin 9 ships the Kotlin compiler built in

3 Where Kotlin Differs From Java

Java Kotlin
final String s = "x"; val s = "x" (type inferred, no semicolons)
String s = null; compiles, NPE at runtime String cannot be null, String? can
Primitive int vs. boxed Integer Only Int, the compiler picks the primitive
new Movie(...) Movie(...)
Fields + getters + setters + constructor class Movie(val title: String)
record / Lombok for value classes data class
static Top-level functions, object, companion object
switch with break when expression, no fall-through
Ternary a ? b : c if (a) b else c (if is an expression)
Consumer<T>, Runnable, Function<A, B> Function types (T) -> Unit, () -> Unit, (A) -> B
Checked exceptions No checked exceptions
Classes and methods are open by default Classes and methods are final unless marked open
Utility classes with static helper methods Extension functions fun String.toYear(): Int

4 Basics: Package, Entry Point, Variables

package de.hdmstuttgart.movietracker.model   // like Java, but a file may contain several classes and functions

import kotlinx.coroutines.flow.Flow

fun main() {                       // top-level function, no class needed
    val title = "Dr. No"           // read-only (like final), type String is inferred
    var year = 1962                // mutable, type Int
    year += 1
    val poster: String? = null     // explicit type when it cannot be inferred (here: nullable)

    println("$title ($year)")                     // string template: $variable
    println("Title length: ${title.length}")     // ${expression}
}
  • Prefer val. Use var only when the value really changes
  • Int, Long, Double, Boolean, Char and String are classes, the compiler uses JVM primitives where possible
  • No implicit widening: val x: Long = 1 fails, write 1L or .toLong()
  • Any is the root type (like Object), Unit replaces void

5 if and when Are Expressions

// if returns a value, there is no ternary operator
val label = if (movies.isEmpty()) "No movies saved" else "${movies.size} movies"

fun maxOf(a: Int, b: Int) = if (a > b) a else b

// when replaces switch: no break, no fall-through, any type, ranges, else is required for expressions
val decade = when (movie.year.toInt()) {
    in 1962..1969 -> "Sixties"
    in 1970..1979 -> "Seventies"
    1995, 1997 -> "Brosnan years"
    else -> "Later"
}

// when without argument replaces an if / else-if chain (typical in Compose)
when {
    uiState.isLoading -> LinearProgressIndicator()
    uiState.errorMessage != null -> Text(text = uiState.errorMessage)
    else -> MovieList(movieList = uiState.movieList, onMovieClick = onMovieClick)
}
  • Because if/when are expressions, the last line of each branch is its value
  • when with a sealed type or an enum is checked for completeness by the compiler (see below)

6 Ranges and Loops

val titles = listOf("Dr. No", "From Russia with Love", "Goldfinger")

for (title in titles) println(title)                 // for-each, there is no C-style for (int i = 0; ...)

for (index in titles.indices) {                      // iterate over the indices
    println("$index: ${titles[index]}")
}

for ((index, title) in titles.withIndex()) {         // index and element at once
    println("$index: $title")
}

for (i in 1..5) print(i)               // 12345  inclusive range
for (i in 1..<5) print(i)              // 1234   end exclusive (also: until)
for (i in 5 downTo 1 step 2) print(i)  // 531

val year = 1964
if (year in 1962..1967) println("Connery era")       // in checks membership
  • while and do { } while () work exactly as in Java
  • Most loops are replaced by collection functions such as forEach, map and filter (see Collections)

7 Null Safety

var title: String = "Dr. No"
title = null                          // compile error: String is not nullable

var poster: String? = null            // nullable type
poster.length                         // compile error: poster might be null
poster?.length                        // safe call: Int?, null if poster is null
poster?.length ?: 0                   // Elvis operator: default value when null
poster!!.length                       // not-null assertion: throws NPE if null, avoid it

if (poster != null) println(poster.length)     // smart cast: poster is String inside the block

// SearchViewModel: OMDb omits "Search" when nothing was found, so SearchResponse.search is List<Movie>?
val movies: List<Movie> = response.search ?: emptyList()

// let runs the block only when the value is not null (SearchScreen)
searchUiState.errorMessage?.let { message ->
    Text(text = message, color = MaterialTheme.colorScheme.error)
}
  • In Java nullability is a comment or an @Nullable annotation, in Kotlin it is part of the type
  • Result: the NullPointerException becomes a compile error. Use !! only when you can prove non-null

8 Classes and Properties

// primary constructor and properties in one line, getters and setters are generated
class OfflineMovieRepository(private val movieDao: MovieDao)

class SearchUiState(val movieList: List<Movie>, var isLoading: Boolean = false)

val state = SearchUiState(emptyList())   // no `new`
println(state.movieList.size)            // property access calls the getter
state.isLoading = true                   // assignment calls the setter (only var has one)

// Classes are final by default. Inheritance and interfaces use `:`, overriding needs `override`
class HomeViewModel(private val movieRepository: MovieRepository) : ViewModel() {
    override fun onCleared() { /* ... */ }
}

interface MovieRepository {
    fun getAllMovies(): Flow<List<Movie>>          // abstract by default
}

open class Shape                                    // `open` allows subclassing
class Circle(val radius: Double) : Shape()
  • val in the constructor = read-only property, var = read/write property, without val/var = plain parameter
  • Visibility: public (default), private, protected, internal (module); no package-private

9 data class

// model/Movie.kt (Room and Gson annotations left out)
data class Movie(
    val imdbId: String,
    val title: String,
    val year: String,
    val poster: String
)

val drNo = Movie("tt0055928", "Dr. No", "1962", "https://...")
println(drNo)                                   // Movie(imdbId=tt0055928, title=Dr. No, year=1962, poster=...)
drNo == Movie("tt0055928", "Dr. No", "1962", "https://...")   // true: == calls equals(), === compares references
val remake = drNo.copy(year = "2026")           // copy with some properties changed
val (id, title) = drNo                          // destructuring via component1(), component2()
  • The compiler generates equals(), hashCode(), toString(), copy() and componentN() from the primary constructor
  • Java records are similar, but have no copy() and are always immutable
  • MovieTracker uses data classes for Movie, HomeUiState, SearchUiState and SearchResponse
  • Prefer val properties: an immutable Movie can be shared safely between threads and Compose

10 object and companion object

// object: a singleton, created on first access. Replaces a class with a private constructor + static instance
object Routes {
    const val HOME = "home"
    const val SEARCH = "search"
}
navController.navigate(Routes.SEARCH)

// companion object: the "static" part of a class (MovieDatabase.kt)
abstract class MovieDatabase : RoomDatabase() {
    companion object {
        const val DATABASE_NAME = "movie_database"

        fun getDatabase(context: Context): MovieDatabase { /* ... */ }
    }
}
val db = MovieDatabase.getDatabase(context)     // called like a static method
  • Kotlin has no static. Use top-level functions, object or companion object
  • const val is a compile-time constant (like static final), only for primitives and String
  • Companion objects are also the place for factory methods and constants such as intent extra keys (ResultActivity.EXTRA_CLICKS in assignment 1)

11 enum class, sealed class and sealed interface

enum class SortOrder { TITLE, YEAR }      // like Java enums, may have properties and functions

// sealed: all implementations are known at compile time, `when` needs no else branch
sealed interface SearchUiState {
    data object Loading : SearchUiState
    data class Success(val movieList: List<Movie>) : SearchUiState
    data class Error(val message: String) : SearchUiState
}

@Composable
fun SearchResult(uiState: SearchUiState, onMovieClick: (Movie) -> Unit) {
    when (uiState) {
        is SearchUiState.Loading -> LinearProgressIndicator()
        is SearchUiState.Success -> MovieList(movieList = uiState.movieList, onMovieClick = onMovieClick)
        is SearchUiState.Error -> Text(text = uiState.message)    // smart cast to Error
    }
}
  • is checks the type and smart casts, like instanceof with pattern matching in Java 16+
  • Sealed hierarchies model UI states: the compiler tells you when you forgot a case
  • MovieTracker keeps it simpler and uses one flat data class SearchUiState(movieList, isLoading, errorMessage); both styles are common

12 Functions: Default and Named Arguments

// default values replace overloaded methods
@Composable
fun MovieItem(movie: Movie, onMovieClick: (Movie) -> Unit, modifier: Modifier = Modifier) { /* ... */ }

MovieItem(movie, onMovieClick)                                      // modifier uses its default
MovieItem(movie = movie, onMovieClick = onMovieClick)               // named arguments
MovieItem(movie, onMovieClick, modifier = Modifier.padding(8.dp))   // positional, then named

// Every Compose function works like this. Text() has 15 parameters, you name the two you need:
Text(text = movie.title, style = MaterialTheme.typography.titleMedium)

// single-expression function: no braces, no return, type can be inferred
fun sum(a: Int, b: Int) = a + b
override fun getAllMovies(): Flow<List<Movie>> = movieDao.getAllMovies()

fun deleteMovie(movie: Movie): Unit { /* ... */ }    // Unit is void and can be omitted
  • Convention in Compose: required parameters first, modifier: Modifier = Modifier is the first optional one
  • Named arguments make calls readable and allow any order of the optional arguments

13 Extension Functions

// Add a function to an existing class without inheriting from it. `this` is the receiver object
fun String.toYearOrNull(): Int? = take(4).toIntOrNull()

"1962".toYearOrNull()         // 1962
"N/A".toYearOrNull()          // null

// ui/AppViewModelProvider.kt: a readable name for a long cast
fun CreationExtras.movieTrackerApplication(): MovieTrackerApplication =
    (this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as MovieTrackerApplication)

// used when the ViewModels are created
val Factory = viewModelFactory {
    initializer {
        HomeViewModel(movieTrackerApplication().container.movieRepository)
    }
}
  • Resolved statically: compiled to a static method with the receiver as first parameter, no access to private members
  • Replaces Java StringUtils.toYear(s) helper classes
  • Large parts of Android KTX and Compose are extension functions: Modifier.padding(), Modifier.clickable {}, Flow.collectAsStateWithLifecycle(), Context.startActivity()

14 Lambdas and Function Types

// A function type: takes a Movie, returns nothing (Unit)
val onMovieClick: (Movie) -> Unit = { movie -> println(movie.title) }
onMovieClick(drNo)                               // call it like a function

val titles = movies.map { it.title }             // `it` is the implicit name of a single parameter

// Higher-order function: a function that takes a function as parameter (ui/composables/MovieList.kt)
@Composable
fun MovieList(movieList: List<Movie>, onMovieClick: (Movie) -> Unit) { /* ... */ }

MovieList(movieList = movies, onMovieClick = { movie -> viewModel.deleteMovie(movie) })
MovieList(movieList = movies, onMovieClick = viewModel::deleteMovie)   // function reference

var counter = 0
val increment: () -> Unit = { counter++ }        // lambdas may modify captured vars (no "effectively final")
  • Java needs a different interface for each shape (Runnable, Consumer<T>, BiFunction<A, B, R>); Kotlin has one syntax: (A, B) -> R
  • The last expression of a lambda is its return value
  • (Movie) -> Unit is the type of every click handler in MovieTracker

15 Trailing Lambda Syntax: This Is All Compose Is

// If the last parameter is a function, the lambda may be written after the parentheses
Button(onClick = { counter++ }, content = { Text("Button") })
Button(onClick = { counter++ }) { Text("Button") }               // same call

// If the lambda is the only argument, the parentheses may be omitted
val connery = movies.filter({ it.actor == "Sean Connery" })
val connery = movies.filter { it.actor == "Sean Connery" }       // same call

// A Compose UI is nested function calls with trailing lambdas
Column(modifier = Modifier.padding(16.dp)) {        // content: @Composable () -> Unit
    Text(text = "Clicked $counter times")
    Button(onClick = { counter++ }) {
        Text(text = "Button")
    }
}

LazyColumn {                                        // content: LazyListScope.() -> Unit
    items(items = movieList) { movie ->             // items() is a function of LazyListScope
        MovieItem(movie = movie, onMovieClick = onMovieClick)
    }
}
  • LazyListScope.() -> Unit is a lambda with receiver: inside the braces this is a LazyListScope, so items(...) is a method call on it. Gradle KTS (android { }, dependencies { }) works the same way

16 Collections

val bondMovies = listOf(                             // List<Movie>: read-only interface
    Movie("Dr. No", "1962", "Sean Connery"),         // Movie(title, year, actor) of assignment 2
    Movie("Goldfinger", "1964", "Sean Connery"),
    Movie("GoldenEye", "1995", "Pierce Brosnan")
)
val watched = mutableListOf<Movie>()                 // MutableList<Movie>
watched.add(bondMovies[0])
watched += bondMovies[1]                             // operator overloading: plusAssign

bondMovies.filter { it.title.contains(searchText, ignoreCase = true) }   // List<Movie>
bondMovies.map { it.title }                          // List<String>
bondMovies.first { it.year == "1964" }               // Goldfinger, throws if nothing matches
bondMovies.firstOrNull { it.year == "2000" }         // null instead of an exception
bondMovies.any { it.actor == "Pierce Brosnan" }      // true
bondMovies.sortedBy { it.year }                      // new sorted list, the original is untouched
bondMovies.groupBy { it.actor }                      // Map<String, List<Movie>>
bondMovies.forEach { println(it.title) }

val routes = mapOf("home" to "Home", "search" to "Search")   // Map<String, String>, `to` creates a Pair
  • No .stream()/.collect(): the functions live directly on the collections and are evaluated eagerly (use asSequence() for lazy pipelines on large data)
  • List is read-only, MutableList adds add/remove. Expose read-only types from your APIs
  • Compose observes mutableStateListOf() instead of mutableListOf() (lecture Android Intro, Managing State)

17 Scope Functions: let, run, with, apply, also

Function Object is Returns Use it for
let it lambda result null checks (x?.let { }), transforming a value
run this lambda result computing a result from an object
with this lambda result calling several functions on one object (not an extension)
apply this the object configuring an object (builder style)
also it the object side effects such as logging, keeping the chain
// apply: configure and return the object (app/build.gradle.kts reads the API key from local.properties)
val localProperties = Properties().apply {
    load(rootProject.file("local.properties").inputStream())
}

// also: do something on the side, return the object (MovieDatabase.getDatabase)
Room.databaseBuilder(context, MovieDatabase::class.java, DATABASE_NAME)
    .build()
    .also { instance = it }

// let: only for non-null values
response.error?.let { message -> Log.w("Search", message) }

// with: several calls on one object
with(movie) { println("$title ($year)") }

18 Property Delegation: by

// `by` forwards get and set of a property to a delegate object

// lazy: computed on first access, then cached (MovieTrackerApplication.kt)
override val movieRepository: MovieRepository by lazy {
    OfflineMovieRepository(MovieDatabase.getDatabase(context).movieDao())
}

// Compose: MutableState<Int> stores the value in .value ...
val counterState = remember { mutableStateOf(0) }
counterState.value++

// ... the delegate reads and writes .value for you, counter looks like a plain Int
var counter by remember { mutableStateOf(0) }       // needs import androidx.compose.runtime.getValue / setValue
counter++                                           // becomes counterState.value = counterState.value + 1

// Android: a ViewModel, created lazily and scoped to the Activity
private val viewModel: HomeViewModel by viewModels()
  • Java has no equivalent, you would write the getter and setter by hand
  • by lazy is thread-safe by default and the standard way to create expensive objects on demand
  • Other delegates: Delegates.observable, by viewModel(...), by navArgs(), delegating to a Map

19 suspend Functions: A First Look at Coroutines

// A suspend function can pause and resume later without blocking a thread.
// It may only be called from another suspend function or from a coroutine.
@Dao
interface MovieDao {
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun insert(movie: Movie)                    // Room runs the query on a background thread
}

interface MovieApi {
    @GET("/")
    suspend fun searchMovies(@Query("s") searchString: String): SearchResponse   // Retrofit does the HTTP call
}

// A coroutine is started with launch { } inside a scope. viewModelScope is cancelled with the ViewModel.
class SearchViewModel(private val movieRepository: MovieRepository) : ViewModel() {
    fun saveMovie(movie: Movie) {
        viewModelScope.launch {
            movieRepository.insert(movie)    // reads like sequential code, the UI thread is never blocked
        }
    }
}
  • Java: threads, executors, callbacks or CompletableFuture chains. Kotlin: sequential code, the compiler turns a suspend function into a state machine
  • Android forbids network and database access on the UI thread, so Room and Retrofit expose suspend APIs
  • Scopes, Dispatchers, Flow and StateFlow follow in the lecture "Coroutines & Flow" (assignment 4 and 5)

20 Interop with Java

// Kotlin calls Java as if it were Kotlin. Retrofit and Gson are Java libraries (MovieTrackerApplication.kt)
val retrofit = Retrofit.Builder()
    .baseUrl(MovieApi.BASE_URL)
    .addConverterFactory(GsonConverterFactory.create())
    .build()
val api = retrofit.create(MovieApi::class.java)      // Java Class<T> is written ::class.java

// Java getters and setters appear as properties
val clicks = intent.getIntExtra("clicks", 0)         // normal method call
intent.action = Intent.ACTION_VIEW                   // calls setAction(), reading calls getAction()

// Platform types: Java does not tell whether a value may be null
val name = intent.getStringExtra("name")             // type String! -> treat it as String?
val length = name?.length ?: 0

// Java calls Kotlin: top-level functions become static methods of a class <FileName>Kt.
// @JvmStatic, @JvmOverloads and @JvmField fine-tune the generated API.
  • The whole Android framework is a Java API (Activity, Intent, Context), you use it from Kotlin all the time
  • Android annotates its APIs with @Nullable/@NonNull, so most framework calls have proper Kotlin types
  • Java keywords that are not Kotlin keywords (and vice versa) are escaped with backticks: Movie.`in`
  • Mixed projects are fine: Java and Kotlin files in the same module compile together

21 Further Information

22 Summary

  • Kotlin is a concise, statically typed JVM language that is fully interoperable with Java and the default for Android
  • val/var with type inference, string templates, if/when as expressions and ranges replace most Java boilerplate
  • Nullability is part of the type system: String?, ?., ?: and let turn NPEs into compile errors
  • data class generates equals/hashCode/toString/copy; object/companion object replace static; sealed types model UI states
  • Default and named arguments, extension functions and function types (Movie) -> Unit are used in every composable
  • The trailing lambda syntax is the reason Compose code looks like a layout language: Column { Text("...") }
  • Collections offer filter, map, first, forEach directly; scope functions apply/also/let keep code short
  • by lazy and by remember { mutableStateOf() } delegate property access; suspend + viewModelScope.launch move work off the UI thread

23 Recap Questions

  • What is the difference between val and var, and between List and MutableList?
  • val poster: String? = movie.poster: how do you get the length of poster without risking a NullPointerException?
  • Which functions does the compiler generate for a data class, and why does that matter for a Movie list in Compose?
  • Kotlin has no static. How do you define the constant Routes.HOME and a factory method MovieDatabase.getDatabase()?
  • Rewrite Button(onClick = { counter++ }, content = { Text("Button") }) with the trailing lambda syntax. What is the type of the content parameter?
  • What does var counter by remember { mutableStateOf(0) } do when you write counter++?
  • Why can movieRepository.insert(movie) only be called inside viewModelScope.launch { }?

Questions?