Using Android Activity Result API

In Android development, it is often necessary to jump to a new page to obtain the result and return the data. The two methods that have been used are the startActivityForResult and onActivityResult methods, but the startActivityForResult method has been deprecation. The official recommendation is to use the Activity Result API.

Jump to a new page and return data startActivityForResult

The operation steps are generally three steps:
1. Define REQUEST_CODE, avoid repetition when there are multiple data on the same page;
2. Call startActivityForResult(Intent, REQUEST_CODE) to jump to a new page;
3. Rewrite onActivityResult() to judge requestCode and resultCode, get the returned data and execute the follow-up logic.

Example:

class MainActivity : AppCompatActivity() {
    companion object {
        private const val REQUEST_CODE_1 = 20
        private const val REQUEST_CODE_2 = 21
    }

    private val binding: ActivityMainBinding by lazy {
        ActivityMainBinding.inflate(layoutInflater)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        supportActionBar?.hide()
        setContentView(binding.root)
        binding.click1.setOnClickListener {
            startActivityForResult(Intent(this, SecondActivity::class.java), REQUEST_CODE_1)
        }
        binding.click2.setOnClickListener {
            //ARouter里仍然是使用startActivityForResult
            ARouter.getInstance()
                .build(ARouterPath.SecondActivity)
                .navigation(this, REQUEST_CODE_2)
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (resultCode == RESULT_OK) {
            when (requestCode) {
                REQUEST_CODE_1 -> {
                    Toast.makeText(
                        this,
                        "startActivityForResult回调:${data?.getStringExtra("data")}",
                        Toast.LENGTH_SHORT
                    )
                        .show()
                }
                REQUEST_CODE_2 -> {
                    Toast.makeText(
                        this,
                        "ARouter回调:${data?.getStringExtra("data")}",
                        Toast.LENGTH_SHORT
                    ).show()
                }
            }
        }
    }
}

Even the commonly used method of using ARouter to manage page routing, jumping through navigation is stuffed with REQUEST_CODE, and it uses the startActivityForResult method to jump inside.

private void startActivity(int requestCode, Context currentContext, Intent intent, Postcard postcard, NavigationCallback callback) {
        if (requestCode >= 0) {  // Need start for result
            if (currentContext instanceof Activity) {
                ActivityCompat.startActivityForResult((Activity) currentContext, intent, requestCode, postcard.getOptionsBundle());
            } else {
                logger.warning(Consts.TAG, "Must use [navigation(activity, ...)] to support [startActivityForResult]");
            }
        } else {
            ActivityCompat.startActivity(currentContext, intent, postcard.getOptionsBundle());
        }

        if ((-1 != postcard.getEnterAnim() && -1 != postcard.getExitAnim()) && currentContext instanceof Activity) {    // Old version.
            ((Activity) currentContext).overridePendingTransition(postcard.getEnterAnim(), postcard.getExitAnim());
        }

        if (null != callback) { // Navigation over.
            callback.onArrival(postcard);
        }
    }

Through a simple setResult(int resultCode, Intent data) on the new page, when closing the new page and returning to the original page, the onActivityResult method of the original page can get the returned data.

Activity Result API for jumping to a new page and returning data

Use the Activity Result API to jump to a new page and return data. The operation is divided into two steps:
1. Define a function to process the returned data through the registerForActivityResult method;
2. Use launch() to jump to a new page.

Example:

//使用registerForActivityResult
        val secondLauncher =
            registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
                if (it.resultCode == RESULT_OK) {
                    it.data?.getStringExtra("data")?.let {
                        Toast.makeText(
                            this,
                            "registerForActivityResult回调:${it}",
                            Toast.LENGTH_SHORT
                        )
                            .show()
                    }
                }
            }
        binding.click3.setOnClickListener {
            secondLauncher.launch(Intent(this, SecondActivity::class.java))
        }

The new page still returns data through setResult(int resultCode, Intent data). You can see that the rewriting of onActivityResult() has been removed from the original page, and one less REQUEST_CODE has been written.

