Converting to Kotlin

1. Welcome!

In this codelab, you'll learn how to convert your code from Java to Kotlin. You'll also learn what the Kotlin language conventions are and how to ensure that the code you're writing follows them.

This codelab is suited to any developer that uses Java who is considering migrating their project to Kotlin. We'll start with a couple of Java classes that you'll convert to Kotlin using the IDE. Then we'll take a look at the converted code and see how we can improve it by making it more idiomatic and avoid common pitfalls.

What you'll learn

You will learn how to convert Java to Kotlin. In doing so you will learn the following Kotlin language features and concepts:

  • Handling nullability
  • Implementing singletons
  • Data classes
  • Handling strings
  • Elvis operator
  • Destructuring
  • Properties and backing properties
  • Default arguments and named parameters
  • Working with collections
  • Extension functions
  • Top-level functions and parameters
  • let, apply, with, and run keywords

Assumptions

You should already be familiar with Java.

What you'll need

2. Getting set up

Create a new project

If you're using IntelliJ IDEA, create a new Java project with Kotlin/JVM.

If you're using Android Studio, create a new project with the No Activity template. Choose Kotlin as the project language. Minimum SDK can be of any value, it will not affect the outcome.

The code

We'll create a User model object and a Repository singleton class that works with User objects and exposes lists of users and formatted user names.

Create a new file called User.java under app/java/<yourpackagename> and paste in the following code:

public class User {

    @Nullable
    private String firstName;
    @Nullable
    private String lastName;

    public User(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

}

You'll notice your IDE is telling you @Nullable is not defined. So import androidx.annotation.Nullable if you use Android Studio, or org.jetbrains.annotations.Nullable if you're using IntelliJ.

Create a new file called Repository.java and paste in the following code:

import java.util.ArrayList;
import java.util.List;

public class Repository {

    private static Repository INSTANCE = null;

    private List<User> users = null;

    public static Repository getInstance() {
        if (INSTANCE == null) {
            synchronized (Repository.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Repository();
                }
            }
        }
        return INSTANCE;
    }

    // keeping the constructor private to enforce the usage of getInstance
    private Repository() {

        User user1 = new User("Jane", "");
        User user2 = new User("John", null);
        User user3 = new User("Anne", "Doe");

        users = new ArrayList();
        users.add(user1);
        users.add(user2);
        users.add(user3);
    }

    public List<User> getUsers() {
        return users;
    }

    public List<String> getFormattedUserNames() {
        List<String> userNames = new ArrayList<>(users.size());
        for (User user : users) {
            String name;

            if (user.getLastName() != null) {
                if (user.getFirstName() != null) {
                    name = user.getFirstName() + " " + user.getLastName();
                } else {
                    name = user.getLastName();
                }
            } else if (user.getFirstName() != null) {
                name = user.getFirstName();
            } else {
                name = "Unknown";
            }
            userNames.add(name);
        }
        return userNames;
    }
}

3. Declaring nullability, val, var and data classes

Our IDE can do a pretty good job of automatically converting Java code into Kotlin code but sometimes it needs a little help. Let's let our IDE do an initial pass at the conversion. Then we'll go through the resulting code to understand how and why it has been converted this way.

Go to the User.java file and convert it to Kotlin: Menu bar -> Code -> Convert Java File to Kotlin File.

If your IDE prompts for correction after conversion, press Yes.

e6f96eace5dabe5f.png

You should see the following Kotlin code:

class User(var firstName: String?, var lastName: String?)

Note that User.java was renamed to User.kt. Kotlin files have the extension .kt.

In our Java User class we had two properties: firstName and lastName. Each had a getter and setter method, making its value mutable. Kotlin's keyword for mutable variables is var, so the converter uses var for each of these properties. If our Java properties had only getters, they would be read-only and would have been declared as val variables. val is similar to the final keyword in Java.

One of the key differences between Kotlin and Java is that Kotlin explicitly specifies whether a variable can accept a null value. It does this by appending a ? to the type declaration.

Because we marked firstName and lastName as nullable, the auto-converter automatically marked the properties as nullable with String?. If you annotate your Java members as non-null (using org.jetbrains.annotations.NotNull or androidx.annotation.NonNull), the converter will recognize this and make the fields non-null in Kotlin as well.

The basic conversion is already done. But we can write this in a more idiomatic way. Let's see how.

