preface

For a long time, we used the startActivityForResult and onActivityResult methods to start an Activity and get its return value. However, since Activity 1.2.0-Alpha02 and Fragment 1.3.0-alpha02, these two functions that have existed since learning Android have been marked by Google as obsolete and replaced by the new Activity Result API. Why did Google redesign this time-honored API? This paper tries to draw a conclusion by comparing these two writing methods.

Traditional onActivityResult notation

This is something any Android developer will be familiar with all too well.

class MainActivity : AppCompatActivity(R.layout.activity_main) {
    companion object {
        const val REQUEST_CODE = 909
    }

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

        fab.setOnClickListener {
            val intent = Intent(this, SecondActivity::class.java)
            intent.putExtra(EXTRA_DATA, "data")
            startActivityForResult(intent, REQUEST_CODE)
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int.data: Intent?). {
        super.onActivityResult(requestCode, resultCode, data)
        if (resultCode == Activity.RESULT_OK && requestCode == REQUEST_CODE) {
            val text = data? .getStringExtra(EXTRA_DATA_BACK) ? :""
            Log.e("MainActivity"."data back is ==>$text")}}}Copy the code

To summarize this writing method, it is mainly:

  1. definerequestCodeconstant
  2. callstartActivityForResultTurn on another oneActivity
  3. inonActivityResultAccording to therequestCodeandresultCodeTo retrieve the returned data and process it

There seems to be nothing wrong with writing boilerplate code, and we’ve been used to it for a long time.

Activity Result API

First we need to introduce dependencies in gradle files:

    // activity
    implementation("Androidx. Activity: activity - KTX: 1.2.0")
    // fragment
    implementation("Androidx. Fragments: fragments - KTX: 1.3.0")
Copy the code

Then you can write the corresponding code:

 private val getContent =
        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
            valtext = it? .data? .getStringExtra(EXTRA_DATA_BACK) ? :""
            Log.e("MainActivity"."query data by activity result API, and data is ==>$text")}override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
      
        fab.setOnLongClickListener {
            getContent.launch(Intent(this, SecondActivity::class.java))
            true}}Copy the code

That seems a lot simpler, but it’s not clear what it means. Let’s break it down one by one.

First is registerForActivityResult this function, it takes two parameters, one is ActivityResultContract, one is ActivityResultCallback, An ActivityResultLauncher object is also returned.

  @NonNull
  @Override
  public final <I, O> ActivityResultLauncher<I> registerForActivityResult(
            @NonNull ActivityResultContract<I, O> contract,
            @NonNull ActivityResultCallback<O> callback) {
        return registerForActivityResult(contract, mActivityResultRegistry, callback);
  }
Copy the code

ActivityResultContract

It is an abstract class, which we can think of as a convention that abstracts the ability of createIntent and parseResult to provide the Intent needed to launch an Activity and parse the data returned, respectively.

The StartActivityForResult we passed in is a subclass of that.

public static final class StartActivityForResult
            extends ActivityResultContract<Intent.ActivityResult> {
        @NonNull
        @Override
        public Intent createIntent(@NonNull Context context, @NonNull Intent input) {
            return input;
        }

        @NonNull
        @Override
        public ActivityResult parseResult(
                int resultCode, @Nullable Intent intent) {
          // ActivityResult encapsulates the returned resultCode and data
            return newActivityResult(resultCode, intent); }}Copy the code

StartActivityForResult is the inner class of ActivityResultContracts, which, as we can see from its naming (in the plural), contains more than one implementation of the protocol (ActivityResultContract), It also provides default implementations such as permission requests, taking photos, videos, opening files, and so on.

ActivityResultCallback

The generic parameter O corresponds to the protocol output type that parseResult returns. For example, the parameter StartActivityForResult returns ActivityResult.

public interface ActivityResultCallback<O> {
    /** * Called when result is available */
    void onActivityResult(@SuppressLint("UnknownNullness") O result);
}
Copy the code

