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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Null Safety Tutorial in Kotlin: Best Practices
30 mins
- Getting Started
- Finding a NullPointerException
- Performing Null Checks
- Using the Safe Call Operator
- Making Use of Let
- Running with Run
- Converting to Safe Casts
- Analyzing Equality
- Using the Elvis Operator
- Designing by Contract
- Thinking About Return Values
- Asserting Not-Null
- Testing With Assertions
- Delaying Initialization
- Using Null in Collections
- Interoperating With Other Languages
- Nullability in Java
- Nullability in C++
- Where to Go From Here
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 this
run 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
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:
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 Build ▸ Make Project. You should see Android Studio compile correctly with all of these changes:
Now that your code is up to Kotlin’s standard, you’ll fix that crash you encountered when you first explored the project:
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
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! :]
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.