Jetpack: use ActivityResult to handle data communication between activities

Posted by fractalvibes on Tue, 04 Jan 2022 17:51:42 +0100

catalogue

Foreword

ActivityResult usage

ActivityResultContract

principle

summary

Foreword

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

In Android, if we want to transfer data between activities in two directions, we need to use startActivityForResult to start, and then process the return in onActivityResult. In addition, applying for permission is a similar step.

However, this processing method will make our code very complex, and can not guarantee the type safety of 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 the processing of data from activities by providing type safe contract s. These protocols define the expected input and output types for some common operations (such as taking photos or requesting permission). In addition, you can customize the protocols to meet the needs of different scenarios.

The ActivityResult API provides some components for registering the processing results of an Activity, initiating requests, and processing the results immediately after the system returns them. You can also use a separate class to receive the returned results where the Activity is started, which still ensures type safety.

ActivityResult usage

Use ActivityResult to add dependencies first:

dependencies {
  // In https://developer.android.google.cn/jetpack/androidx/releases/activity Get the latest version number
  def activity_version = "1.2.0"
  // In https://developer.android.google.cn/jetpack/androidx/releases/fragment Get the latest version number
  def fragment_version = "1.3.0"
  
  implementation "androidx.activity:activity:$activity_version"
  implementation "androidx.fragment:fragment:$fragment_version"
}

Then take a look at the simplest way to use it. For example, open the system file manager and select an image. The code is as follows:

val getContent = registerForActivityResult(GetContent()) { uri: Uri? ->
    // Processing returned Uri
}

getContent.launch("image/*") //Filter pictures

Here are several important classes and functions:

(1) registerForActivityResult: it is a function of componentactivity. Note that the componentactivity here is Android X activity. Componentactivity instead of Android core. app. ComponentActivity´╝îandroidx. The corresponding class in core (as of 1.3.0) does not support this function.

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: ActivityResultContract and callback ActivityResultCallback. ActivityResultContract encapsulates the parameters required for startup (consisting of Intent, which will be described in detail later). Function returns ActivityResultLauncher. You can see that you can start the activity through its launch function.

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

(3) launch: the function of ActivityResultLauncher, which starts the activity instead of the previous startActivity.

ActivityResultContract

Let's see 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 to implement:

  • createIntent is used to encapsulate the passed parameters into intent and start the activity. The function of GetContent encapsulates an intent that opens the system file;

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

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

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

The encapsulated ActivityResultContracts provided in Jetpack include (all subclasses of ActivityResultContracts):

(1)StartActivityForResult

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

The simplest method is equivalent to the traditional method of startActivityForResult, but encapsulates several parameters of onActivityResult into an ActivityResult

public ActivityResult(int resultCode, @Nullable Intent data)

(2)StartIntentSenderForResult

Equivalent to activity Startintentsender (intentsender, intent, int, int, int), used with PendingIntent

(3)RequestMultiplePermissions

Used to apply for permission in batch

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

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

(4)RequestPermission

Request a single permission

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

Applying for permission through these two can facilitate subsequent processing.

(5)TakePicturePreview

Pull up photo Preview

public static class TakePicturePreview extends ActivityResultContract<Void, Bitmap>

Returns bitmap data directly. (like the traditional method, this bitmap is just a picture preview, because too large data cannot be transmitted in intent.)

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

(6)TakePicture

Pull up and take pictures

public static class TakePicture extends ActivityResultContract<Uri, Boolean>

Enter the uri where the picture will be saved

(7)TakeVideo

record video

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.

(8)PickContact

Select contacts

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

(9)GetContent

Get a single file

public static class GetContent extends ActivityResultContract<String, Uri>

Enter the filter type and return the file uri

(10)GetMultipleContents

File multiple selection

public static class GetMultipleContents extends ActivityResultContract<String, List<Uri>>

ditto

(11)OpenDocument

Open a single document (the system document manager is pulled up)

@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

(12)OpenMultipleDocuments

Open multiple documents, similar to the above

(13)OpenDocumentTree

Open the document tree, corresponding to intent ACTION_ OPEN_ DOCUMENT_ TREE

(14)CreateDocument

Create a new document corresponding to intent ACTION_ CREATE_ DOCUMENT

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

principle

What is the principle of ActivityResult and why can it be implemented in this way?

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

So how to implement 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 (m ActivityResultRegistry):

@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>() {
            @Override
            public void launch(I input, @Nullable ActivityOptionsCompat options) {
                onLaunch(requestCode, contract, input, options);
            }

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

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

First of all, you can see that the call of this function is time limited. 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 function of lifecycle. An Observer is added to the started context (such as activity). In the Observer, it is found that the return is processed in the onStart event. However, in fact, the return is in the onActivityResult function. Here, we need to pay attention to mPendingResults. Data is given to it in the doDispatch function in the ActivityResultRegistry, and doDispatch is called by the dispatchResult function. So where did you execute the dispatchResult?

    @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, an object of ActivityResultRegistry is held in ComponentActivity, that is, the above-mentioned m ActivityResultRegistry. dispatchResult function will be called in both onActivityResult and onRequestPermissionsResult of ComponentActivity. In this way, the callback of results (including application permission) is realized.

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

summary

Topics: Android jetpack