Starting Activities for Results in Android

There are many situations in Android where we need to start a second activity from the current activity or fragment. This second activity could either be another internal activity as defined by your application, or it could be from some external application that we can leverage to do something such as take a picture, choose from a list of contacts, or view something in Google Maps.

Furthermore, it's very likely that when we want to leverage some other application's functionality we also want that other application to provide us with a result. Luckily, Android provides us with a mechanism for receiving information back from other activities.

In this article, I want to go over the two different approaches that are available to us to solve this issue: one is the original, but deprecated approach, and the other is the modern approach.

I will provide small code snippets where needed, but the full sample project can be found here on GitHub

💡
Both solutions are available in activities and fragments. However, for brevity, I'll just be referencing activities.

Project Setup

To start, I created a brand new project using Android Studio's "Empty Views Activity" template. Then along with the provided MainActivity I added a SecondActivity

I added one button to the layout of MainActivity that when clicked will open SecondActivity. In the layout of SecondActivity I have a TextView to display any input passed from MainActivity, an EditText to input a result to send back to MainActivity, and lastly a Button that when clicked will finish the activity and return the result.

MainActivity:

SecondActivity:

The Legacy Approach

The legacy solution is simple enough to implement, and it revolves around two key functions: startActivityForResult(...), and onActivityResult(...). Both are provided by the activity's parent class.

Starting SecondActivity with an Input

In MainActivity, I implemented a click listener for the button:

binding.btnStartSecondActivityOld.setOnClickListener {  
    startSecondActivityForResultUsingOldWay()  
}

private fun startSecondActivityForResultUsingOldWay() {  
    val intent = Intent(this, SecondActivity::class.java).putExtra(  
        MAIN_ACTIVITY_BUNDLE_ID,  
        "Input From Main Activity Old Way"  
    )  

    startActivityForResult(  
        intent,  
        MAIN_ACTIVITY_REQUEST_CODE  
    )  
}

First, I create an Intent that specifies the current context and the name of the activity's class I wish to start. Also, I used putExtra to store a String in the Bundle of the Intent, which will be accessible to SecondActivity as input. Then once I have the Intent created, all I have to do is call startActivityForResult(...) passing along the Intent and a request code.

💡
The request code can be anything. It can be used to keep track of different requests if you expect to be dealing with multiple requests.

Handling Input and Returning a Result

In SecondActivity.onCreate(...), I first retrieve the input from the Bundle of the Intent and display it. Then I add a click-listener to the button that when fired will return the result. The click listener creates its own Intent to bundle the return data. Then it calls setResult(...) to send the Intent and a result code back to MainActivity. Lastly, it calls finish() to end the activity.

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

    val inputFromMainActivity = intent.getStringExtra(MainActivity.MAIN_ACTIVITY_BUNDLE_ID)  

    binding.tvInput.text = inputFromMainActivity

    binding.btnSendResult.setOnClickListener {  
        val result = binding.etResult.text.toString()  
        val intent = Intent().putExtra(SECOND_ACTIVITY_BUNDLE_ID, result)  
        setResult(SECOND_ACTIVITY_RESULT_CODE, intent)  
        finish()  
    }
}
💡
Much like the request code, the result code can also be any integer. For convenience, the Activity class already has some predefined constants, like Activity.RESULT_OK.

Handling the Result

To process the result back in MainActivity we override the onActivityResult(...) function. onActivityResult(...) has arguments for a request code and a result code so you can uniquely handle each result based on where it came from. It also has an Intent argument which stores the returned result.

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {  
    super.onActivityResult(requestCode, resultCode, data)  

    if (requestCode == MAIN_ACTIVITY_REQUEST_CODE && resultCode == SecondActivity.SECOND_ACTIVITY_RESULT_CODE) {  
        data?.getStringExtra(SecondActivity.SECOND_ACTIVITY_BUNDLE_ID)?.let {  
            Toast.makeText(this, "Got Result: $it", Toast.LENGTH_SHORT).show()  
        }  
    }  
}