Data class

Our User class only holds data. Kotlin has a keyword for classes with this role: data. By marking this class as a data class, the compiler will automatically create getters and setters for us. It will also derive the equals(), hashCode(), and toString() functions.

Let's add the data keyword to our User class:

data class User(var firstName: String?, var lastName: String?)

Kotlin, like Java, can have a primary constructor and one or more secondary constructors. The one in the example above is the primary constructor of the User class. If you're converting a Java class that has multiple constructors, the converter will automatically create multiple constructors in Kotlin as well. They are defined using the constructor keyword.

If we want to create an instance of this class, we can do it like this:

val user1 = User("Jane", "Doe")

Equality

Kotlin has two types of equality:

  • Structural equality uses the == operator and calls equals() to determine if two instances are equal.
  • Referential equality uses the === operator and checks if two references point to the same object.

The properties defined in the primary constructor of the data class will be used for structural equality checks.

val user1 = User("Jane", "Doe")
val user2 = User("Jane", "Doe")
val structurallyEqual = user1 == user2 // true
val referentiallyEqual = user1 === user2 // false

4. Default arguments, named arguments

In Kotlin, we can assign default values to arguments in function calls. The default value is used when the argument is omitted. In Kotlin, constructors are also functions, so we can use default arguments to specify that the default value of lastName is null. To do this, we just assign null to lastName.

data class User(var firstName: String?, var lastName: String? = null)

// usage
val jane = User("Jane") // same as User("Jane", null)
val joe = User("Joe", "Doe")

Kotlin allows you to label your arguments when your functions are called:

val john = User(firstName = "John", lastName = "Doe") 

As a different use case, let's say that the firstName has null as its default value and lastName does not. In this case, because the default parameter would precede a parameter with no default value, you must call the function with named arguments:

data class User(var firstName: String? = null, var lastName: String?)

// usage
val jane = User(lastName = "Doe") // same as User(null, "Doe")
val john = User("John", "Doe")

Default values are an important and often used concept in Kotlin code. In our codelab we want to always specify the first and last name in a User object declaration, so we don't need default values.

5. Object initialization, companion object and singletons

Before continuing the codelab, make sure that your User class is a data class. Now, let's convert the Repository class to Kotlin. The automatic conversion result should look like this:

import java.util.*

class Repository private constructor() {
    private var users: MutableList<User?>? = null
    fun getUsers(): List<User?>? {
        return users
    }

    val formattedUserNames: List<String?>
        get() {
            val userNames: MutableList<String?> =
                ArrayList(users!!.size)
            for (user in users) {
                var name: String
                name = if (user!!.lastName != null) {
                    if (user!!.firstName != null) {
                        user!!.firstName + " " + user!!.lastName
                    } else {
                        user!!.lastName
                    }
                } else if (user!!.firstName != null) {
                    user!!.firstName
                } else {
                    "Unknown"
                }
                userNames.add(name)
            }
            return userNames
        }

    companion object {
        private var INSTANCE: Repository? = null
        val instance: Repository?
            get() {
                if (INSTANCE == null) {
                    synchronized(Repository::class.java) {
                        if (INSTANCE == null) {
                            INSTANCE =
                                Repository()
                        }
                    }
                }
                return INSTANCE
            }
    }

    // keeping the constructor private to enforce the usage of getInstance
    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")
        users = ArrayList<Any?>()
        users.add(user1)
        users.add(user2)
        users.add(user3)
    }
}

Let's see what the automatic converter did:

  • The list of users is nullable since the object wasn't instantiated at declaration time
  • Functions in Kotlin like getUsers() are declared with the fun modifier
  • The getFormattedUserNames() method is now a property called formattedUserNames
  • The iteration over the list of users (that was initially part of getFormattedUserNames() ) has a different syntax than the Java one
  • The static field is now part of a companion object block
  • An init block was added

Before we go further, let's clean up the code a bit. If we look in the constructor, we notice the converter made our users list a mutable list that holds nullable objects. While the list can indeed be null, let's assume it can't hold null users. So let's do the following:

  • Remove the ? in User? within the users type declaration
  • Remove the ? in User? for the return type of getUsers() so it returns List<User>?

Init block

In Kotlin, the primary constructor cannot contain any code, so initialization code is placed in init blocks. The functionality is the same.

