Null Safety Tutorial in Kotlin: Best Practices

In this tutorial, you’ll look at Kotlin’s nullability best practices. You’ll learn about null safety in Kotlin and how to avoid NPEs. By Kolin Stürt.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Making Use of Let

What if you are using a nullable variable for more than one statement?

You can use the safe call operator together with let. That allows you to group multiple calls together that execute if the variable isn’t null.

So now that filePath is nullable, change the method to use let like below:

val filePath = arguments?.getString(ARGUMENTS_PATH)
filePath?.let {
  storedPath = it
  fileUpdateBroadcastReceiver = FileUpdateBroadcastReceiver(it) {
    refreshView()
  }
}

Here, the code inside the function block executes only if filePath exists. Inside the block, the it parameter references a non-nullfilePath.

Running with Run

There are times when you want to run operations on the class by accessing thisrun instead of let.

In FileStack.kt file, the push, pop and eraseTo methods all end with an if-not-null check. In the push method, replace the null check and everything inside it with this code:

onStackChangeListener?.run {
  invoke(files)
  println("pushing, calling " + toString())
}

Do the same for the pop method:

onStackChangeListener?.run {
  invoke(files)
  println("popping, calling " + toString())
}

And for the eraseTo method:

onStackChangeListener?.run {
  invoke(files)
  println("erasing, calling " + toString())
}

These methods call invoke and toString on this, as if the blocks are extension functions of onStackChangeListener.

Converting to Safe Casts

You’ve seen how powerful ? can be, but it doesn’t stop there. You can also use ? for safe casts.

Regular casts will throw ClassCastException if the object is not of the right type, which means you’d need to scatter exception handling around your code. Instead, you can return null for that case.

In the FileBrowserFragment.kt file, replace the try/catch block in the onAttach method with this:

onItemClickListener = context as? OnItemClickListener

Here, you only assign onItemClickListener to context as long as the cast to OnItemClickListener succeeds.

Analyzing Equality

Symbols

Kotlin allows the receiver type of extension functions to be nullable. As long as there are proper null checks inside the function, you can call the function on a null object without having to do any additional null checks.

Head over to line 46 of FileUpdateBroadcastReceiver.kt file. Remove the null check by replacing the line with the following:

if (filePath.equals(path)) {

Press Control on PC/Linux or Command on Mac and hover over that equals function:

Equals code definition

Notice that the receiver, String? is nullable. That’s why you don’t need the extra null check. :]

In fact, Kotlin takes null into account when you’re testing for equality in general. The == operator will call the equals function as long as the item on the left isn’t null. If it is, it does a referential check of the right item for null. That means you can leave out null checks when doing comparisons using ==.

In the wipeFile method inside FileHelpers.kt file, find and replace the two occurrences of if (item.equals(SHRED_MODE_RANDOM)) { with this code:

if (item == SHRED_MODE_RANDOM) {

That was a lot of code cleanup! Choose BuildMake Project. You should see Android Studio compile correctly with all of these changes:

Build success

Now that your code is up to Kotlin’s standard, you’ll fix that crash you encountered when you first explored the project:

Crash log

Using the Elvis Operator

You’ll often see null checks written in a single line when setting or returning a variable:

if (list != null) return list.size else return 0

Kotlin streamlines this with the Elvis operator, ?:.

Line 45 of FileHelpers.kt file, listFiles() returns null when file is not a directory. This is the culprit of the crash.

Add the Elvis operator by replacing that line with the following:

it.listFiles()?.size ?: 0)

First, you added the safe call operator to listFiles(). Then you used the Elvis operator on size. ?: uses the value on the left, size, as long as it’s non-null. Otherwise it uses the right, 0.

This is a good example of failsafe programming, where you return a default or safe value that causes minimal harm in the event that something goes wrong. You can take that idea a step further.

Designing by Contract

Happy document

Design by contract is a method to improve software correctness, where you assert the inputs and outputs of each method. If you’ve defined a method to return a list of strings, the user of the method should not expect it to return anything else, including null.

For example, the fileList method definition states it will return a List, but as you now know, listFiles may return null.

If you change this method to return an optional, then you’ll have to go and add safety checks to all the code that uses this method. However, if you’ve already published your app or other developers are already using your class, you’ll want to make the least amount of code changes possible.

To avoid this, replace the body of the fileList method defined in FileHelpers.kt file with this:

val file = File(path)
val list = file.listFiles()
    ?.filter { showHiddenFiles || !it.name.startsWith(".") }
    ?.filter { !onlyFolders || it.isDirectory }
    ?.toList()
return list ?: listOf()

Here, you added safe call operators. If list is null, instead of returning null, you return an empty List, which still adheres to the contract.

Build and run the app. This time, you should be able to traverse all the directories without the app crashing! :]

FileVoid App

While this returns a value known to be harmless, depending on your design requirements you’ll want to consider if your app should be robust or correct.

If your app shows the temperature outside and, during one of the iterations, the value is null, you’d simply skip that iteration and show the previous or next valid data. On the other hand, if your app controls factory equipment, you’d want to immediately abort whenever your app finds an incorrect value!

It’s sometimes acceptable to return null on an error, but using null to represent a state is generally problematic. Variables shouldn’t have hidden or double meanings. An example is an Int? that stores the number of logged-in users, unless it’s null, which then means the app is in maintenance mode.

Here are a few other best practices to keep in mind:

  • Avoid unclear optionals. Write clear and consistent class interfaces as opposed to ones that require a magic combination of parameters.
  • Don’t make assumptions about how other developers will use the class. If you have to pass null into the class constructor to initialize some internal state, it’s a good indicator that the class is too specific and aware of it’s current use.
  • Don’t depend on knowledge of private implementation. For example, not calling a.initialize() because you know a.execute() will lazy-initialize if it needs to. Maybe it won’t in the future, and then you’ll get an NPE.
  • Isolate nullable operations into a single method or class. That way, you don’t have to strew ? in many places throughout your code.