Limitations

This seems straightforward enough to implement, so why would this be deprecated?

One problem is that since the request code is just an integer there is a possibility of duplication between two unrelated requests, or even the wrong integer value being used by mistake which could lead to confusing behavior. A way to circumvent these issues is to define a library of request code constants for each type of request, but that is still not ideal if there are many codes to manage.

A similar issue is that onActivityResult(...) is a single function that has to handle all requests. Depending on the number of requests, this function could just turn into one large, messy switch statement.

The New Activity Result API

The new Activity Result API centers on three new classes: ActivityResultContract, ActivityResultCallback, and ActivityResultLauncher. These three classes are used in the registerForActivityResult(...) function, which is provided to us by the activity or fragment.

@NonNull   
public final <I, O> ActivityResultLauncher<I> registerForActivityResult(  
    @NonNull ActivityResultContract<I, O> contract,  
    @NonNull ActivityResultCallback<O> callback
)

As the name implies, registerForActivityResult(...) will register your callback, but it will not launch anything. That is the responsibility of the ActivityResultLauncher object that registerForActivityResult(...) returns. We can use the ActivityResultLauncher wherever in our code we are ready to launch the request.

The first argument we need to pass registerForActivityResult(...) is an instance of an ActivityResultContract. This contract is essentially a way to define the input and output for the request in a type-safe way. We'll look more into the details of contracts in a later section when we implement our own. Luckily, there's an existing ActivityResultContracts class (notice the additional 's' on the end) that contains several prebuilt contracts to use.

Finally, the last piece is the ActivityResultCallback argument. This is what is called when the result is ready to be returned. The callback is given the type of result as defined by the output of the contract.

Implementation

First, I defined a top-level ActivityResultLauncher property for the MainActivity class.

private val startSecondActivityForResultLauncher: ActivityResultLauncher<Intent> = 
    registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult: ActivityResult ->  
        if (activityResult.resultCode == SecondActivity.SECOND_ACTIVITY_RESULT_CODE) {
            activityResult.data?.getStringExtra(SecondActivity.SECOND_ACTIVITY_BUNDLE_ID)?.let {  
                Toast.makeText(this, "Got Result: $it", Toast.LENGTH_SHORT).show()  
            }  
        }  
    }

For the contract, I am passing the prebuilt StartActivityForResult contract, which defines an Intent as an input and an ActivityResult as the output.

Therefore, in the callback function, I can take the ActivityResult, do some code comparisons and then grab the data from the bundle just like in onActivityResult(...) from the legacy approach.

For the UI I add another button to my view and then define an on-click to launch the request:

<Button  
    android:id="@+id/btnStartSecondActivityNew"  
    android:layout_width="wrap_content"  
    android:layout_height="wrap_content"  
    android:text="Start SecondActivity (new)"  
    app:layout_constraintEnd_toEndOf="parent"  
    app:layout_constraintStart_toStartOf="parent"  
    app:layout_constraintTop_toBottomOf="@+id/btnStartSecondActivityOld" 
/>
binding.btnStartSecondActivityNew.setOnClickListener {  
    startSecondActivityUsingNewApi()  
}

private fun startSecondActivityUsingNewApi() {  
    val intent = Intent(  
        this,  
        SecondActivity::class.java  
    ).putExtra(  
        MAIN_ACTIVITY_BUNDLE_ID,  
        "Input From Main Activity New API"  
    )  
    startSecondActivityForResultLauncher.launch(intent)
}

Since the StartActivityForResult contract takes an Intent as an input, I create one in the same way that I did when calling startActivityForResult(...) for the legacy approach. The Intent defines the context, the activity class to start, as well as some input String value. Then it's as simple as calling the launch(...) function on our ActivityResultLauncher object and passing in the Intent as input.

Benefits

