Activity / Jetpack

Activity Result Contract – The Basics

As regular readers of Styling Android will know, I generally publish sample code projects along with each series of articles. Whenever the code requires runtime permissions my heart sinks because I know that I must add a chunk of boilerplate. This not only means extra work for me, but it can also make the sample code more difficult to follow. However, there is a relatively new addition to AndroidX Activity which simplifies this somewhat: ActivityResultContracts.

Background

Original image by janjf93
from Pixabay

Requesting runtime permissions has been a requirement since API 23 (Marshmallow) which was released in 2015. To request permission from the user, we need to pass control to the OS to perform this request. This generally uses the startActivityForResult() flow (if not directly, then indirectly) so that we’ll get a specific method invoked when control is returned to us.

A new pattern that was introduced in the Jetpack Activity library 1.2.0-alpha02 which replaces startActivityForResult() followed by an invocation of onActivityResult(). We can use this new pattern wherever we previously used startActivityForResult() – it is not restricted to runtime permissions. For example, we can use it to take a picture, or open a document, etc.

Contracts

The new APIs are based on different contracts which are specific to each use-case. For example, when requesting runtime permission, we need to know whether the permission has been granted. However, when we request the taking of a picture, the result needs to be a Uri to the picture that was taken. An example will help to explain this:

    private val requestPermissions =
        registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
            if (isGranted) {
                navController.navigate(R.id.grantedPermissionsFragment)
            }
        }

Here the contract is ActivityResultsContracts.RequestPermission(). It extends ActivityResultsContract which is a generic class with two type parameters representing the input and output types. For a permission request, the input is a string representing the permission, and the output is a boolean indicating whether the permission has been granted.

Internally this contract implements the logic for requesting the required permission.

The registerForActivityResult() function takes two arguments. The first is the contract, and the second is a lambda which will be invoked on completion. The output type of the contract dictates the argument type of the lambda. In this example, it is a boolean that indicates whether the permission was granted.

The registerForActivityResult() function returns an ActivityResultLauncher which we can invoke when we want to perform the operation:

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

    binding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(binding.root)

    val permission = Manifext.permission.READ_PHONE_STATE

    when {
        ContextCompat.checkSelfPermission(this, permission) == PERMISSION_GRANTED ->
            navController.navigate(R.id.grantedPermissionsFragment)
        shouldShowRequestPermissionRationale(permission) -> showRationale()
        else -> requestPermissions.launch(permission)
    }
}

private fun showRationale() {
    MaterialAlertDialogBuilder(this)
        .setTitle(R.string.dialog_title)
        .setMessage(R.string.dialog_message)
        .setPositiveButton(R.string.button_ok) { _, _ ->
            requestPermissions.launch(permission)
        }
        .setNegativeButton(R.string.button_cancel) { _, _ -> }
        .show()
}

Here we first check whether we already have the required permission. If so, we navigate to the appropriate destination. If we don’t have the necessary permission we check whether we should show thew request permission rationale. Otherwise, we launch the ActivityResultLauncher instance that we created earlier. The input type of the contract dictates the argument for the launch method. In this case it is a string representing the required permission.

There is a separate contract that we can use for requesting multiple permissions in a single operation.

Benefits

This approach makes the code much easier to understand, imo. Therefore it is easier to maintain because someone viewing it for the first time requires less cognitive load to understand the logic.

The previous pattern of startActivityForResult() / onActivityResult() required un understanding of how this mechanism works. However, looking at requestPermissions.launch(...) should lead directly to the lambda which will be invoked on completion.

Moreover, this does not require us to even know that control is passing elsewhere while this is running. It is an opaque box operation and we’re agnostic of the details. All that we need to know is that we launch the contract and the lambda is invoked with the result once it completes.

There are a number of ready-made contracts – just look at the known subclasses of ActivityResultContract for a list. Further to that, it is actually fairly trivial to create our own subclass of ActivityResultContract to create custom logic.

Conclusion

ActivityResultContracts are a really useful addition. As I have already mentioned, I think that it greatly improves the understandability of our code. That said, this is one specific use-case, and in the next article, we’ll delve a little deeper.

The source code for this article is available here.

© 2021, Mark Allison. All rights reserved.

Copyright © 2021 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.