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.

Version

  • Kotlin 1.3, Android 4.4, Android Studio 3

Does nothing exist? Or does it exist only in reference to something tangible? How can you divide a number of things among no things?

Nothing is a concept tied to language; in Kotlin, the closest relative is null. In order to write solid code, it’s important to understand the concept of null.

In this tutorial, you’ll look at Kotlin’s nullability feature and it’s best practices. During the process, you’ll learn about the following:

  • Nullable variables.
  • Null pointer exceptions.
  • How to make safe calls on objects.

Note: This tutorial assumes that you’re already familiar with the basics of Android development and Android Studio. If Android development is new to you, first read through the Beginning Android Development and Kotlin for Android tutorials.

Getting Started

Download and unzip the materials for this tutorial using the Download Materials button at the top or bottom of this page. Open and run the starter project in Android Studio 3.3.0 or higher to see what you’ll be working with. When you open the app for the first time you will be prompted for File access permission, simply choose Allow to grant file access for the app:

Allow file access permission

This app deletes files of your choice. Start using it by browsing through the various folders and tapping on a file. Once you select and tap the file, you’ll encounter this error:

Null Pointer Exception Crash

You’ll fix that error soon. But first, if you don’t have test files to delete, you can copy the sample text file included in the root folder of the materials, BlackHoles.txt, to your device. Yes, it’s a list of various black holes in the universe. :]

If you want to move test files to your physical device, set your device to file transfer mode:
File Transfer Mode

On Linux and Windows you can access the device as a regular drive. For Mac you’ll need Android File Transfer.

Finding a NullPointerException

This app crashes when you tap a file due to a null variable. If you’re a Java developer, you’ll already be familiar with the misery of NullPointerExceptions.

In Java, all variables except primitive variables actually store references to memory addresses. Because they are references, you can set the variables to null. When the system expects a reference to be valid but instead it’s null, it throws a NullPointerException, NPE for short.

If you haven’t implemented exception handling, the app questions the nature of reality, and then crashes.

Sad dinosaur

Kotlin aims at being a safer language. By default, variables are non-null references and cannot be set to null.

You can make variables nullable by adding ? to the end of the variable. Once you do this, the compiler tries to make sure you use the variable safely for the duration of its lifecycle by using compile-time checks.

While Kotlin attempts to eliminate NPEs, it doesn’t do away with them entirely. That’s why the best practice during development is to start with non-null variables at the narrowest possible scope. You should only change the variable to be nullable or move it to a broader scope if it’s absolutely necessary.

Note: NPEs can cause security vulnerabilities, especially when they happen in security-related code or processes. If attackers can trigger an NPE, they might be able to use the resulting exception to bypass security logic or cause the app to reveal debugging information that is valuable in planning future attacks. NPEs can also be considered security vulnerabilities if temporary work files aren’t cleaned up before the process terminates.

Performing Null Checks

Now that you understand NPEs, you can avoid problems by checking a variable for null reference before using it. It’s common to see multiple checks even when the variable is not expected to be null.

This is defensive programming — the process of making sure your app still functions under unexpected conditions. This is especially important in safety-critical apps.

Null checks are a good idea when you need to check for an uninitialized state or when you need to call UI setup only once.

Check out line 66 of MainActivity.kt. The isInitialized flag is attempting to track if you’ve started the activity for the first time or if you are recreating it, such as after an orientation change. It’s better to leave that logic up to the standard lifecycle behavior.

In the onCreate method definition, the savedInstanceState argument is of nullable type Bundle?. It is initialized to null by default the first time you create the activity.

Instead of trying to track this with an extra variable, you can check for null directly. Change line 66 to the following:

