Google has finally released the new Activity Result APIs, which are something I’ve been looking forward to for a very long time. In this post I’ll run through the basics, how you can create your own contracts, and how this allows us to abstract away even more responsibilities from your UI.
The basics
As I’m sure you’re aware, when you want to request data from another activity (say for instance, requesting an image from a camera), you would override onActivityResult
. This works absolutely fine but has a couple of downsides:
Repetitive logic
Actually receiving the result requires some slightly tedious code. You need to check the request code to see if it was your activity that requested what’s being returned, then you need to check to see whether or not the request was successful. After that, you pull the data out of the Intent
object. It’s become commonplace to see code like this littered around the UI:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_CODE && resultCode == Activity.RESULT_OK) {
if (data != null) {
val requiredValue = data.getStringExtra("key")
} else {
// Some error handling
}
}
}
Wouldn’t it be nice if we could abstract this away?
Tight coupling
The only place to get these onActivityResult
callbacks is in an Activity
or Fragment
and there’s simply no getting around that.
The Activity Result API
Starting with Activity 1.2.0-alpha02
and Fragment 1.3.0-alpha02
, we now have a nice abstraction which allows us to handle onActivityResult
callbacks in a neat and reusable way, and Google were kind enough to add a few commonly used contracts so that we don’t need to manage them ourselves.
val takePicture = prepareCall(ActivityResultContracts.TakePicture()) { bitmap: Bitmap? ->
// Do something with the Bitmap, if present
}
This is the new callback which we can register at any point in our Activity
. Making the actual request is as simple as invoking takePicture
:
private val takePicture = prepareCall(ActivityResultContracts.TakePicture()) { bitmap: Bitmap? ->
// Do something with the Bitmap, if present
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener { takePicture() }
}
So what’s going on here? Let’s break it down slightly. takePicture
is just a callback which returns a nullable Bitmap
- whether or not it’s null depends on whether or not the onActivityResult
process was successful. prepareCall
then registers this call into a new feature on ComponentActivity
called the ActivityResultRegistry
- we’ll come back to this later. ActivityResultContracts.TakePicture()
is one of the built-in helpers which Google have created for us, and finally invoking takePicture
actually triggers the Intent
in the same way that you would previously with Activity.startActivityForResult(intent, REQUEST_CODE)
.
The built in ActivityResultContracts
The built-in ActivityResultContracts currently include a few common operations:
- Requesting multiple permissions with
RequestPermissions
- Requesting just one permission with
RequestPermission
- Making a phone call with
Dial
- And taking a picture with
TakePicture
So what do these actually do under the hood? Let’s take a closer look at TakePicture
:
public static class TakePicture extends ActivityResultContract<Void, Bitmap> {
@NonNull
@Override
public Intent createIntent(@Nullable Void input) {
return new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
}
@Nullable
@Override
public Bitmap parseResult(int resultCode, @Nullable Intent intent) {
if (resultCode != Activity.RESULT_OK) return null;
if (intent == null) return null;
return intent.getParcelableExtra("data");
}
}
ActivityResultContract
takes two type parameters which correspond to data that’s required to make this request (in this case, nothing or Void
but quite often a String
such as a URI), and the data type to be returned - a Bitmap
. As we can see, the contract has two functions, createIntent
and parseResult
:
public abstract class ActivityResultContract<I, O> {
@NonNull
public abstract Intent createIntent(I input);
public abstract O parseResult(int resultCode, @Nullable Intent intent);
}
In these built-in contracts, you can see we’ve moved the logic which previously tended to live in our Activity
and moved them to pretty trivial helper classes. It’s also incredibly easy for us to write our own reusable contracts: we simply specify the inputs and outputs, and handle the Intent
creation and Intent
+ resultCode
parsing ourselves.
For a concrete example: we often launch one of our own Activities to send custom data types back. Here’s a ActivityResultContract
which would allow us to handle that:
@Parcelize
data class SomeArtibtraryData(
val aString: String,
val anInt: Int
) : Parcelable
class FetchArbitraryData : ActivityResultContract<Context, SomeArtibtraryData?>() {
override fun createIntent(input: Context?): Intent =
Intent(input, SecondActivity::class.java)
override fun parseResult(
resultCode: Int,
intent: Intent?
): SomeArtibtraryData? = when {
resultCode != Activity.RESULT_OK -> null
else -> intent?.getParcelableExtra<SomeArtibtraryData>("data")
}
}
private val fetchArbitraryData = prepareCall(FetchArbitraryData()) { data ->
// Handle data
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener { fetchArbitraryData(this) }
}
Lovely. However, this is still coupled to the Activity
or Fragment
. Is there a way around this?
The ActivityResultRegistry
Happily, there is, and it allows us to do all sorts of clever things. The ActivityResultRegistry
is a new feature of the ComponentActivity
class, and ultimately it contains a list of callbacks to be invoked when onActivityResult
is triggered. All we require is a reference to one to be able to register our listeners from any class:
class TakePictureHandler(
private val registry: ActivityResultRegistry,
private val func: (Bitmap) -> Unit
) : DefaultLifecycleObserver {
private lateinit var resultLauncher: ActivityResultLauncher<Void>
override fun onCreate() {
resultLauncher = registry.registerActivityResultCallback(
"key",
ActivityResultContracts.TakePicture()
) { bitmap -> func(bitmap) }
}
fun takePicture() {
resultLauncher()
}
}
It’s worth pointing out that the resultLauncher()
invocation here is an activity-ktx
extension for invoking an ActivityResultLauncher
of type Void
, which is actually just:
operator fun ActivityResultLauncher<Void?>.invoke() = launch(null)
Thanks to Ian Lake for letting me know about this one. The TakePictureHandler
class allows our Activities to be more composable, and we can test this functionality fairly easily as a small, single-responsibility class, abstracted away from our view:
@RunWith(RobolectricTestRunner::class)
class TakePictureHandlerTest {
@Test
fun `returns bitmap when called`() {
val lifeCycleOwner = TestLifecycleOwner()
val expectedResult = mock<Bitmap>()
val testRegistry = object : ActivityResultRegistry() {
override fun <Void, Bitmap> invoke(
requestCode: Int,
contract: ActivityResultContract<Void, Bitmap>,
input: Void
) {
// Here we dispatch the result we want directly
dispatchResult(
requestCode,
Activity.RESULT_OK,
Intent().apply { putExtra("data", expectedResult) }
)
}
}
val handler = TakePictureHandler(testRegistry) { bitmap ->
bitmap shouldEqual expectedResult
}
// Don't forget to add the observer!
lifeCycleOwner.lifecycle.addObserver(handler)
lifeCycleOwner.onCreate()
handler.takePicture()
}
}
class TestLifecycleOwner : LifecycleOwner {
private val lifecycle = LifecycleRegistry(this)
fun onCreate() {
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
}
override fun getLifecycle() = lifecycle
}
One slight downside of this is you have to remember to add the handler to the lifecycle observer in your Activity
or Fragment
. We can improve this a little so that it manages this task itself:
interface TakePicture {
fun takePicture()
}
class TakePictureHandler(
private val activity: ComponentActivity,
private val func: (Bitmap) -> Unit
) : TakePicture by TakePictureImpl(
activity.activityResultRegistry,
activity,
func
)
@VisibleForTesting(otherwise = PRIVATE)
internal class TakePictureImpl(
private val registry: ActivityResultRegistry,
lifecycleOwner: LifecycleOwner,
private val func: (Bitmap) -> Unit
) : LifecycleObserver, TakePicture {
private lateinit var resultLauncher: ActivityResultLauncher<Void>
init {
lifecycleOwner.lifecycle.addObserver(this)
}
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun onCreate() {
resultLauncher = registry.registerActivityResultCallback(
"key",
ActivityResultContracts.TakePicture()
) { bitmap -> func(bitmap) }
}
override fun takePicture() {
resultLauncher()
}
}
Here, we pass both an ActivityResultRegistry
and a LifecycleOwner
to the underlying implementation - this is because otherwise it wouldn’t be possible to test the implementation; we need to be able to pass our own result registry with a dispatching callback AND we need to be able to trigger the onCreate
lifecycle callback. By keeping the TakePictureImpl
internal
, we can test this class thoroughly but expose a simpler API for the caller where we only pass the ComponentActivity
. There’s a bit of Kotlin delegation magic here too, and if you’re unsure what this does you can check out one of my previous articles for some more info.
Back to the Activity Result API itself: there’s plenty of situations where this would be useful. We have a class which abstracts away Google’s in-app update API and emits a sealed class of results through a Kotlin Flow
type. However the API requires implementing onActivityResult
, and we haven’t been able to get around that, so we’ve had to pass the Intent
returned through the callback manually to the class. Now we don’t have to do that, the class can be entirely self-contained. It’s a pretty complex example so I haven’t posted it here, but I might put it up separately once I’ve refactored it to use the new registry.
I’m also looking forward to seeing what abstractions people come up with for requesting permissions with this new API.
Gotchas
After some discussion, there does appear to be a small gotcha whilst using this API. Google are pretty explicit in their docs that you must re-register your callback in onCreate
:
To clarify, these callbacks are not persisted on a configuration change - but any results returned are in-fact queued, so that when you re-register you’ll instantly receive the awaiting result. Basically to avoid weird behaviour, always make sure these callbacks are registered in onCreate
; don’t do anything weird like registering them ad-hoc in click listeners. Thanks to Gabor Varadi and Vasiliy Zukanov for spotting this and a good discussion on the underlying issue on Twitter. There’s also a great point made here:
The issue isn’t the number of pending requests, it’s that the ID for a result contract is dynamic based on an AtomicInteger.getAndIncrement instead of something stable. So if you misuse the API and call prepareCall each time you want to launch a result request, then you wouldn’t get the result if the camera low-memory-kills you. You only get it if you initialize the ResultCaller once in onCreate and re-use it each time.
So be aware of this behaviour, and that this may change in future. A bug has been filed with Google and this is an Alpha release, so perhaps this post will require updating in a few weeks.
Last thoughts
This is a useful abstraction and I’m super pleased that Google have released this. One minor thing to note is that the the current documentation on the activity result page appears to be incorrect in a couple of places (how to register a contract, how to dispatch a result in a test), but hopefully that’ll get resolved shortly.
Hopefully this has given you some good ideas for some neat abstractions and I’d love to see them, so please share anything you come up with. I’ll likely update this post in a few weeks with some bits and pieces that I come up with too.
Thanks for reading!
Edit: Thanks to Ian Lake for the tips on activity-ktx
and pointing me in the direction of the documentation bug tracker.