DEV Community

bright inventions
bright inventions

Posted on • Updated on • Originally published at brightinventions.pl

Don't fool yourself with lateinit modifier

Kotlin language provides a nice and clean way to handle nullable variables in code so that it is less error prone. Unless you do it all wrong, of course.

lateinit modifier

Normally, all non-nullable properties in Kotlin classes must be properly initialized. You can do it in several ways:

  • in primary constructor,
  • in initializer block(s),
  • in the class body,
  • with a getter*,
  • with a delegate*.

* Well, it's not an initialization per se, but it makes compiler aware that's not null

But if your property is lifecycle-driven (e.g. a reference to a button which gets inflated during Android Activity lifecycle) or it is initialized through injection, you cannot supply a non-null initializer and you must declare its type as nullable. This in turn will require you to use null checks every time you reference the property, which may be a bit inconvenient, especially if you are absolutely sure the property will get initialized at some point, before you access it for the first time.

Kotlin has a simple solution for such a scenario, allowing you to mark the property with the lateinit modifier. Thanks to this you can have a non-nullable type so that you don't have to check if it's null while referencing it. Of course, if you access the property before initialization, you'll get an UninitializedPropertyAccessException.

Lateinits vs nullables

The described possibility, despite being a great feature, may also be tempting to overuse in places where the initialization is not so certain (e.g. conditional or just too late), making the code less null-aware, less predictable and more like Java, thus - naturally - you might be doubtful if certain usages are right or not. So here's my rule of thumb that I use when making decision if particular variable should be lateinit or just nullable. It's very subjective and it tends to change with time so feel free to share your own experiences in comments.

Lateinit #1: beginning of a lifecycle

You can intialize properties when your classes' lifecycle-beginning methods get invoked, e.g. Activity.onCreate(). It's a common case if you use a dependency injection framework like Dagger.

abstract class BaseActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        AndroidInjection.inject(this)
        super.onCreate(savedInstanceState)
    }
}

class LoginActivity : BaseActivity() {

    @Inject
    lateinit var viewModelFactory: ViewModelProvider.Factory

    lateinit var viewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        viewModel = ViewModelProviders.of(this, viewModelFactory).get(LoginViewModel::class.java)

        setContentView(R.layout.login_activity)
    }
}
Enter fullscreen mode Exit fullscreen mode

It's also very useful if you want to getSystemService():

class MainActivity : AppCompatActivity() {

    lateinit var alarmManager: AlarmManager

    // This wouldn't work:
    // val alarmService = getSystemService(AlarmManager::class.java)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        alarmManager = getSystemService(AlarmManager::class.java)
}

Enter fullscreen mode Exit fullscreen mode

Otherwise you would get an IllegalStateException saying that system services are not available to Activities before onCreate().

You can also avoid the this reference escape during object construction for thread-safety:

class MyActivity : AppCompatActivity() {

    lateinit var alien: Alien
    // instead of:
    // val alien = Alien(this)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        alien = Alien(this)
    }
}
Enter fullscreen mode Exit fullscreen mode

Lateinit #2: fail-fast approach

You may prefer to write a code that is designed to fail as soon as possible and stop normal operation rather than attempt to continue a flawed process. An extreme case could be crashing the app so that you can detect failures early. It can be especially useful in situations where the code is straightforward and predictable.

For example, let's imagine an Activity that uses MediaPlayer to play music. It might look like this:

class PlayerActivity : AppCompatActivity() {

    lateinit var mediaPlayer: MediaPlayer
    lateinit var mediaUri: Uri

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_player)
        mediaUri = intent.getParcelableExtra("mediaUri")
    }

    override fun onStart() {
        super.onStart()
        mediaPlayer = MediaPlayer()
        mediaPlayer.setDataSource(this, mediaUri)
        mediaPlayer.prepare()
        mediaPlayer.start()
    }

    override fun onStop() {
        super.onStop()
        mediaPlayer.stop()
        mediaPlayer.release()
    }
}
Enter fullscreen mode Exit fullscreen mode