registerForActivityResult

With the basis of the above two functions, we registerForActivityResult again, it will eventually call ActivityResultRegistry register function in the class. In this function, the corresponding requestCode is generated and cached, and the corresponding key is added or removed according to the lifecycle of the current controller, ensuring safety of the lifecycle.

 @NonNull
    public final <I, O> ActivityResultLauncher<I> register(
            @NonNull final String key,
            @NonNull final LifecycleOwner lifecycleOwner,
            @NonNull final ActivityResultContract<I, O> contract,
            @NonNull final ActivityResultCallback<O> callback) {

        Lifecycle lifecycle = lifecycleOwner.getLifecycle();
      / /... Omit code
        final int requestCode = registerKey(key);
        mKeyToCallback.put(key, new CallbackAndContract<>(callback, contract));

        final ActivityResult pendingResult = mPendingResults.getParcelable(key);
        LifecycleContainer lifecycleContainer = mKeyToLifecycleContainers.get(key);
        if (lifecycleContainer == null) {
            lifecycleContainer = new LifecycleContainer(lifecycle);
        }
        if(pendingResult ! =null) {
            mPendingResults.remove(key);
            LifecycleEventObserver observer = new LifecycleEventObserver() {
                @Override
                public void onStateChanged(
                        @NonNull LifecycleOwner lifecycleOwner,
                        @NonNull Lifecycle.Event event) {
                    if(Lifecycle.Event.ON_START.equals(event)) { callback.onActivityResult(contract.parseResult( pendingResult.getResultCode(), pendingResult.getData())); }}}; lifecycleContainer.addObserver(observer); mKeyToLifecycleContainers.put(key, lifecycleContainer); } LifecycleEventObserver observer =new LifecycleEventObserver() {
            @Override
            public void onStateChanged(@NonNull LifecycleOwner lifecycleOwner,
                    @NonNull Lifecycle.Event event) {
                if(Lifecycle.Event.ON_DESTROY.equals(event)) { unregister(key); }}}; lifecycleContainer.addObserver(observer);return new ActivityResultLauncher<I>() {
            @Override
            public void launch(I input, @Nullable ActivityOptionsCompat options) {
                onLaunch(requestCode, contract, input, options);
            }

            @Override
            public void unregister(a) {
                ActivityResultRegistry.this.unregister(key);
            }

            @NonNull
            @Override
            publicActivityResultContract<I, ? > getContract() {returncontract; }}; }Copy the code

ActivityResultLauncher

It is an abstract class. The core abstract method is the launch function, which is responsible for executing various contracts. When we use it externally, we call its launch function directly.

The list of declared operations for the previous protocol and return value handling is actually registered in the ActivityResultRegistry class, which is also an abstract class with an important abstract method, onLaunch, that contains all the previously declared information.

   /**
     * Start the process of executing an {@link ActivityResultContract} in a type-safe way,
     * using the provided {@link ActivityResultContract contract}.
     *
     * @param requestCode request code to use
     * @param contract contract to use for type conversions
     * @param input input required to execute an ActivityResultContract.
     * @param options Additional options for how the Activity should be started.
     */
    @MainThread
    public abstract <I, O> void onLaunch(
            int requestCode,
            @NonNull ActivityResultContract<I, O> contract,
            @SuppressLint("UnknownNullness") I input,
            @Nullable ActivityOptionsCompat options);
Copy the code