Explore the principle of Activity Result API

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

Activity Results API consists of three elements, Launcher, Contract, Callback

ActivityResultLauncher

public abstract class ActivityResultLauncher<I> {

    /**
     * Executes an {@link ActivityResultContract}.
     *
     * <p>This method throws {@link android.content.ActivityNotFoundException}
     * if there was no Activity found to run the given Intent.

     * @param input the input required to execute an {@link ActivityResultContract}.
     *
     * @throws android.content.ActivityNotFoundException
     */
    public void launch(@SuppressLint("UnknownNullness") I input) {
        launch(input, null);
    }

    /**
     * Executes an {@link ActivityResultContract}.
     *
     * <p>This method throws {@link android.content.ActivityNotFoundException}
     * if there was no Activity found to run the given Intent.
     *
     * @param input the input required to execute an {@link ActivityResultContract}.
     * @param options Additional options for how the Activity should be started.
     *
     * @throws android.content.ActivityNotFoundException
     */
    public abstract void launch(@SuppressLint("UnknownNullness") I input,
            @Nullable ActivityOptionsCompat options);

    /**
     * Unregisters this launcher, releasing the underlying result callback, and any references
     * captured within it.
     *
     * You should call this if the registry may live longer than the callback registered for this
     * launcher.
     */
    @MainThread
    public abstract void unregister();

    /**
     * Get the {@link ActivityResultContract} that was used to create this launcher.
     *
     * @return the contract that was used to create this launcher
     */
    @NonNull
    public abstract ActivityResultContract<I, ?> getContract();
}

ActivityResultLauncher is the return value of registerForActivityResult, which is used to connect the startup object and the return object.

ActivityResultContract

ActivityResultContract is the first input parameter of registerForActivityResult, which specifies an input type and a result return type.

public abstract class ActivityResultContract<I, O> {

    /** Create an intent that can be used for {@link Activity#startActivityForResult} */
    public abstract @NonNull Intent createIntent(@NonNull Context context,
            @SuppressLint("UnknownNullness") I input);

    /** Convert result obtained from {@link Activity#onActivityResult} to O */
    @SuppressLint("UnknownNullness")
    public abstract O parseResult(int resultCode, @Nullable Intent intent);

    /**
     * An optional method you can implement that can be used to potentially provide a result in
     * lieu of starting an activity.
     *
     * @return the result wrapped in a {@link SynchronousResult} or {@code null} if the call
     * should proceed to start an activity.
     */
    public @Nullable SynchronousResult<O> getSynchronousResult(
            @NonNull Context context,
            @SuppressLint("UnknownNullness") I input) {
        return null;
    }

    /**
     * The wrapper for a result provided in {@link #getSynchronousResult}
     *
     * @param <T> type of the result
     */
    public static final class SynchronousResult<T> {
        private final @SuppressLint("UnknownNullness") T mValue;

        /**
         * Create a new result wrapper
         *
         * @param value the result value
         */
        public SynchronousResult(@SuppressLint("UnknownNullness") T value) {
            this.mValue = value;
        }

        /**
         * @return the result value
         */
        public @SuppressLint("UnknownNullness") T getValue() {
            return mValue;
        }
    }
}

There are mainly two methods in the ActivityResultContract, the createIntent() method creates an Intent for startActivityForResult, and the parseResult() method converts the result of onActivityResult.

The commonly used ActivityResultContract is provided in ActivityResultContracts, which can be used directly.
image.png

For example, our most commonly used jump new page returns data: ActivityResultContracts.StartActivityForResult()

    public static final class StartActivityForResult
            extends ActivityResultContract<Intent, ActivityResult> {

        /**
         * Key for the extra containing a {@link android.os.Bundle} generated from
         * {@link androidx.core.app.ActivityOptionsCompat#toBundle()} or
         * {@link android.app.ActivityOptions#toBundle()}.
         *
         * This will override any {@link ActivityOptionsCompat} passed to
         * {@link androidx.activity.result.ActivityResultLauncher#launch(Object,
         ActivityOptionsCompat)}
         */
        public static final String EXTRA_ACTIVITY_OPTIONS_BUNDLE = "androidx.activity.result"
                + ".contract.extra.ACTIVITY_OPTIONS_BUNDLE";

        @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);
        }
    }

