This is the 8th day of my participation in the November Gwen Challenge. Check out the event details: The last Gwen Challenge 2021

AndroidX adds Result API to Activity:1.2.0-alpha02 and Fragment:1.3.0-alpha02 Use ActivityResultContract instead of startActivityForResult for more efficient and typesafe handling of cross-activity communication.

First, how to use it

In AppCompatActivity or fragments through registerForActivityResult () in creating ActivityResultLauncher, then calls the launch (…). Start the target Activity instead of startActivityForResult.

//MainActivity.kt class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) fab.setOnClickListener { view -> val intent = Intent(this, MainActivity::class.java) launcher.launch(intent)\ } } // Create ActivityResultLauncher private val launcher : ActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult ->  // activityResult will return this as ActivityResult Log.d("MainActivity", activityResult.toString()) // D/MainActivity: ActivityResult{resultCode=RESULT_OK, data=Intent { (has extras) }} } }Copy the code
//SecondActivity.kt
class SecondActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setResult(Activity.RESULT_OK, Intent().putExtra("my-data", "data"))
        finish()
    }
}
Copy the code

You don’t need to implement onActivityResult as before, and use the resultCode requestCode to judge.

Second, source code analysis

The Result API, which replaces startActivityResult as a callback, is centered on the ActivityResultContract protocol class. StartActivityForResul is one of several ActivityResultContract implementations preset in ActivityResultContracts.

StartActivityForResult

//ActivityResultContracts.java 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) { return new ActivityResult(resultCode, intent); }}Copy the code

The ActivityResultContract accepts two generics representing the type of launch parameter for ActivityResultLauncher and the type of result returned by onActivityResult. For StartActivityForResult, it is started with the Intent and waits for the target Activity to return ActivityResult.

Custom ActivityResultContract

Of course, you can customize ActivityResultContract to accept any type of startup parameter. Then construct the Intent in createIntent, for example:

//MyActivityContract.kt class MyActivityContract : ActivityResultContract<Int, String? >() { override fun createIntent(context: Context, input: Int): Intent { return Intent(context, PostActivity::class.java).apply { putExtra("PostActivity.ID", input) } } override fun parseResult(resultCode: Int, intent: Intent?) : String? { val data = intent? .getStringExtra(PostActivity.TITLE) return if (resultCode == Activity.RESULT_OK && data ! = null) data else null } } // MainActivity.kt launcher.launch(100089) //launch with Int private val launcher = registerForActivityResult(MyctivityContract()) { result -> // Result will return this as string? \ if (result ! = null) toast("Result : $result") else toast("No Result") }Copy the code

ctivityResultRegistry

RegisterForActivityResult internal call ActivityResultRegistry the register () method

//ComponentActivity.java

   @NonNull
    @Override
    public final <I, O> ActivityResultLauncher<I> registerForActivityResult(
            @NonNull final ActivityResultContract<I, O> contract,
            @NonNull final ActivityResultRegistry registry,
            @NonNull final ActivityResultCallback<O> callback) {
        return registry.register(
                "activity_rq#" + mNextLocalRequestCode.getAndIncrement(), this, contract, callback);
    }

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

    @NonNull
    @Override
    public final ActivityResultRegistry getActivityResultRegistry() {
        return mActivityResultRegistry;
    }
Copy the code

The ActivityResultContract is stored internally in a HashMap by Register (). You can also use custom ActivityResultRegistry instead of using mActivityResultRegistry:

val launcher: ActivityResultLauncher<Intent> = activityResultRegistry
        .register(ActivityResultContracts.StartActivityForResult(),
                "activity_rq#0",
                this, //LifecyleOwner: ComponentActivity
        ) { activityResult: ActivityResult ->
            Log.d("MainActivity", activityResult.toString())
            //  D/MainActivity: ActivityResult{resultCode=RESULT_OK, data=Intent { (has extras) }}
        }
Copy the code

The Register accepts a LifecycleOwner (ComponentActivity itself) and stores/removes callbacks to the Map at the appropriate lifecycle, ensuring that the timing of the callback response is correct.

LifecycleEventObserver observer = new LifecycleEventObserver() { @Override public void onStateChanged( @NonNull LifecycleOwner lifecycleOwner, @NonNull Lifecycle.Event event) { if (Lifecycle.Event.ON_START.equals(event)) { mKeyToCallback.put(key, new CallbackAndContract<>(callback, contract)); . } else if (Lifecycle.Event.ON_STOP.equals(event)) { mKeyToCallback.remove(key); } else if (Lifecycle.Event.ON_DESTROY.equals(event)) { unregister(key); }}};Copy the code