When registered, ActivityResultRegistry returns an anonymous implementation of ActivityResultLauncher each time, calling the onLaunch function in its launch function. At the same time, the Activity’s parent ComponentActivity class holds the implementation of ActivityResultRegistry, so the final action is returned to the Activity.

  private ActivityResultRegistry mActivityResultRegistry = new ActivityResultRegistry() {
        @Override
        public <I, O> void onLaunch(
                final int requestCode,
                @NonNull ActivityResultContract<I, O> contract,
                I input,
                @Nullable ActivityOptionsCompat options) {
            ComponentActivity activity = ComponentActivity.this;
            / /... Omit code

            // Start activity path
            Intent intent = contract.createIntent(activity, input);
            Bundle optionsBundle = null;
            if(intent.hasExtra(EXTRA_ACTIVITY_OPTIONS_BUNDLE)) { optionsBundle = intent.getBundleExtra(EXTRA_ACTIVITY_OPTIONS_BUNDLE);  intent.removeExtra(EXTRA_ACTIVITY_OPTIONS_BUNDLE); }else if(options ! =null) {
                optionsBundle = options.toBundle();
            }
            if (ACTION_REQUEST_PERMISSIONS.equals(intent.getAction())) {
              / /... Omit code
            } else if (ACTION_INTENT_SENDER_REQUEST.equals(intent.getAction())) {
              / /... Omit code
            } else {
                // startActivityForResult pathActivityCompat.startActivityForResult(activity, intent, requestCode, optionsBundle); }}};Copy the code

As you can see, the previous createIntent is called and executed differently depending on the protocol. It is worth noting that startActivityForResult is marked deprecated at the top, but it is actually called at the bottom.

scalability

As mentioned above, the ActivityResultContract class contains all the officially provided default implementations of ActivityResultContract, and developers can implement their own protocols as required by passing in the corresponding generic parameters and implementation methods. Take this official example:

 class PickRingtone : ActivityResultContract<Int, Uri?>() {
        override fun createIntent(context: Context, ringtoneType: Int) =
            Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply {
                putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, ringtoneType)
            }

        override fun parseResult(resultCode: Int, result: Intent?). : Uri? {
            if(resultCode ! = Activity.RESULT_OK) {return null
            }
            returnresult? .getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI) } }Copy the code

conclusion

Through the above analysis, the calling process of the Activity Result API is sorted out and at least three advantages can be found:

  1. Reduced boilerplate code. The developer does not have to define the requestCode and override the onActivityResult function every time startActivityForResult is used.

  2. Decoupled from the view controller. When onActivityResult is used, it must be bound to an Activity or fragment. After using the ActivityResult API, you can separate the logic for processing the return value. So how do we do that?

    As you can see from the previous section, the core of the ActivityResult API is ActivityResultRegistry. Holding it in ComponentActivity and exposing it for external use gives developers a lot of room for imagination. For example, you can register the ActivityResultRegistry object directly from the outside, separating the code from the Activity or Fragment to reduce the coupling with the view controller.

     class MyLifecycleObserver(private val registry : ActivityResultRegistry)
                : DefaultLifecycleObserver {
            lateinit var getContent : ActivityResultLauncher<String>
    
            fun onCreate(owner: LifecycleOwner) {
                getContent = registry.register("key", owner, GetContent()) { uri ->
                    // Handle the returned Uri}}fun selectImage(a) {
                getContent("image/*")}}Copy the code
  3. Life cycle security. Because Lifecycle is bound and unbound using ActivityResultRegistry at the time of registration, there will not be a situation where the Activity will be destroyed when the result is returned.

Overall, using the new Activity Result API makes the code more flexible, reduces some of the developer’s rework, and is easy to use and worth getting started with.

At the same time, we can learn the design ideas of Google bosses, how to use design patterns reasonably, decouple problems and abstract higher-level modules. The previous API uses intents to add different flags to jump to the page, take photos, etc. Although it is simple, it is too scattered, and it is not easy to see what it does at first glance. After using the new API, the corresponding protocol is used (although the underlying implementation is the same), but the meaning is obvious. Only the protocol for ActivityResultContracts needs to be maintained, which conforms to the design principles of high cohesion and low coupling and is worth learning from.

Finally, Enjoy!

Note: The above source analysis is based on Activity 1.2.0-beta01.

reference

Google doc

It’s time to drop onActivityResult