Inherit ActivityResultContract, agree that the input type is Intent, and the result return type is ActivityResult. In the createIntent method, because the input type is Intent, no processing is done, and it returns directly. In the parseResult method, an ActivityResult instance is created and returned according to the specified resultCode and intent.

Look at another ActivityResultContracts.TakePicturePreview()

    public static class TakePicturePreview extends ActivityResultContract<Void, Bitmap> {

        @CallSuper
        @NonNull
        @Override
        public Intent createIntent(@NonNull Context context, @Nullable Void input) {
            return new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        }

        @Nullable
        @Override
        public final SynchronousResult<Bitmap> getSynchronousResult(@NonNull Context context,
                @Nullable Void input) {
            return null;
        }

        @Nullable
        @Override
        public final Bitmap parseResult(int resultCode, @Nullable Intent intent) {
            if (intent == null || resultCode != Activity.RESULT_OK) return null;
            return intent.getParcelableExtra("data");
        }
    }

The input type is Void, because an Intent instance of MediaStore.ACTION_IMAGE_CAPTURE is created in createIntent. ParseResult is returned according to the Bitmap instance obtained in the specified intent.
If the commonly used ones in ActivityResultContracts cannot meet the requirements, it is natural to customize a wave and implement the corresponding createIntent method and parseResult method.

ActivityResultCallback

As the name suggests, it is the result callback.

public interface ActivityResultCallback<O> {

    /**
     * Called when result is available
     */
    void onActivityResult(@SuppressLint("UnknownNullness") O result);
}

Implementation of registerForActivityResult in Activity

RegisterForActivityResult() can be used directly in Activity and Fragment, because both ComponentActivity and Fragment implement the ActivityResultCaller interface.

    @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);
    }

The first parameter uses "activity_rq#" + mNextLocalRequestCode.getAndIncrement() to construct a key. mNextLocalRequestCode is an AtomicInteger value. In this way, there is no need to define REQUEST_CODE to distinguish.

    @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 lifecycle = lifecycleOwner.getLifecycle();
        //register要在当前生命周期组件处于STARTED状态之前调用
        if (lifecycle.getCurrentState().isAtLeast(Lifecycle.State.STARTED)) {
            throw new IllegalStateException("LifecycleOwner " + lifecycleOwner + " is "
                    + "attempting to register while current state is "
                    + lifecycle.getCurrentState() + ". LifecycleOwners must call register before "
                    + "they are STARTED.");
        }
        //通过传入的key生成requestCode
        final int requestCode = registerKey(key);
        //通过key在集合中获取LifecycleContainer实例,没有则生成一个
        LifecycleContainer lifecycleContainer = mKeyToLifecycleContainers.get(key);
        if (lifecycleContainer == null) {
            lifecycleContainer = new LifecycleContainer(lifecycle);
        }
        //生成观察者,当状态为ON_START时执行回调,为ON_STOP时移除与回调的关联,为ON_DESTROY时取消注册
        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));
                    if (mParsedPendingResults.containsKey(key)) {
                        @SuppressWarnings("unchecked")
                        final O parsedPendingResult = (O) mParsedPendingResults.get(key);
                        mParsedPendingResults.remove(key);
                        callback.onActivityResult(parsedPendingResult);
                    }
                    final ActivityResult pendingResult = mPendingResults.getParcelable(key);
                    if (pendingResult != null) {
                        mPendingResults.remove(key);
                        callback.onActivityResult(contract.parseResult(
                                pendingResult.getResultCode(),
                                pendingResult.getData()));
                    }
                } else if (Lifecycle.Event.ON_STOP.equals(event)) {
                    mKeyToCallback.remove(key);
                } else if (Lifecycle.Event.ON_DESTROY.equals(event)) {
                    unregister(key);
                }
            }
        };
        //为LifecycleContainer实例添加观察者
        lifecycleContainer.addObserver(observer);
        mKeyToLifecycleContainers.put(key, lifecycleContainer);
        //返回了一个ActivityResultLauncher实例
        return new ActivityResultLauncher<I>() {
            @Override
            public void launch(I input, @Nullable ActivityOptionsCompat options) {
                mLaunchedKeys.add(key);
                Integer innerCode = mKeyToRc.get(key);
                onLaunch((innerCode != null) ? innerCode : requestCode, contract, input, options);
            }

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

            @NonNull
            @Override
            public ActivityResultContract<I, ?> getContract() {
                return contract;
            }
        };
    }