It's quite easy to spot a risky assignment in onCreate as retrieving mediaUri from the passed intent may end up with an exception if the intent did not contain it.

java.lang.IllegalStateException: intent.getParcelableExtra("mediaUri") must not be null

On the other hand, the code is clear and simple and it's not too hard to test it. But if you feel your specific case needs a bit of explanation then I encourage you to document such design decision.

Risk inside

Nullable #1: crash-free approach

There might be cases when you would like to avoid crashing the app at all cost, even if something is not going to work properly. It doesn't mean that using lateinit like in the previous "fail-fast" example must end up with a crash as you can catch the exceptions, but I think it's quite common to choose between these two extremes: either let the app crash and don't mind catching the exceptions or avoid crashing using nullable variables.

Let's modify the previous example by replacing lateinits with nullable types:

class PlayerActivity : AppCompatActivity() {

    var mediaPlayer: MediaPlayer? = null
    var mediaUri: Uri? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_player)
        mediaUri = intent.getParcelableExtra("mediaUri")
    }

    override fun onStart() {
        super.onStart()

        mediaUri?.let {
            val player = MediaPlayer()
            player.setDataSource(this, it)
            player.prepare()
            player.start()
            mediaPlayer = player
        }
    }

    override fun onStop() {
        super.onStop()
        mediaPlayer?.stop()
        mediaPlayer?.release()
    }
}
Enter fullscreen mode Exit fullscreen mode

So this time, if we don't get any mediaUri, we just don't play the music. The app won't crash, but the user will be surprised by the silence (he might even wonder if his phone is broken). Is it better than crashing? That's your choice. But the good thing about this approach is that you have an opportunity to actually test the variable using a regular if statement and do something else if it wasn't initialized with a non-null value.

Note: since Kotlin 1.2 you can use .isInitialized on a reference to a lateinit property, but it may render your code even more unreadable and it has some limitations (see the docs):

This check is only available for the properties that are lexically accessible, i.e. declared in the same type or in one of the outer types, or at top level in the same file.

Nullable #2: very late initialization

I've seen a piece of code that was "remembering" a clicked list item data in a lateinit variable, starting another Activity with startActivityForResult() and finally, when it has finished and onActivityResult() was called, the "remembered" value was read and used (and I was quite surprised it hasn't been lost in the meantime).

class SomeActivity : AppCompatActivity() {

    lateinit var selectedItemData: ItemData

    // a ton of code ...

    private fun doSomethingWithItem(data: ItemData) {
        // another ton of code ...

        if (...) { // long and complex conditions
            if (...) {
                data?.anotherData?.let { // may not happen and won't let you know
                    selectedItemData = data
                    startActivityForResult(...)
                }
            }
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        doSomethingElse(selectedItemData)
    }
}
Enter fullscreen mode Exit fullscreen mode

Generally speaking, I avoid such constructions and I call them "too late initialization". In such cases I prefer to use a nullable type instead to make it clear the variable is null most of the time.

How not to fool yourself?

Make the property nullable if it makes better sense and improves null safety. lateinit might be good if used properly but it has its cons as well. Don't make it a replacement for NullPointerException.

Originally published at brightinventions.pl

By Andrzej Zabost, Android Developer at Bright Inventions
Blog, Email

Oldest comments (1)

Collapse
 
andrewtheant profile image
Andrew Bastin • Edited

Isn't it better to use the lazy delegate in those cases ?

Like instead of:

class MainActivity : AppCompatActivity() {

    lateinit var alarmManager: AlarmManager

    // This wouldn't work:
    // val alarmService = getSystemService(AlarmManager::class.java)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        alarmManager = getSystemService(AlarmManager::class.java)
}

You could use :

class MainActivity : AppCompatActivity() {

    val alarmManager: AlarmManager by lazy {
         getSystemService(AlarmManager::class.java)
    }


    // This wouldn't work:
    // val alarmService = getSystemService(AlarmManager::class.java)

    override fun onCreate(savedInstanceState: Bundle?) {       
         super.onCreate(savedInstanceState)
    }