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