In the register method, first get the lifecycle of the current lifecycle component. Then register is called before the current lifecycle component is in the STARTED state. Generate requestCode from the passed key. Get the LifecycleContainer instance in the collection through the key, if there is no one, generate one. Generate an observer, execute the callback when the state is ON_START, remove the association with the callback when the state is ON_STOP, and cancel the registration when it is ON_DESTROY. Add observers to LifecycleContainer instances. Finally, an instance of ActivityResultLauncher is returned.

Implementation of onLaunch in Activity

The ActivityResultLauncher instance is finally returned in registerForActivityResult, and the ActivityResultRegistry.onLaunch method is called in the launch method of ActivityResultLauncher. This method is an abstract method, which is actually implemented in ComponentActivity.

        this.mActivityResultRegistry = new ActivityResultRegistry() {
            public <I, O> void onLaunch(final int requestCode, @NonNull ActivityResultContract<I, O> contract, I input, @Nullable ActivityOptionsCompat options) {
                ComponentActivity activity = ComponentActivity.this;
                final SynchronousResult<O> synchronousResult = contract.getSynchronousResult(activity, input);
                if (synchronousResult != null) {
                    //不需要启动Activity就能知道结果的场景处理
                    (new Handler(Looper.getMainLooper())).post(new Runnable() {
                        public void run() {
                            dispatchResult(requestCode, synchronousResult.getValue());
                        }
                    });
                } else {
                    //需要启动Activity才能知道结果的场景处理
                    //通过ActivityResultContract.createIntent初始化Intent实例
                    Intent intent = contract.createIntent(activity, input);
                    //初始化Bundle
                    Bundle optionsBundle = null;
                    if (intent.getExtras() != null && intent.getExtras().getClassLoader() == null) {
                        intent.setExtrasClassLoader(activity.getClassLoader());
                    }

                    if (intent.hasExtra("androidx.activity.result.contract.extra.ACTIVITY_OPTIONS_BUNDLE")) {
                        optionsBundle = intent.getBundleExtra("androidx.activity.result.contract.extra.ACTIVITY_OPTIONS_BUNDLE");
                        intent.removeExtra("androidx.activity.result.contract.extra.ACTIVITY_OPTIONS_BUNDLE");
                    } else if (options != null) {
                        optionsBundle = options.toBundle();
                    }
                    //如果是权限申请,请求权限
                    if ("androidx.activity.result.contract.action.REQUEST_PERMISSIONS".equals(intent.getAction())) {
                        String[] permissions = intent.getStringArrayExtra("androidx.activity.result.contract.extra.PERMISSIONS");
                        if (permissions == null) {
                            permissions = new String[0];
                        }

                        ActivityCompat.requestPermissions(activity, permissions, requestCode);
                    } else if ("androidx.activity.result.contract.action.INTENT_SENDER_REQUEST".equals(intent.getAction())) {
                        IntentSenderRequest request = (IntentSenderRequest)intent.getParcelableExtra("androidx.activity.result.contract.extra.INTENT_SENDER_REQUEST");

                        try {
                            ActivityCompat.startIntentSenderForResult(activity, request.getIntentSender(), requestCode, request.getFillInIntent(), request.getFlagsMask(), request.getFlagsValues(), 0, optionsBundle);
                        } catch (final SendIntentException var11) {
                            (new Handler(Looper.getMainLooper())).post(new Runnable() {
                                public void run() {
                                    dispatchResult(requestCode, 0, (new Intent()).setAction("androidx.activity.result.contract.action.INTENT_SENDER_REQUEST").putExtra("androidx.activity.result.contract.extra.SEND_INTENT_EXCEPTION", var11));
                                }
                            });
                        }
                    } else {
                        ActivityCompat.startActivityForResult(activity, intent, requestCode, optionsBundle);
                    }
                }
            }
        };