if (savedInstanceState == null) {

Search and remove the remaining two occurrences of isInitialized in the file:

Init flags

Using the Safe Call Operator

You’ve just seen a great example where checking for null makes sense. In many other cases, you end up with code that looks like this:

if (loggedInUser != null) {
 if (userToSend != null) {
   if (validator != null) {
     ...
     validator.canUserSendMessage(loggedInUser, userToSend)
     ...
   } //end if (loggedInUser != null)
 } //end if (userToSend != null)
} //end if (validator != null)

As you can see, it’s not immediately clear what the piece of code does without first weeding out the if-not-null checks.

Kotlin discourages the use of those checks, and instead introduces a safe call operator. Just as variable declarations use ? to mark variables as nullable, adding ? to a method will only allow the method to execute if the variable is non-null.

Check out the statements within the onResume and onPause methods in the FileBrowserFragment.kt file. Notice how you use the safe call operator on the context? object. You’ll apply the same operator to the arguments object in this app to prevent the crash you saw earlier.

In the onCreate method in the FileBrowserFragment.kt file, remove the first if-not-null check and replace the line right after it with this:

val filePath = arguments?.getString(ARGUMENTS_PATH)

This line calls getString only if arguments exists; otherwise, it returns null. filePath is now an inferred nullable val. See how the logic is clear all in a single line of code?

There’s no question that it is defined as a best practice to use the safe call operator in place of if-not-null checks!

Note: While the safe call operator is usually great for chaining calls together, it can sometimes hinder self-documenting code:
object?.first?.second?.third?.active().

If your code is starting to look like this, assign the complicated line to a variable for readability:
var isUserVerified = object?.first?.second?.third?.active()

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.

Thinking About Return Values

Early returns are useful to see the contract assertion up front. It’s clear when methods have one entry and one exit point. If you’ll be returning null, don’t scatter the return statements around in the middle of the code; these are hard to notice. If your code has a structure like this one:

if x
  if y
    return object
  else
    return null
...
return null

You can declare the nullable variable at the beginning of the method, update it along the way, and then return it at the end of the method instead, like this:

var object: Object? = null
if x
  if y
    object = ...
...
return object

Asserting Not-Null

Once you’ve spent time hardening your code as just discussed, there will be times you know for sure you’ve instantiated a value. You may have noticed that some of the code you’ve replaced uses !!. That’s Kotlin’s non-null assertion operator. It force-casts a nullable variable to a non-null one.

When using !!, if the variable is in fact null, you’ll get an NPE!

Inside the wipeFile method, you instantiate the random variable only when item == SHRED_MODE_RANDOM. Because there’s a check for this mode on line 83 you know for sure that you have instantiated random.

Remove that if check, and replace random.nextBytes(data) with this:

random!!.nextBytes(data)

Choose BuildMake Project. You’ll see that the code compiles, but in most cases, this is dangerous to use. Someone might change the flow of your code in the future, and they might not know to change all the places where you added !!.

As the complexity of a program increases, the edge cases that you originally thought won’t happen, starts to happen more often. There can be cases when a vulnerability isn’t obvious until the user enters something unexpected. In a way, the double exclamation mark is Kotlin yelling at you not to use it too often!! :]

Replace the line you just changed to the safer approach that you learned about earlier:

random?.nextBytes(data)

If you do use !!, here are some tips:

  • Cover all your code: Test each flow control case at least once. Perform stress and fuzz testing to see if there’s a state that causes it to be null that you’ve missed.
  • Don’t blindly trust data from external inputs. Sanitize and validate data from user input, serialized/unarchived object graphs and from network sources.
  • Declare and initialize the !! variable right before you use it, in order to reduce its scope.
  • Use each variable for exactly one purpose. That way, there’s less chance that other parts of the code will set that variable to something else, such as null!

These tips apply to development in general, but if your code has !!, assertion functions can help you out while you develop.

Testing With Assertions

Can you count how many NPEs in the universe haven’t happened yet? While some things remain undefined, Kotlin can at least make some assertions about reality. :]

The Kotlin.test framework has two valuable functions, assertNull and assertNotNull. If you’ve enabled JVM assertions, assert functions will throw an AssertionError when they evaluate to false. These functions are good for early bug catching while testing your code.

Besides asserts, there are a few other functions that you should know about:

  • require(Boolean) throws an IllegalArgumentException if you pass it an argument that evaluates to false. This is great to test the parameters of your methods.
  • requireNotNull returns the value if it’s not null; otherwise, it throws an IllegalArgumentException.
  • check(Boolean) throws an IllegalStateException error when it evaluates to false. This is great for testing app or object state.

Inside FileHelpers.kt file, add requireNotNull checks by replacing line 45 with the following:

it.listFiles()?.size ?: 0).also { theFileInfo ->
   requireNotNull(theFileInfo.path)
   requireNotNull(theFileInfo.fileType)
   requireNotNull(theFileInfo.name)
   requireNotNull(theFileInfo.extension)
   requireNotNull(theFileInfo.size)
}

With this code, you’re making sure that theFileInfo is valid.

Notice the use of also which is very similar to let, except that it returns itself. It’s a great way to chain together separate manipulations to the same object.

Just as with run verses let, also uses it inside the function block. If you want to use this to apply additional operations, you can use apply.

In MainActivity.kt file, replace the last statement on line 101 of the setupUI method with the code below:

fileStack.push(FileInfo(Environment.getExternalStorageDirectory().absolutePath,
    FileType.FOLDER,
    "/",
    0.0).apply { numberOfChildren = 0 })

You accessed numberOfChildren on this of the FileInfo instance.

You can do much more by using run, let, also and apply with the safe call operator.

You now have a whole lot of methods with slight differences at your disposal, so here’s a flowchart to help you decide which one to use:

Kotlin method choices

Delaying Initialization

You’ve now covered most scenarios when it comes to nullability! But here’s another case to consider: Given the way the Android Activity Lifecycle works, there are times you’ll need to declare a variable but not initialize it until later.

A great example is when you often initialize variables, such as an OnClickListener in the onCreate method.

It’s messy to declare the variable as nullable and then scatter safe call checks everywhere.

