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, theActivity
class already has some predefined constants, likeActivity.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.
Top comments (1)
Thank you very much for this material.
I'm pretty dumb when it comes to developing on Android and this material of yours helped me a lot.
I'm already going to refactor a resource following these practices.