MponentActivity related implementation

ComponentActivity is simpler, hold ActivityResultRegistry and provide registerForActivityResult method. ActivityResultRegistry stores callbacks through a HashMap, whose key is incremented.

Do YOU still need a requestCode

OnActivityResult, which used to require requestCode to identify which startActivityForResult return, can now be managed by AutoIncrement. OnSaveInstanceState will automatically save the key pair of the requestCode and ActivityResultRegistry when the process is killed. When onActivityResult returns a requestCode, You can find the key through the correspondence, and then find the ActivityResultCallback.

//ActivityResultRegistry.java private int registerKey(String key) { Integer existing = mKeyToRc.get(key); if (existing ! = null) { return existing; } int rc = mNextRc.getAndIncrement(); bindRcKey(rc, key); return rc; }Copy the code

Fragment-dependent implementation

Internal fragments. RegisterForActivityResult (), when onAttach invokes getActivity (). GetActivityResultRegistry () to register

//Fragment.java public final <I, O> ActivityResultLauncher<I> registerForActivityResult( @NonNull final ActivityResultContract<I, O> contract, @NonNull final ActivityResultCallback<O> callback) { return prepareCallInternal(contract, new Function<Void, ActivityResultRegistry>() { @Override public ActivityResultRegistry apply(Void input) { if (mHost instanceof ActivityResultRegistryOwner) { return ((ActivityResultRegistryOwner) mHost).getActivityResultRegistry(); } return requireActivity().getActivityResultRegistry(); } }, callback); } @NonNull private <I, O> ActivityResultLauncher<I> prepareCallInternal( @NonNull final ActivityResultContract<I, O> contract, @NonNull final Function<Void, ActivityResultRegistry> registryProvider, @NonNull final ActivityResultCallback<O> callback) { ... registerOnPreAttachListener(new OnPreAttachedListener() { @Override void onPreAttached() { final String key = generateActivityResultKey(); ActivityResultRegistry registry = registryProvider.apply(null); ref.set(registry.register(key, Fragment.this, contract, callback)); }}); return new ActivityResultLauncher<I>() { @Override public void launch(I input) { ... }}; } private void registerOnPreAttachListener(@NonNull final OnPreAttachedListener callback) { //If we are already attached, we can register immediately if (mState >= ATTACHED) { callback.onPreAttached(); } else { // else we need to wait until we are attached mOnPreAttachedListeners.add(callback); }}Copy the code

The register will inject framgent instances into the HashMap held by the parent, but the LifecycleOwner used at this time is the Fragment itself. So you don’t have to worry about memory leaks.

More preset ActivityResultContract

In addition to StartActivityFroResult, there are a number of anticipated ActivityResultContracts:

For example, open the file manager to select the image with GetContent and return the URI:

class MainActivity : AppCompatActivity() {

    private val launcher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
        Log.d("MainActivity", "uri: $uri")
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        button_get_content.setOnClickListener {
            launcher.launch("image/*")
        }
    }
}
Copy the code

Through the RequestPermission and RequestMultiplePermission permissions application will also be easier:

request_permission.setOnClickListener {
    requestPermission.launch(permission.BLUETOOTH)
}

request_multiple_permission.setOnClickListener {
    requestMultiplePermissions.launch(
        arrayOf(
            permission.BLUETOOTH,
            permission.NFC,
            permission.ACCESS_FINE_LOCATION
        )
    )
}

// Request permission contract
private val requestPermission =
    registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
        // Do something if permission granted
        if (isGranted) toast("Permission is granted")
        else toast("Permission is denied")
    }

// Request multiple permissions contract
private val requestMultiplePermissions =
    registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions : Map<String, Boolean> ->
        // Do something if some permissions granted or denied
        permissions.entries.forEach {
            // Do checking here
        }                                                                             
}
Copy the code

conclusion

Finally, we review the process of using the Result API through the following figure

The benefits of the Result API are clear: The ActivityResultContract eliminates awareness of requestCode and onActivityResult and reduces template code. More importantly, a variety of preset ActivityResultContract greatly reduces the communication cost of system apis and facilitates daily development. It’s time to say goodbye to startActivityForResult!