[Jetpack] ActivityResult introduction and principle analysis

​​insert image description here

foreword

This article first introduces the basic use of ActivityResult, and finally discusses the principles behind it through the source code.

In Android, if we want to transfer data between activities in both directions, we need to use startActivityForResult to start, and then process the return in onActivityResult, and apply for permissions in similar steps.

But this way of handling will make our code very complicated, and it cannot guarantee the type safety of the parameters when the Activity sends or receives data.

ActivityResult is a function provided by Jetpack, which can simplify the direct data transfer of Activity (including permission application). It simplifies handling data from an Activity by providing a type-safe contract. These contracts define the expected input and output types for some common operations (such as taking a picture or requesting permission), and you can also customize the contracts to meet the needs of different scenarios.

The ActivityResult API provides some components for registering the processing result of the Activity, initiating a request, and processing it immediately after the system returns the result. You can also use an independent class to receive the return result at the place where the Activity is started, which can still ensure type safety.

ActivityResult

To use ActivityResult, you need to add dependencies first:
insert image description here

Then take a look at the simplest way to use it, such as opening the system file manager to select a picture, the code is as follows:

val getContent = registerForActivityResult(GetContent()) {
    
     uri: Uri? ->
    // 处理返回的 Uri
}

getContent.launch("image/*") //过滤图片

Several important classes and functions are involved here:

  • registerForActivityResult: It is a function of ComponentActivity ( note that ComponentActivity here is androidx.activity.ComponentActivity instead of androidx.core.app.ComponentActivity, and the corresponding class in androidx.core does not support this function yet. ) The definition of this function is as follows:
public final <I, O> ActivityResultLauncher<I> registerForActivityResult(
            @NonNull ActivityResultContract<I, O> contract,
            @NonNull ActivityResultCallback<O> callback)

You can see that this function receives two parameters, which are ActivityResultContract and callback ActivityResultCallback. ActivityResultContract is the parameters required for encapsulation and startup (comprising Intent, which will be described in detail later). The function returns ActivityResultLauncher, you can see that the activity can be started through its launch function later.

  • GetContent: ActivityResultContracts.GetContent class is a concrete implementation class that inherits ActivityResultContract and encapsulates the function of calling the system file manager. Jetpack provides some commonly used ActivityResultContract, such as selecting pictures, taking pictures, etc. If we need to pull up our own Activity, we need to customize an ActivityResultContract.

  • launch: The function of ActivityResultLauncher, which starts the activity and replaces the previous startActivity.

ActivityResultContract

Let's take a look at how GetContent is implemented. The code is as follows:

public static class GetContent extends ActivityResultContract<String, Uri> {
    
    

        @CallSuper
        @NonNull
        @Override
        public Intent createIntent(@NonNull Context context, @NonNull String input) {
    
    
            return new Intent(Intent.ACTION_GET_CONTENT)
                    .addCategory(Intent.CATEGORY_OPENABLE)
                    .setType(input);
        }

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

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

You can see that there are two key interfaces implemented:

  • createIntent is used to encapsulate the incoming parameters into an intent, which is used to start the activity, and the function of GetContent is to encapsulate an intent to open a system file;

  • parseResult is to parse the returned intent and organize it into the format we need to return. In GetContent, we only need to return the file uri.

The callback ActivityResultCallback we mentioned above, its parameter is the return value of parseResult.

So if we communicate between our own pages, we can customize ActivityResultContract. Similar to GetContent, we can implement these two functions according to our own needs. Of course, we can also directly use StartActivityForResult (see below) provided by jetpack.

ActivityResultContracts subclass

The encapsulated ActivityResultContract provided by Jetpack has (both are subclasses of ActivityResultContracts):

  1. StartActivityForResult

as follows:

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

The simplest one is equivalent to the traditional startActivityForResult, except that several parameters of onActivityResult are encapsulated into an ActivityResult, as follows:

public ActivityResult(int resultCode, @Nullable Intent data)

  1. StartIntentSenderForResult

Equivalent to Activity.startIntentSender(IntentSender, Intent, int, int, int), used in conjunction with PendingIntent

  1. RequestMultiplePermissions

Used to apply for permissions in batches, as follows:

public static final class RequestMultiplePermissions extends ActivityResultContract<String[], java.util.Map<String, Boolean>>

Returns the status of each permission in the form of a Map.

  1. RequestPermission

Apply for a single permission, as follows:

public static final class RequestPermission extends ActivityResultContract<String, Boolean>

Applying for permissions through these two can facilitate subsequent processing.

  1. TakePicturePreview

Pull up the photo preview, as follows:

public static class TakePicturePreview extends ActivityResultContract<Void, Bitmap>

Return the bitmap data directly. (Same as the traditional way, this bitmap is just a picture preview, because too large data cannot be transmitted in the intent)

Note that although the input is Void, a null needs to be passed in to execute the lanch function of the ActivityResultLauncher.

