Kotlin for Java developers: as much as you need for this course
fun main, val/var, string templatesif and when as expressions, ranges and fordata class, object, enum, sealedbysuspend functions: a first look at coroutinesAll code examples are taken from or modelled on the MovieTracker app of the assignments.
| 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 |
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}
}
val. Use var only when the value really changesInt, Long, Double, Boolean, Char and String are classes, the compiler uses JVM primitives where possibleval x: Long = 1 fails, write 1L or .toLong()Any is the root type (like Object), Unit replaces voidif 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)
}
if/when are expressions, the last line of each branch is its valuewhen with a sealed type or an enum is checked for completeness by the compiler (see below)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 JavaforEach, map and filter (see Collections)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)
}
@Nullable annotation, in Kotlin it is part of the typeNullPointerException becomes a compile error. Use !! only when you can prove non-null// 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 parameterpublic (default), private, protected, internal (module); no package-privatedata 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()
equals(), hashCode(), toString(), copy() and componentN() from the primary constructorrecords are similar, but have no copy() and are always immutableMovieTracker uses data classes for Movie, HomeUiState, SearchUiState and SearchResponseval properties: an immutable Movie can be shared safely between threads and Composeobject 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
static. Use top-level functions, object or companion objectconst val is a compile-time constant (like static final), only for primitives and StringResultActivity.EXTRA_CLICKS in assignment 1)enum class, sealed class and sealed interfaceenum 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+MovieTracker keeps it simpler and uses one flat data class SearchUiState(movieList, isLoading, errorMessage);
both styles are common// 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
modifier: Modifier = Modifier is the first optional one// 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)
}
}
private membersStringUtils.toYear(s) helper classesModifier.padding(), Modifier.clickable {},
Flow.collectAsStateWithLifecycle(), Context.startActivity()// 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")
Runnable, Consumer<T>, BiFunction<A, B, R>);
Kotlin has one syntax: (A, B) -> R(Movie) -> Unit is the type of every click handler in MovieTracker// 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 wayval 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
.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 APIsmutableStateListOf() instead of mutableListOf() (lecture Android Intro, Managing State)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)") }
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()
by lazy is thread-safe by default and the standard way to create expensive objects on demandDelegates.observable, by viewModel(...), by navArgs(), delegating to a Mapsuspend 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
}
}
}
CompletableFuture chains. Kotlin: sequential code, the compiler turns a
suspend function into a state machinesuspend APIsDispatchers, Flow and StateFlow follow in the lecture "Coroutines & Flow" (assignment 4 and 5)// 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.
Activity, Intent, Context), you use it from Kotlin all the time@Nullable/@NonNull, so most framework calls have proper Kotlin typesMovie.`in`val/var with type inference, string templates, if/when as expressions and ranges replace most Java boilerplateString?, ?., ?: and let turn NPEs into compile errorsdata class generates equals/hashCode/toString/copy; object/companion object replace static;
sealed types model UI states(Movie) -> Unit are used in every composableColumn { Text("...") }filter, map, first, forEach directly; scope functions apply/also/let keep code shortby lazy and by remember { mutableStateOf() } delegate property access; suspend + viewModelScope.launch
move work off the UI threadval and var, and between List and MutableList?val poster: String? = movie.poster: how do you get the length of poster without risking a NullPointerException?data class, and why does that matter for a Movie list in Compose?static. How do you define the constant Routes.HOME and a factory method MovieDatabase.getDatabase()?Button(onClick = { counter++ }, content = { Text("Button") }) with the trailing lambda syntax.
What is the type of the content parameter?var counter by remember { mutableStateOf(0) } do when you write counter++?movieRepository.insert(movie) only be called inside viewModelScope.launch { }?