The solution is lateinit, which works on non-primitive objects to reduce the number of null checks you need to use. If you use it before you initialize it, you’ll get a kotlin.UninitializedPropertyAccessException.

It’s time to simplify the code in FileBrowserFragment.kt file. Change the recyclerAdapter definition at the top of the class to the following:

private lateinit var recyclerAdapter: RecyclerAdapter

Now that you declared recyclerAdapter as lateinit, find the instances of recyclerAdapter!! in the file and remove the unnecessary !! characters from the call:

Find and replace code

There are a few more alternatives to lateinit that you should know:

  • lateinit has a backing field with the same visibility as the property, so the backing field can still be set to null from Java. Delegates.notNull is a better choice in that case — var age: Int by Delegates.notNull().
  • If you use it before initializing it, Delegates.notNull throws an IllegalStateException.
  • lazy is a handy way to defer initializing a variable until you need it.
  • You define a block that’s not called until the first time you read the property.
  • The runtime saves the return value of that block for future access. The block will only run once.

Using Null in Collections

Kotlin also improves safety by disallowing nullable variables in collections by default, but there are times when you might want to allow them. Some examples are interoperability with legacy code or a circular buffer that is initially set to all nulls.

Navigate to the legacy folder in the project and open the FileShredder.java file. Inside the stringForMode method, you’ll notice that it will return null for modes that are unknown. It does that by design, but Kotlin doesn’t really know that.

To test how it works, create a test directory on your device and try to delete it:

Delete a file

Null cast exception

That happens because there isn’t a valid mode for folders, so item is null.

Let’s say you can’t change the legacy code. Instead, in FileHelpers.kt, replace the wipeFile method definition with the following:

private fun wipeFile(file: File, operations: List<String?>) {

You’ve just told Kotlin that the List can include nulls. At this point, you could start introducing safe calls around the code, but since there isn’t any logic associated with null states, you can simply filter them out.

Replace the line right before the for loop with this code:

val validOperations = operations.filterNotNull()
val iterator = validOperations.listIterator()

Here, you called filterNotNull() to remove nulls from the List. This also works well if you are using helper methods such as String::toDoubleOrNull().

Build and run the app. Go ahead and delete the test directory that you created. This time, you should succeed!

Deleted successfully

Happy face

Interoperating With Other Languages

You’ve now gotten through all the best practices for nullability in Kotlin.

However, although Kotlin is safer than Java, you won’t always work with a pure Kotlin app. There will often be legacy code that’s too expensive to change, and some teams simply prefer Java.

Nullability in Java

There are no null safety checks for types you declare in Java. As you’ve seen with the legacy code you just worked with, types coming from Java subvert the checking system!

There’s a good chance that you expect an object in Kotlin, but from Java it’s null.

Note: NPEs from Java are still security issues. When the app crashes, it gives valuable insight into the internal logic of the executing code block. This could potentially enable someone with enough interest to exploit the app. i.e You forgot to check user input, the app crashes on no input value(null) and generates a stacktrace. This stacktrace usually consists of the call stack, which in turn allows hackers to deduce how to feed a valid albeit fake value to the app and fool it.

The best practice is to treat all variables coming from Java as nullable in your Kotlin code. If you can change the Java code, a better solution is to update it to include nullability annotations.

Annotations don’t alter any existing logic, but instead tell the Kotlin compiler about nullability. The two important annotations are @Nullable and @NotNull.

Nullability in C++

For games and for code that is performance-sensitive or portable, it’s common to use C++ as the preferred language.

C++ is powerful because it allows you to work with memory pointers. Here are a few points about pointers:

  • As with references, you can set a pointer to null.
  • As soon as you bring native code into the mix, you introduce a whole set of memory, safety and security issues surrounding null dereferences. Pointers allow you to access raw memory locations, making it easier to read and write to any memor area. This counts as one of the major ways for attackers to maliciously change a program.
  • C++ doesn’t offer nullability annotations like Java does. Instead, document your functions well by stating whether the parameters and return values can be null or not.
  • In normal cases, you’ll set a pointer to null when you are finished with it, and not store or return pointers for later use. That allows you to work with the pointer only while it’s valid.
  • The true native meaning of null is actually a zero. Zero was late to the party in computational systems, arriving only after 5,000BC. It was null before that. :]
  • You can find more info about how Kotlin works with pointers by checking out the Pointer types section of the Kotlin reference, as well as the CPointer and COpaquePointer objects.

Where to Go From Here

You’ve just completed a crash course in null safety!

Download the final project using the Download Materials button at the top or bottom of this tutorial. Armed with your new knowledge, you can safely embark onto the journey into the void.

Finding a crash is like holding a mirror up to yourself. It provides you valuable insight into which common mistakes you make as a developer and how you can improve. Instead of waiting for your users to find them, testing your code will go a long way. There’s a lot of info available on Android’s page about testing.

If you have any questions about nullability, or perhaps thoughts about the meaning of nothing, please join the discussion below!

Add a rating for this content

Contributors

Comments