This approach provides us with a few very nice benefits over the deprecated approach:

  • We no longer have to worry about request code handling and maintenance. Each separate request that's launched has its own callback. We are no longer limited to one callback function (onActivityResult(...)) handling all requests.

  • Because we are using the ActivityResultContract class, we get the convenience of a strongly typed input and output.

  • ActivityResultContract also wraps up some of the boilerplate code for managing the input and output of the request. We will see in the next section that if we create our own custom contract we can move even more input and output logic into the contract.

Activity Result API with a Custom Contract

Creating our own custom contract involves implementing two functions from ActivityResultContract: one to define how our input is processed and one to define how the output is processed.

abstract class ActivityResultContract<I, O> {
    abstract fun createIntent(context: Context, input: I): Intent

    abstract fun parseResult(resultCode: Int, intent: Intent?): O
}

You'll notice that for the implementation, all we are doing is moving some code from the activity into these functions. In createIntent(...) I'm creating the same intent as before. Then in parseResult(...) I'm doing the same result code checks and getting the result from the Bundle as before.

class MyCustomContract : ActivityResultContract<String, String?>() {  
    override fun createIntent(context: Context, input: String): Intent {  
        val intent = Intent(  
            context,  
            SecondActivity::class.java  
        ).putExtra(  
            MAIN_ACTIVITY_BUNDLE_ID,  
            input  
        )
        return intent
    }  

    override fun parseResult(resultCode: Int, intent: Intent?): String? {  
        if (resultCode == SecondActivity.SECOND_ACTIVITY_RESULT_CODE) {  
            intent?.getStringExtra(SecondActivity.SECOND_ACTIVITY_BUNDLE_ID)?.let {  
                return it  
            }  
        }  
        return null  
    }   
}

Now that I have my custom contract I can go ahead and define another top level ActivityResultLauncher property for the MainActivity class. Notice that now I don't have to handle any result code checking in the callback since I handle all of that in MyCustomContract.parseResult(...) instead.

private val startSecondActivityCustomContractLauncher =
        registerForActivityResult(MyCustomContract()) { result: String? ->
            result?.let {
                Toast.makeText(this, "Got Result: $it", Toast.LENGTH_SHORT).show()
            }
        }

I again updated the UI with another button and click listener. Similarly to the simplified callback definition, the launch call is also simplified since all I have to do is pass the input and MyCustomContract.createIntent(...) handles any Intent building logic.

<Button  
    android:id="@+id/btnStartSecondActivityCustomContract"  
    android:layout_width="wrap_content"  
    android:layout_height="wrap_content"  
    android:text="Start SecondActivity (Custom Contract)"  
    app:layout_constraintEnd_toEndOf="parent"  
    app:layout_constraintStart_toStartOf="parent"  
    app:layout_constraintTop_toBottomOf="@+id/btnStartSecondActivityNew" 
/>
binding.btnStartSecondActivityCustomContract.setOnClickListener {  
    startSecondActivityCustomContractLauncher.launch("Custom Contract Input")  
}

Conclusion

The Activity Result API provides us with a safer and more convenient way of launching activities that are expected to return some result.

We first looked at the old approach and its straightforward implementation. However, although it works well for simple cases, it can get messy for complex cases where we need to handle many different requests. Managing request code uniqueness, and having one large callback function to handle all requests is less than ideal from a maintainability standpoint.

We then explored how to utilize ActivityResultContract, ActivityResultCallback, ActivityResultLauncher, and registerForActivityResult(...) from the Activity Result API. This implementation removed the need for request code handling, brought us type safety, and cleaned up the code by allowing for individual callbacks for each request type.

Lastly, we looked at how to create our own ActivityResultContract implementation. By rolling our own, we were able to further clean up the code in the activity by moving the intent creation and much of the result handling into the contract.


If you noticed anything in the article that is incorrect or isn't clear, please let me know. I always appreciate the feedback.