  1. TakePicture

Pull up to take a picture, as follows:

public static class TakePicture extends ActivityResultContract<Uri, Boolean>

Enter the location uri where the image is to be saved

  1. TakeVideo

Record a video as follows:

public static class TakeVideo extends ActivityResultContract<Uri, Bitmap>

Enter the location uri where the video is to be saved and return the thumbnail of the video.

  1. PickContact

Select a contact, as follows:

public static final class PickContact extends ActivityResultContract<Void, Uri>

  1. GetContent

Get a single file, as follows:

public static class GetContent extends ActivityResultContract<String, Uri>

Enter the filter type and return the file uri

  1. GetMultipleContents

Multiple selection of files, as follows:

public static class GetMultipleContents extends ActivityResultContract<String, List>

ditto

  1. opendocument

Open a single document (the system document manager is pulled up), as follows:

@TargetApi(19)
public static class OpenDocument extends ActivityResultContract<String[], Uri>

Corresponding to Intent.ACTION_OPEN_DOCUMENT, the input is type filtering (such as image/*), and the output uri

  1. OpenMultipleDocuments

Open multiple documents, similar to above

  1. OpenDocumentTree

Open the document tree, corresponding to Intent.ACTION_OPEN_DOCUMENT_TREE

  1. CreateDocument

Create a new document corresponding to Intent.ACTION_CREATE_DOCUMENT

It can be seen that Android has encapsulated common functions, which can basically meet our development and use.

principle

So what is the principle of ActivityResult, and why can it be implemented in this way?

launch should be well understood, that is, the intent obtained through the createIntent of ActivityResultContract can be started.

So how to realize the callback of result?

First look at the source code of registerForActivityResult:

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

Finally call the register function of ActivityResultRegistry (mActivityResultRegistry):

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

        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.");
        }

        final int requestCode = registerKey(key);
        LifecycleContainer lifecycleContainer = mKeyToLifecycleContainers.get(key);
        if (lifecycleContainer == null) {
    
    
            lifecycleContainer = new LifecycleContainer(lifecycle);
        }
        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.addObserver(observer);
        mKeyToLifecycleContainers.put(key, lifecycleContainer);

        return new ActivityResultLauncher<I>() {
    
    
            ...
        };
    }

First of all, you can see that the call of this function has an opportunity limit, and it needs to be before the start life cycle of the Activity (including start), otherwise an exception will be thrown.

As you can see below, it is realized through the lifecycle function. Add an Observer to the started context (such as activity), and it is found in the Observer that the return is processed in the onStart event. But in fact, the return is in the onActivityResult function. Here you need to pay attention to mPendingResults. It is given data in the doDispatch function in ActivityResultRegistry, and doDispatch is called by the dispatchResult function. So dispatchResult is executed there?

    @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) {
    
    
            ActivityResultCallback<O> callback = callbackAndContract.mCallback;
            ActivityResultContract<?, O> contract = callbackAndContract.mContract;
            callback.onActivityResult(contract.parseResult(resultCode, data));
        } else {
    
    
            // Remove any parsed pending result
            mParsedPendingResults.remove(key);
            // And add these pending results in their place
            mPendingResults.putParcelable(key, new ActivityResult(resultCode, data));
        }
    }

The answer is that in ComponentActivity, ComponentActivity holds an ActivityResultRegistry object, which is the mActivityResultRegistry mentioned above. The dispatchResult function is called in both onActivityResult and onRequestPermissionsResult of ComponentActivity.

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

    @CallSuper
    @Override
    @Deprecated
    public void onRequestPermissionsResult(
            int requestCode,
            @NonNull String[] permissions,
            @NonNull int[] grantResults) {
    
    
        if (!mActivityResultRegistry.dispatchResult(requestCode, Activity.RESULT_OK, new Intent()
                .putExtra(EXTRA_PERMISSIONS, permissions)
                .putExtra(EXTRA_PERMISSION_GRANT_RESULTS, grantResults))) {
    
    
            if (Build.VERSION.SDK_INT >= 23) {
    
    
                super.onRequestPermissionsResult(requestCode, permissions, grantResults);
            }
        }
    }

In this way, the callback of the result (including the application permission) is realized.

Summarize

From the above introduction, we can see that ActivityResult is actually an encapsulation of the previous startActivityForResult mode, which simplifies the use and increases security. But we need to register the callback in advance and generate the ActivityResultLauncher object, and this step requires the ComponentActivity object, and there are time constraints, so it is not particularly flexible (especially in the permission management area).

Recommended reading: "Android 12 New Features: SplashScreen Splash Screen"

Guess you like

Origin blog.csdn.net/chzphoenix/article/details/130109475