class Repository private constructor() {
    ...
    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")
        users = ArrayList<Any?>()
        users.add(user1)
        users.add(user2)
        users.add(user3)
    }
}

Much of the init code handles initializing properties. This can also be done in the declaration of the property. For example, in the Kotlin version of our Repository class, we see that the users property was initialized in the declaration.

private var users: MutableList<User>? = null

Kotlin's static properties and methods

In Java, we use the static keyword for fields or functions to say that they belong to a class but not to an instance of the class. This is why we created the INSTANCE static field in our Repository class. The Kotlin equivalent for this is the companion object block. Here you would also declare the static fields and static functions. The converter created the companion object block and moved the INSTANCE field here.

Handling singletons

Because we need only one instance of the Repository class, we used the singleton pattern in Java. With Kotlin, you can enforce this pattern at the compiler level by replacing the class keyword with object.

Remove the private constructor and replace the class definition with object Repository. Remove the companion object as well.

object Repository {

    private var users: MutableList<User>? = null
    fun getUsers(): List<User>? {
       return users
    }

    val formattedUserNames: List<String>
        get() {
            val userNames: MutableList<String> =
                ArrayList(users!!.size)
        for (user in users) {
            var name: String
            name = if (user!!.lastName != null) {
                if (user!!.firstName != null) {
                    user!!.firstName + " " + user!!.lastName
                } else {
                    user!!.lastName
                }
            } else if (user!!.firstName != null) {
                user!!.firstName
            } else {
                "Unknown"
            }
            userNames.add(name)
       }
       return userNames
   }

    // keeping the constructor private to enforce the usage of getInstance
    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")
        users = ArrayList<Any?>()
        users.add(user1)
        users.add(user2)
        users.add(user3)
    }
}

When using the object class, we just call functions and properties directly on the object, like this:

val formattedUserNames = Repository.formattedUserNames

Note that if a property does not have a visibility modifier on it, it is public by default, as in the case of formattedUserNames property in the Repository object.

6. Handling nullability

When converting the Repository class to Kotlin, the automatic converter made the list of users nullable, because it wasn't initialized to an object when it was declared. As a result, for all the usages of the users object, the not-null assertion operator !! needs to be used. (You'll see users!! and user!! throughout the converted code.) The !! operator converts any variable to a non-null type, so you can access properties or call functions on it. However, an exception will be thrown if the variable value is indeed null. By using !!, you're risking exceptions being thrown at runtime.

Instead, prefer handling nullability by using one of these methods:

  • Doing a null check ( if (users != null) {...} )
  • Using the elvis operator ?: (covered later in the codelab)
  • Using some of the Kotlin standard functions (covered later in the codelab)

In our case, we know that the list of users doesn't need to be nullable, since it's initialized right after the object is constructed (in the init block). Thus we can directly instantiate the users object when we declare it.

When creating instances of collection types, Kotlin provides several helper functions to make your code more readable and flexible. Here we're using a MutableList for users:

private var users: MutableList<User>? = null

For simplicity, we can use the mutableListOf() function and provide the list element type. mutableListOf<User>() creates an empty list that can hold User objects. Since the data type of the variable can now be inferred by the compiler, remove the explicit type declaration of the users property.

private val users = mutableListOf<User>()

We also changed var into val because users will contain a read-only reference to the list of users. Note that the reference is read-only, so it can never point to a new list, but the list itself is still mutable (you can add or remove elements).

Since the users variable is already initialized, remove this initialization from the init block:

users = ArrayList<Any?>()

Then the init block should look like this:

init {
    val user1 = User("Jane", "")
    val user2 = User("John", null)
    val user3 = User("Anne", "Doe")

    users.add(user1)
    users.add(user2)
    users.add(user3)
}

With these changes, our users property is now non-null, and we can remove all the unnecessary !! operator occurrences. Note that you will still see compile errors in Android Studio, but continue with the next few steps of the codelabs to resolve them.

val userNames: MutableList<String?> = ArrayList(users.size)
for (user in users) {
    var name: String
    name = if (user.lastName != null) {
        if (user.firstName != null) {
            user.firstName + " " + user.lastName
        } else {
            user.lastName
        }
    } else if (user.firstName != null) {
        user.firstName
    } else {
        "Unknown"
    }
    userNames.add(name)
}

Also, for the userNames value, if you specify the type of ArrayList as holding Strings, then you can remove the explicit type in the declaration because it will be inferred.

