Joao Alves

Engineering Manager at Skyscanner

Kotlin runtime checks — require and check

Posted at — Apr 11, 2018

image

Hey everyone, and welcome to article number 10 on the Kotlin Playground Series. Last time we looked into a few Kotlin standard functions that are very common (let, apply, run, with and also). Today we’ll be looking into another couple of standard functions that are not as popular, maybe because we don’t do these things as often but we definitely should.

The title mentions runtime checks, so what do I mean by that? Let’s see, have you ever written some code like this in Java?

public class SomeActivityJava extends AppCompatActivity {

    private static final String SOME_EXTRA_KEY = "some_extra_key";

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        if (!getIntent().getExtras().containsKey(SOME_EXTRA_KEY)) {
            finish();
        }

        String arg = getIntent().getExtras().getString(SOME_EXTRA_KEY);
        // do something with passed argument
    }
}

Or like this?

public class SomeActivityJava extends AppCompatActivity {
  
    private boolean isRefreshing = false;

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        refreshSomething();
    }

    private void refreshSomething() {
        if (isRefreshing) {
            return;
        }
        isRefreshing = true;
        // do something that requires a valid state
    }
}

Well, this is what I mean when I say runtime checks, verifying that the state or context of your app is valid before continuing to do something else. Now, some of us don’t like to crash the app for some of those cases and just let it run but why?

Let’s look at the first example: if we don’t throw an exception and just call finish() the user will see a flash of the screen and go back to the previous screen again. Do we want this behaviour? Wouldn’t it be better to have the exception being thrown and having the app crash?

If that was the case, hopefully you would get this crash during the development stage and could actually debug and find out what the issue is, fixing it and it wouldn’t actually happen in production to any of your valuable users. If you just call finish not only you don’t help the user experience as you never see or realise the issue exists if it never happens to you.

This is why I believe these kinds of checks are important and we should add them whenever necessary. As well as writing tests this is a very nice way of failing fast and finding potential bugs that don’t really need to go into production.

But this article is about Kotlin and some standard functions I said, so let’s get into that now that the why is explained. Kotlin has a couple of standard functions that allow us to do these verifications super quickly without writing any of that boilerplate code above, let’s look into them.

require

inline fun require(value: Boolean)

We can use require to test function arguments on in our case to test if the intent has the required extra. It throws an IllegalArgumentException when the argument passed is false.

This is good, we’ll definitely going to catch some bugs with this but it would be nice to see some more meaningful messages in the logs. Well, require has another variation where it takes a message we can use for debug as a second argument:

inline fun require(value: Boolean, lazyMessage: () -> Any)

Great, with this variant it’ll be even easier to debug and catch the exact bug much faster. If what we require is that something is not null we can use requireNotNull directly rather than passing a null check as an argument. It also has a variant with the lazy message and another one without it:

inline fun <T : Any> requireNotNull(value: T?): T
inline fun <T : Any> requireNotNull(
    value: T?, 
    lazyMessage: () -> Any
): T

So, if we want to write the first Java example in Kotlin, this is what we do:

class SomeActivity : AppCompatActivity() {

    companion object {
        private const val SOME_EXTRA_KEY = "some_extra_key"
    }

    public override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        require(intent.extras.containsKey(SOME_EXTRA_KEY), {
            "SOME_EXTRA is required to launch SomeActivity"
        })

        val passedArgument = intent.extras.getString(SOME_EXTRA_KEY)
        // do something with passed argument
    }
}

Rather than doing finish() and letting the user going to the previous screen without a clue why this is happening and not working we crash the app with an IllegalArgumentException and our lazy message to help us debug why it happened.

check

inline fun check(value: Boolean)

If we look at check above it looks pretty similar to require and in fact, it is. The only difference being it throws an IllegalStateException rather than an IllegalArgumentException which means we should use it to test object states and not function arguments like we do with the require function. Basically, we should use it to replace our 2nd Java example from the beginning of the article.

With check we also have the variant that takes a message as a second parameter, so we can add our meaningful message to help with debugging.

inline fun check(value: Boolean, lazyMessage: () -> Any) (source)

And since both functions are so similar, we also have a checkNotNull function to directly validate if object state is not null, also with both variants:

inline fun <T : Any> checkNotNull(value: T?): T
inline fun <T : Any> checkNotNull(
    value: T?, 
    lazyMessage: () -> Any
): T

Now, if we have one of those cases where the state is something that it should never be, rather than allowing the app flow to proceed and possibly provide weird and wrong behaviour to the user, we crash it and are able to debug and find the issue.

It’s another one of those cases, where some flows should fail fast. There’s no point in moving to the next step if we know it’s going to fail or cause a wrong behaviour or output for the user somewhere down the way.

So, if we want to write the 2nd Java example in Kotlin, this is what we do:

class SomeActivity : AppCompatActivity() {

    private var isRefreshing = false

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

    private fun refreshSomething() {
        check(!isRefreshing, {
            "Trying to refresh state while already refreshing"
        })

        isRefreshing = true
        // do something that requires a valid state
    }
}

Rather than returning and exiting the function without doing anything, and maybe cause an unexpected behaviour later in the flow, we crash the app with an IllegalStateException and our lazy message to help us debugging.

And that’s all for today, a small one but hopefully useful. Give it a go with these functions and find and get rid of some of those weird bugs that we don’t even realise we introduce. If you enjoyed the article don’t forget to 👏 and please share ideas and comments as usual. The code is available under the runtimechecks package on the series project. See you at number 11 👋

Kotlin standard functions — just another guide ⇦ PREVIOUS

NEXT ⇨ Kotlin try-with-resources — use


comments powered by Disqus