First of all, distinguish whether to start the Activity. If the Activity needs to be started, initialize the Intent instance through ActivityResultContract.createIntent, initialize the Bundle, and finally jump to a new page through ActivityCompat.startActivityForResult.

Summarize

  • ComponentActivity internally initializes an instance of ActivityResultRegistry and rewrites onLaunch().
  • Call registerForActivityResult() and finally call ActivityResultRegistry.register(), and add an observer here. When the life cycle state switches to ON_START, execute Contract.parseResult() to generate output content, and pass the result as a parameter to callback callback.onActivityResult ()middle.
  • Call ActivityResultLauncher.launch() to initiate a jump, in which the onLaunch() method is called back, and Contract.createIntent() is called here to create an Intent instance for use with startActivityForResult().
  • After returning to this page after jumping to the target Activity, the life cycle changes, and the related code of the callback will be executed in the observer.

postscript

When a page needs to open multiple pages at once through the same ActivityResultLauncher, it is found that the performance is different on different Android versions.
Each registerForActivityResult will generate a RequestCode as a key inside, ActivityResultLauncher has an observer queue, ON_START will add observers, ON_STOP will remove observers.
When onActivityResult is called back, the dispatchResult method is executed, the observer is retrieved from the observer queue for return, and the doDispatch method is executed.
The key is in the doDispatch method, there are observers to perform the onActivityResult callback of the observers, and there are no observers, and the key is used to store the data in the bundle information.

    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        if (!mActivityResultRegistry.dispatchResult(requestCode, resultCode, data)) {
            super.onActivityResult(requestCode, resultCode, data);
        }
    }

    @MainThread
    public final boolean dispatchResult(int requestCode, int resultCode, @Nullable Intent data) {
        String key = mRcToKey.get(requestCode);
        if (key == null) {
            return false;
        }
        doDispatch(key, resultCode, data, mKeyToCallback.get(key));
        return true;
    }

    private <O> void doDispatch(String key, int resultCode, @Nullable Intent data,
            @Nullable CallbackAndContract<O> callbackAndContract) {
        if (callbackAndContract != null && callbackAndContract.mCallback != null
                && mLaunchedKeys.contains(key)) {
            ActivityResultCallback<O> callback = callbackAndContract.mCallback;
            ActivityResultContract<?, O> contract = callbackAndContract.mContract;
            callback.onActivityResult(contract.parseResult(resultCode, data));
            mLaunchedKeys.remove(key);
        } else {
            // Remove any parsed pending result
            mParsedPendingResults.remove(key);
            // And add these pending results in their place
            mPendingResults.putParcelable(key, new ActivityResult(resultCode, data));
        }
    }

On the Android13 system, when returning to the page, ON_START will add observers first, and then onActivityResult callback, no problem.
On the Android 10 system, when returning to the page, the onActivityResult callback is first. Since the observer has not been added back to the queue, the key is used to store the bundle information. Therefore, when the same registerForActivityResult is used multiple times, the data will be lost, and the bundle information stored with the key will be overwritten. , leaving only the bundle information returned last time.

Guess you like

Origin blog.csdn.net/yuantian_shenhai/article/details/125922639