val userNames = ArrayList<String>(users.size)

Destructuring

Kotlin allows destructuring an object into a number of variables, using a syntax called destructuring declaration. We create multiple variables and can use them independently.

For example, data classes support destructuring so we can destructure the User object in the for loop into (firstName, lastName). This allows us to work directly with the firstName and lastName values. Update the for loop as shown below. Replace all instances of user.firstName with firstName and replace user.lastName with lastName.

for ((firstName, lastName) in users) {
    var name: String
    name = if (lastName != null) {
        if (firstName != null) {
            firstName + " " + lastName
        } else {
            lastName
        }
    } else if (firstName != null) {
        firstName
    } else {
        "Unknown"
    }
    userNames.add(name)
}

if expression

The names in the list of userNames are not quite in the format that we want yet. Since both lastName and firstName can be null, we need to handle nullability when we build the list of formatted user names. We want to display "Unknown" if either name is missing. Since the name variable will not be changed after it's set once, we can use val instead of var. Make this change first.

val name: String

Take a look at the code that sets the name variable. It may look new to you to see a variable being set to equal an if / else block of code. This is allowed because in Kotlin if and when are expressions—they return a value. The last line of the if statement will be assigned to name. This block's only purpose is to initialize the name value.

Essentially, this logic presented here is if the lastName is null, name is either set to the firstName or "Unknown".

name = if (lastName != null) {
    if (firstName != null) {
        firstName + " " + lastName
    } else {
        lastName
    }
} else if (firstName != null) {
    firstName
} else {
    "Unknown"
}

Elvis operator

This code can be written more idiomatically by using the elvis operator ?:. The elvis operator will return the expression on its left hand side if it's not null, or the expression on its right hand side, if the left hand side is null.

So in the following code, firstName is returned if it is not null. If firstName is null, the expression returns the value on the right hand , "Unknown":

name = if (lastName != null) {
    ...
} else {
    firstName ?: "Unknown"
}

7. String templates

Kotlin makes working with Strings easy with String templates. String templates allow you to reference variables inside string declarations by using the $ symbol before the variable. You could also put an expression within a string declaration, by placing the expression within { } and using the $ symbol before it. Example: ${user.firstName}.

Your code currently uses string concatenation to combine the firstName and lastName into the user name.

if (firstName != null) {
    firstName + " " + lastName
}

Instead, replace the String concatenation with:

if (firstName != null) {
    "$firstName $lastName"
}

Using string templates can simplify your code.

Your IDE will show you warnings if there is a more idiomatic way to write your code. You'll notice a squiggly underline in the code, and when you hover over it, you'll see a suggestion for how to refactor your code.

Currently, you should see a warning that the name declaration can be joined with the assignment. Let's apply this. Because the type of the name variable can be deduced, we can remove the explicit String type declaration. Now our formattedUserNames looks like this:

val formattedUserNames: List<String?>
    get() {
        val userNames = ArrayList<String>(users.size)
        for ((firstName, lastName) in users) {
            val name = if (lastName != null) {
                if (firstName != null) {
                    "$firstName $lastName"
                } else {
                    lastName
                }
            } else {
                firstName ?: "Unknown"
            }
            userNames.add(name)
        }
        return userNames
    }

We can make one additional tweak. Our UI logic displays "Unknown" in case the first and last names are missing, so we're not supporting null objects. Thus, for the data type of formattedUserNames replace List<String?> with List<String>.

val formattedUserNames: List<String>

8. Operations on collections

Let's take a closer look at the formattedUserNames getter and see how we can make it more idiomatic. Right now the code does the following:

  • Creates a new list of strings
  • Iterates through the list of users
  • Constructs the formatted name for each user, based on the user's first and last name
  • Returns the newly created list
    val formattedUserNames: List<String>
        get() {
            val userNames = ArrayList<String>(users.size)
            for ((firstName, lastName) in users) {
                val name = if (lastName != null) {
                    if (firstName != null) {
                        "$firstName $lastName"
                    } else {
                        lastName
                    }
                } else {
                    firstName ?: "Unknown"
                }
                userNames.add(name)
            }
            return userNames
        }

