background-shape

Namaskaram!

Problem

Android provides the following intent launcher APIs:

  • Legacy, Deprecated : startActivityForResult() & onActivityResult()

  • New: registerForActivityResult()

Both APIs are tightly coupled with Activity as they are functions of Activity.

Launching such intents from composables is tricky because of 2 reasons:

  1. Passing launcher instance to Composable : registerForActivityResult() returns an instance of ActivityResultLauncher. To launch, this instance is required ( like launcher.launch(intent) ). For Composable to be able to launch, launcher needs to passed to it.

  2. Returning result from callback to Composable : registerForActivityResult() takes a parameter ActivityResultCallback in which we receive the result from intent. If Composable fires the intent, then probably it is the one that needs the result. But the problem here is that we need to pass the callback while registering (and intent registration needs to be done in Activity’s onCreate() lifecycle function). So, returning this result from callback to composable is also difficult.

So with the help of this article, I would like to show a way to bridge this gap using lambdas. Let’s dive right in!


Solution

Let’s take an example of launching imagePicker.

We can divide the startActivityForResult flow into 3 steps:

  1. Registration

  2. Launcher

  3. ResultHandler

S1. Registration

Registration can only be done in Activity’s onCreate() function, which is straight forward :

class MainActivity : ComponentActivity() {

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

        setContent { /* ... */ }

        registerIntentLaunchers()
    }

    private lateinit var imagePickerLauncher: ActivityResultLauncher<Intent>

    private fun registerIntentLaunchers() {
        imagePickerLauncher =
            registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
                //...
            }
    }
}

S2. Launcher

Coming to Launcher, this need not be done directly in Activity. Suppose we have a @Composable ImagePickerScreen() in which onClick() of a button, we need to launch this intent. To achieve this, firstly we create a launcher function in Activity:

private fun launchImagePicker() {
    ImagePicker.with(this)
        .cropSquare()
        .compress(1024)
        .createIntent { intent ->
            imagePickerLauncher.launch(intent)
        }
}

Now launchImagePicker() needs to be passed as lambda to the composable, and this lambda can be easily invoked inside onClick of the button.

@Composable
fun ImagePickerScreen(
    launchImagePicker: () -> Unit
) {

    Column(
        Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {

        //...

        Button(
            onClick = {
                launchImagePicker()
            },
            modifier = Modifier.padding(16.dp)
        ) {
            Text(text = "Pick Image")
        }
    }
}

S3. ResultHandler

Finally coming to ResultHandler (S3). We want the result of intent in the onClick itself. But it is not as simple as:

val uri = launchImagePicker()

The reason being we receive the result in ActivityResultCallback which is passed while registration and returning from there is not possible.

So, here is a solution - we can have another lambda i.e. resultHandler. This lambda is to be provided while invoking launcher function - launchImagePicker(). This resultHandler will then be invoked in the ActivityResultCallback when we receive the result. Here is the Activity code :

private lateinit var imagePickerLauncher: ActivityResultLauncher<Intent>
private var imagePickerResultHandler: ((Uri) -> Unit)? = null

private fun registerIntentLaunchers() {
    imagePickerLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
        val resultCode = result.resultCode
        val data = result.data

        when (resultCode) {
            Activity.RESULT_OK -> {
                //Image Uri will not be null for RESULT_OK
                val fileUri = data?.data!!

                imagePickerResultHandler?.invoke(fileUri)

            }
            ImagePicker.RESULT_ERROR -> {
                Toast.makeText(this, ImagePicker.getError(data), Toast.LENGTH_SHORT).show()
            }
            else -> {
                Toast.makeText(this, "Task Cancelled", Toast.LENGTH_SHORT).show()
            }
        }
    }
}

private fun launchImagePicker(
    resultHandler: (Uri) -> Unit
) {
    imagePickerResultHandler = resultHandler

    ImagePicker.with(this)
        .cropSquare()
        .compress(1024)
        .createIntent { intent ->
            imagePickerLauncher.launch(intent)
        }
}

Observe:

  • The launchImagePicker(resultHandler: (Uri) -> Unit) function now takes resultHandler as input.

  • It then sets it to local property of activity - imagePickerResultHandler so that ActivityResultCallback can access it.

  • Finally, in ActivityResultCallback, we invoke this lambda when the result is received.

Final code

View the final source code here.


Conclusion

To understand the complete solution in simple words : Composable couldn’t launch activity for result directly, so we asked Activity to provide the code (i.e. the launcher lamdba) to do so. And in return the activity asks for code (i.e. the resultHandler lambda) that needs to be executed when result is received in Activity.

You can tweak the launcher & resultHandler according to your needs. Not only that, this approach can be used for any other ActivityForResult flows eg.- Login flow.

Any positive or negative feedback, improvements, issues on the approach are welcomed.

Thank you for your time.