Introducing the Activity Result APIs

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:

A good point

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.

comments powered by Disqus