Kotlin provides an extensive list of collection transformations that make development faster and safer by expanding the capabilities of the Java Collections API. One of them is the map function. This function returns a new list containing the results of applying the given transform function to each element in the original list. So, instead of creating a new list and iterating through the list of users manually, we can use the map function and move the logic we had in the for loop inside the map body. By default, the name of the current list item used in map is it, but for readability you can replace it with your own variable name. In our case, let's name it user:

val formattedUserNames: List<String>
        get() {
            return users.map { user ->
                val name = if (user.lastName != null) {
                    if (user.firstName != null) {
                        "${user.firstName} ${user.lastName}"
                    } else {
                        user.lastName ?: "Unknown"
                    }
                }  else {
                    user.firstName ?: "Unknown"
                }
                name
            }
        }

Notice that we use the Elvis operator to return "Unknown" if user.lastName is null, since user.lastName is of type String? and a String is required for the name.

...
else {
    user.lastName ?: "Unknown"
}
...

To simplify this even more, we can remove the name variable completely:

val formattedUserNames: List<String>
        get() {
            return users.map { user ->
                if (user.lastName != null) {
                    if (user.firstName != null) {
                        "${user.firstName} ${user.lastName}"
                    } else {
                        user.lastName ?: "Unknown"
                    }
                }  else {
                    user.firstName ?: "Unknown"
                }
            }
        }

9. Properties and backing properties

We saw that the automatic converter replaced the getFormattedUserNames() function with a property called formattedUserNames that has a custom getter. Under the hood, Kotlin still generates a getFormattedUserNames() method that returns a List.

In Java, we would expose our class properties via getter and setter functions. Kotlin allows us to have a better differentiation between properties of a class, expressed with fields, and functionalities, actions that a class can do, expressed with functions. In our case, the Repository class is very simple and doesn't do any actions so it only has fields.

The logic that was triggered in the Java getFormattedUserNames() function is now triggered when calling the getter of the formattedUserNames Kotlin property.

While we don't explicitly have a field corresponding to the formattedUserNames property, Kotlin does provide us an automatic backing field named field which we can access if needed from custom getters and setters.

Sometimes, however, we want some extra functionality that the automatic backing field doesn't provide.

Let's go through an example.

Inside our Repository class, we have a mutable list of users which is being exposed in the function getUsers() which was generated from our Java code:

fun getUsers(): List<User>? {
    return users
}

Because we didn't want the callers of the Repository class to modify the users list, we created the getUsers() function that returns a read-only List<User>. With Kotlin, we prefer using properties rather than functions for such cases. More precisely, we would expose a read-only List<User> that is backed by a mutableListOf<User>.

First, let's rename users to _users. Highlight the variable name, right click to Refactor > Rename the variable. Then add a public read-only property that returns a list of users. Let's call it users:

private val _users = mutableListOf<User>()
val users: List<User>
    get() = _users

At this point, you can delete the getUsers() method.

With the above change, the private _users property becomes the backing property for the public users property. Outside of the Repository class, the _users list is not modifiable, as consumers of the class can access the list only through users.

When users is called from Kotlin code, the List implementation from the Kotlin Standard Library is used, where the list is not modifiable. If users is called from Java, the java.util.List implementation is used, where the list is modifiable and operations like add() and remove() are available.

Full code:

object Repository {

    private val _users = mutableListOf<User>()
    val users: List<User>
        get() = _users

    val formattedUserNames: List<String>
        get() {
            return _users.map { user ->
                if (user.lastName != null) {
                    if (user.firstName != null) {
                        "${user.firstName} ${user.lastName}"
                    } else {
                        user.lastName ?: "Unknown"
                    }
                }  else {
                    user.firstName ?: "Unknown"
                }
            }
        }

    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")

        _users.add(user1)
        _users.add(user2)
        _users.add(user3)
    }
}

10. Top-level and extension functions and properties

Right now the Repository class knows how to compute the formatted user name for a User object. But if we want to reuse the same formatting logic in other classes, we need to either copy and paste it or move it to the User class.

Kotlin provides the ability to declare functions and properties outside of any class, object, or interface. For example, the mutableListOf() function we used to create a new instance of a List is already defined in Collections.kt from the Kotlin Standard Library.

In Java, whenever you need some utility functionality, you would most likely create a Util class and declare that functionality as a static function. In Kotlin you can declare top-level functions, without having a class. However, Kotlin also provides the ability to create extension functions. These are functions that extend a certain type but are declared outside of the type.

The visibility of extension functions and properties can be restricted by using visibility modifiers. These restrict the usage only to classes that need the extensions, and don't pollute the namespace.

For the User class, we can either add an extension function that computes the formatted name, or we can hold the formatted name in an extension property. It can be added outside the Repository class, in the same file:

// extension function
fun User.getFormattedName(): String {
    return if (lastName != null) {
        if (firstName != null) {
            "$firstName $lastName"
        } else {
            lastName ?: "Unknown"
        }
    } else {
        firstName ?: "Unknown"
    }
}

// extension property
val User.userFormattedName: String
    get() {
        return if (lastName != null) {
            if (firstName != null) {
                "$firstName $lastName"
            } else {
                lastName ?: "Unknown"
            }
        } else {
            firstName ?: "Unknown"
        }
    }

// usage:
val user = User(...)
val name = user.getFormattedName()
val formattedName = user.userFormattedName

We can then use the extension functions and properties as if they're part of the User class.

Because the formatted name is a property of the User class and not a functionality of the Repository class, let's use the extension property. Our Repository file now looks like this:

val User.formattedName: String
    get() {
        return if (lastName != null) {
            if (firstName != null) {
                "$firstName $lastName"
            } else {
                lastName ?: "Unknown"
            }
        } else {
            firstName ?: "Unknown"
        }
    }

object Repository {

    private val _users = mutableListOf<User>()
    val users: List<User>
      get() = _users

    val formattedUserNames: List<String>
        get() {
            return _users.map { user -> user.formattedName }
        }

    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")

        _users.add(user1)
        _users.add(user2)
        _users.add(user3)
    }
}

The Kotlin Standard Library uses extension functions to extend the functionality of several Java APIs; a lot of the functionalities on Iterable and Collection are implemented as extension functions. For example, the map function we used in a previous step is an extension function on Iterable.

11. Scope functions: let, apply, with, run, also

In our Repository class code, we are adding several User objects to the _users list. These calls can be made more idiomatic with the help of Kotlin scope functions.

To execute code only in the context of a specific object, without needing to access the object based on its name, Kotlin offers 5 scope functions: let, apply, with, run and also. These functions make your code easier to read and more concise. All scope functions have a receiver (this), may have an argument (it) and may return a value.

Here's a handy cheat sheet to help you remember when to use each function:

6b9283d411fb6e7b.png

Since we're configuring our _users object in our Repository, we can make the code more idiomatic by using the apply function:

init {
    val user1 = User("Jane", "")
    val user2 = User("John", null)
    val user3 = User("Anne", "Doe")
   
    _users.apply {
       // this == _users
       add(user1)
       add(user2)
       add(user3)
    }
 }

12. Wrap up

In this codelab, we covered the basics you need to start converting your code from Java to Kotlin. This conversion is independent of your development platform and helps to ensure that the code you write is idiomatic Kotlin.

Idiomatic Kotlin makes writing code short and sweet. With all the features Kotlin provides, there are so many ways to make your code safer, more concise, and more readable. For example, we can even optimize our Repository class by instantiating the _users list with users directly in the declaration, getting rid of the init block:

private val users = mutableListOf(User("Jane", ""), User("John", null), User("Anne", "Doe"))

We covered a large array of topics, from handling nullability, singletons, Strings, and collections to topics like extension functions, top-level functions, properties, and scope functions. We went from two Java classes to two Kotlin ones that now look like this:

User.kt

data class User(var firstName: String?, var lastName: String?)

Repository.kt

val User.formattedName: String
    get() {
       return if (lastName != null) {
            if (firstName != null) {
                "$firstName $lastName"
            } else {
                lastName ?: "Unknown"
            }
        } else {
            firstName ?: "Unknown"
        }
    }

object Repository {

    private val _users = mutableListOf(User("Jane", ""), User("John", null), User("Anne", "Doe"))
    val users: List<User>
        get() = _users

    val formattedUserNames: List<String>
        get() = _users.map { user -> user.formattedName }
}

Here's a TL;DR of the Java functionalities and their mapping to Kotlin:

Java

Kotlin

final object

val object

equals()

==

==

===

Class that just holds data

data class

Initialization in the constructor

Initialization in the init block

static fields and functions

fields and functions declared in a companion object

Singleton class

object

To find out more about Kotlin and how to use it on your platform, check out these resources: