preface
Two previous articles covered the encapsulation of ViewBinding, a component of Jetpack that replaces findViewById, ButterKnife, and KAE. However, I have used some relatively advanced points of Kotlin usage, and some people who are not familiar with Kotlin may not understand the encapsulated code.
So this time, let’s take a simpler look at wrapping another component of Jetpack, the Activity Result API. This is the official tool used to replace startActivityForResult() and onActivityResult(), can replace but not good enough, some friends read or choose to write startActivityForResult(). Need to optimize the use of encapsulation, but the introduction of more than half a year of personal did not see better use of encapsulation. Initially, many people would use extension functions for encapsulation, but after the activity-ktx:1.2.0-beta02 version, the registration method must be called before onStart(), the original extension function did not work, and since then no one has seen the encapsulation.
I’ve been thinking about wrapping the Activity Result API for a long time, trying to make it usable enough in Kotlin and Java to be a perfect replacement for startActivityForResult(). Let’s wrap the Activity Result API.
Basic usage
First, understand the use of the Activity Result API.
Add a dependency:
Dependencies {implementation "androidx. Activity: activity - KTX: 1."}Copy the code
ComponentActivity or fragments invokes the Activity Result of API provides registerForActivityResult () method callback registration results (in onStart () before the call). This method takes the ActivityResultContract and ActivityResultCallback parameters and returns an ActivityResultLauncher object that can launch another activity.
val getContent = registerForActivityResult(GetContent()) { uri: Uri? ->
// Handle the returned Uri
}
Copy the code
The ActivityResultContract protocol class defines the input type needed to generate results and the output type of the results. The ActivityResult API already provides a number of default protocol classes for common operations such as requesting permissions, taking pictures, and so on.
Just registering the callback does not launch another activity; it also calls the ActivityResultLauncher#launch() method. The input parameters defined by the protocol class are passed in, and the onActivityResult() callback method in ActivityResultCallback is executed when the user completes subsequent activities and returns.
getContent.launch("image/*")
Copy the code
Complete use code:
val getContent = registerForActivityResult(GetContent()) { uri: Uri? ->
// Handle the returned Uri
}
override fun onCreate(savedInstanceState: Bundle?). {
// ...
selectButton.setOnClickListener {
getContent.launch("image/*")}}Copy the code
ActivityResultContracts provides a number of default protocol classes:
Protocol class | role |
---|---|
StartActivityForResult() | General agreement |
TakePicturePreview() | Take a photo preview and return the Bitmap |
TakePicture() | Take a picture and return the Uri |
TakeVideo() | Video, return Uri |
GetContent() | Gets a single content file |
GetMultipleContents() | Get multiple content files |
RequestPermission() | Request a single permission |
RequestMultiplePermissions() | Request multiple permissions |
CreateDocument() | Create a document |
OpenDocument() | Open a single document |
OpenMultipleDocuments() | Open multiple documents |
OpenDocumentTree() | Open the document directory |
PickContact() | Selecting contacts |
We can also customize the protocol class by inheriting from ActivityResultContract and defining the input and output classes. If no input is required, use Void or Unit as the input type. You need to implement two methods to create the Intent to use with startActivityForResult() and to resolve the output result.
class PickRingtone : ActivityResultContract<Int, Uri?>() {
override fun createIntent(context: Context, ringtoneType: Int) =
Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply {
putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, ringtoneType)
}
override fun parseResult(resultCode: Int, result: Intent?). : Uri? {
if(resultCode ! = Activity.RESULT_OK) {return null
}
returnresult? .getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI) } }Copy the code
Custom protocol class implementation, you can call registerForActivityResult () and launch () method is used.
val pickRingtone = registerForActivityResult(PickRingtone()) { uri: Uri? ->
// Handle the returned Uri
}
Copy the code
pickRingtone.launch(ringtoneType)
Copy the code
Don’t want to custom protocol class, you can use general agreement ActivityResultContracts. StartActivityForResult (), before implementation is similar to StartActivityForResult () function.
val startForResult = registerForActivityResult(StartActivityForResult()) { result: ActivityResult ->
if (result.resultCode == Activity.RESULT_OK) {
val intent = result.intent
// Handle the Intent}}Copy the code
startForResult.launch(Intent(this, InputTextActivity::class.java))
Copy the code
Packaging ideas
Why encapsulate?
After reading the above usage, I do not know if you will be the same as when I first understand, the feeling is much more complicated than the original.
The main reason is the introduction of more new concepts, the original only need to understand the use of startActivityForResult() and onActivityResult(), now to understand a lot of classes do, learning costs are much higher.
For example, the official example uses the registration method to get an object called getContent, which is more like the name of a function, and uses this object to call the launch() method, which makes the code a little weird to read.
And a place for people think is not very good, the callback was registerForActivityResult transfer () method. In my opinion, callback is more customary, logical, and readable in the launch() method. It is better to use the following usage to start and then process the resulting logic.
getContent.launch("image/*") { uri: Uri? ->
// Handle the returned Uri
}
Copy the code
So it is still necessary to encapsulate the Activity Result API.
How to encapsulate?
The first step is to change the location of the callback parameter. It’s easy to do this by overloading the launch() method with a callback parameter and caching it as a variable. The cached callback object is executed during the callback.
private var callback: ActivityResultCallback<O>? = null
fun launch(input: I? , callback:ActivityResultCallback<O>) {
this.callback = callback
launcher.launch(input)
}
Copy the code
Since you need to cache the callback object, you also need to write a class to hold the cache variable.
Have a bad deal with problem is registerForActivityResult onStart () need before () call. It is possible to automate registration at onCreate() via Lifecycle but I haven’t thought of a better way to do this for a long time. To get the lifecycleOwner observing the declaration cycle automatically registered, it also needs to be called before onStart(), so why not execute the registration method directly? So individuals have changed their thinking, and instead of obsessing over automatic registration, they have simplified the code for registration.
As mentioned above, we need to write another class cache callback object. One method that we usually use when using a class is the constructor. We can register objects when they are created.
To register a method, you need both the callback, which you get from the launch() method, and the protocol object, which you pass. I think this is not friendly enough to use. After comprehensive consideration, I decided to use inheritance method to “hide” the protocol class object.
The result is the following base class.
public class BaseActivityResultLauncher<I.O> {
private final ActivityResultLauncher<I> launcher;
private ActivityResultCallback<O> callback;
public BaseActivityResultLauncher(ActivityResultCaller caller, ActivityResultContract<I, O> contract) {
launcher = caller.registerForActivityResult(contract, (result) -> {
if(callback ! =null) {
callback.onActivityResult(result);
callback = null; }}); }public void launch(@SuppressLint("UnknownNullness") I input, @NonNull ActivityResultCallback<O> callback) {
this.callback = callback; launcher.launch(input); }}Copy the code
Instead of Java code to achieve, the return result can be null or not null, such as the return array must not be empty, but the array size is 0. The Kotlin implementation has to write two methods with different names to handle this situation, which is not very convenient.
This is more than add a packaging steps to simplify the subsequent use, originally only class inheritance ActivityResultContract implementation agreement, now also need to write a starter BaseActivityResultLauncher class hierarchy.
For example, using the previous example of getting an image, we encapsulate a GetContentLauncher class.
class GetContentLauncher(caller: ActivityResultCaller) :
BaseActivityResultLauncher<String, Uri>(caller, GetContent())
Copy the code
With this simple inheritance encapsulation, subsequent use is much simpler and easier to use.
val getContentLauncher = GetContentLauncher(this)
override fun onCreate(savedInstanceState: Bundle?). {
// ...
selectButton.setOnClickListener {
getContentLauncher.launch("image/*") { uri: Uri? ->
// Handle the returned Uri}}}Copy the code
The benefit of wrapping a second Launcher class is that it makes it easier to override the launch() method, for example by adding a method to the class that grants access to pictures before getting them. Using Kotlin extension functions instead makes it harder to use in Java. The Launcher class allows for both Java usage.
In conclusion, what has been improved compared to the original use of the Activity Result API?
- Simplifying the verbose registration code to simply create an object;
- Improve the naming of objects, such as the official example name
getContent
Objects are weird. This is usually the name of a function. It’s optimized to naturally use the class namegetContentLauncher
, using an initiator objectlaunch()
The approach would be more reasonable; - Change the location of callbacks to make them more user-friendly, logical, and readable.
- Input and output parameters are not restricted to one object, and methods can be overridden to simplify usage.
- It is more convenient to integrate the functions of multiple initiators, such as obtaining read permission and then jump to the photo album to select pictures;
In the end use
Since the ActivityResult API has many protocol classes, if each protocol encapsulates a launcher class, it will be a little troublesome, so I have written a library ActivityResultLauncher for you to use. Also added and improved some functions, with the following features:
- Perfect replacement
startActivityForResult()
- Support for Kotlin and Java usage
- Support request permission
- Support the photo
- Support the video
- Supports image or video selection (Android 10)
- Supports cropping pictures (for Android11)
- Enable Bluetooth
- Enable positioning
- Support for the use of the storage access framework SAF
- You can select contacts
I have written a Demo for you to show what functions are available. The complete code is on Github.
The use of Kotlin is described below, and the use of Java can be viewed in the Wiki documentation.
In the root directory of build.gradle add:
allprojects {
repositories {
// ...
maven { url 'https://www.jitpack.io' }
}
}
Copy the code
Add a dependency:
Dependencies {implementation 'com. Making. DylanCaiCoding: ActivityResultLauncher: 1.0.0'}Copy the code
There are two simple steps to use:
The first step is to create an object in ComponentActivity or Fragment before onStart(). For example, to create a generic initiator:
private val startActivityLauncher = StartActivityLauncher(this)
Copy the code
The following default initiator classes are provided:
starter | role |
---|---|
StartActivityLauncher | Perfect replacementstartActivityForResult() |
TakePicturePreviewLauncher | Call the system camera to take a photo preview, returning only the Bitmap |
TakePictureLauncher | Call the system camera to take a picture |
TakeVideoLauncher | Call the system camera to record |
PickContentLauncher, GetContentLauncher | Select a single image or video for Android 10 |
GetMultipleContentsLauncher | Select multiple images or videos for Android 10 |
CropPictureLauncher | Crop image for Android 11 |
RequestPermissionLauncher | Request a single permission |
RequestMultiplePermissionsLauncher | Request multiple permissions |
AppDetailsSettingsLauncher | Open the App details page of system Settings |
EnableBluetoothLauncher | Open the bluetooth |
EnableLocationLauncher | Open the positioning |
CreateDocumentLauncher | Create a document |
OpenDocumentLauncher | Open a single document |
OpenMultipleDocumentsLauncher | Open multiple documents |
OpenDocumentTreeLauncher | Accessing directory contents |
PickContactLauncher | Selecting contacts |
StartIntentSenderLauncher | alternativestartIntentSender() |
Second, call the launch() method of the initiator object.
For example, jump to a page of input text, click the save button to call back the result. Let’s replace startActivityForResult().
val intent = Intent(this, InputTextActivity::class.java)
intent.putExtra(KEY_NAME, "nickname")
startActivityLauncher.launch(intent) { activityResult ->
if (activityResult.resultCode == RESULT_OK) {
data? .getStringExtra(KEY_VALUE)? .let { toast(it) } } }Copy the code
For ease of use, some initiators add a launch() method that is easier to use. For example, this example could be written in a more concise way.
startActivityLauncher.launch<InputTextActivity>(KEY_NAME to "nickname") { resultCode, data ->
if (resultCode == RESULT_OK) {
data? .getStringExtra(KEY_VALUE)? .let { toast(it) } } }Copy the code
Since the input text page may have many places to jump and reuse, we can use the previous packaging idea to customize an InputTextLauncher class to further simplify the calling code. We only care about the input value and output value, and do not need to deal with the jump and parsing process.
inputTextLauncher.launch("nickname") { value ->
if(value ! =null) {
toast(value)
}
}
Copy the code
The return value is usually judged because there may be a cancellation and you want to determine if it has been cancelled. For example, return Boolean true, return Uri not null, return array not empty, etc.
There are also some common functions, such as calling the system camera to take photos and jumping to the system album to select pictures, has adapted Android 10, you can directly get URI to load pictures and file upload operations.
takePictureLauncher.launch { uri, file ->
if(uri ! =null&& file ! =null) {
// Delete the cache file after upload or cancel, call file.delete()}}Copy the code
pickContentLauncher.launchForImage(
onActivityResult = { uri, file ->
if(uri ! =null&& file ! =null) {
// Delete the cache file after upload or cancel, call file.delete()
}
},
onPermissionDenied = {
// Deny the read permission and do not ask for it. Redirect the user to Settings to grant the permission
},
onExplainRequestPermission = {
// A read permission is denied. A popup box explains why the read permission was obtained})Copy the code
There are also some new features for individuals, such as cropping pictures, which are usually cropped to a 1:1 ratio when you upload your avatar, which is suitable for Android 11.
cropPictureLauncher.launch(inputUri) { uri, file ->
if(uri ! =null&& file ! =null) {
// Delete the cache file after upload or cancel, call file.delete()}}Copy the code
Also enable Bluetooth to make it easier to enable Bluetooth and make sure that bluetooth is available (you need to grant location permission and make sure location is enabled).
enableBluetoothLauncher.launchAndEnableLocation(
"To ensure normal use of Bluetooth, please enable location.".// If the location is not enabled but the permission has been granted, the corresponding setting page will be redirected and the string will be toasted
onLocationEnabled= { enabled ->
if (enabled) {
// Bluetooth is enabled, location permissions are granted and location is enabled
}
},
onPermissionDenied = {
// Deny the location permission and do not ask for it. Direct the user to the Settings to grant the permission
},
onExplainRequestPermission = {
// If a location permission is denied, a popup box explains why the permission should be obtained})Copy the code
See the Wiki documentation for more usage.
The Activity Result API already has a number of default protocol classes that encapsulate the corresponding initiator class. You probably won’t be using all the classes, and the obfuscations will automatically remove the unused classes.
eggs
I have previously encapsulated a startActivityForResult() extension function that can be directly followed by the callback logic.
startActivityForResult(intent, requestCode) { resultCode, data ->
// Handle result
}
Copy the code
Here is the code to implement it, using a Fragment to distribute the results of onActivityResult. The amount of code is small, and the logic should be clear. If you are interested, you can learn about the implementation principle of the Activity Result API.
inline fun FragmentActivity.startActivityForResult(
intent: Intent,
requestCode: Int.noinline callback: (resultCode: Int.data: Intent?). ->Unit
) =
DispatchResultFragment.getInstance(this).startActivityForResult(intent, requestCode, callback)
class DispatchResultFragment : Fragment() {
private val callbacks = SparseArray<(resultCode: Int.data: Intent?) -> Unit> ()override fun onCreate(savedInstanceState: Bundle?). {
super.onCreate(savedInstanceState)
retainInstance = true
}
fun startActivityForResult(
intent: Intent,
requestCode: Int,
callback: (resultCode: Int.data: Intent?). ->Unit
) {
callbacks.put(requestCode, callback)
startActivityForResult(intent, requestCode)
}
override fun onActivityResult(requestCode: Int, resultCode: Int.data: Intent?). {
super.onActivityResult(requestCode, resultCode, data)
val callback = callbacks.get(requestCode)
if(callback ! =null) {
callback.invoke(resultCode, data)
callbacks.remove(requestCode)
}
}
companion object {
private const val TAG = "dispatch_result"
fun getInstance(activity: FragmentActivity): DispatchResultFragment =
activity.run {
val fragmentManager = supportFragmentManager
var fragment = fragmentManager.findFragmentByTag(TAG) as DispatchResultFragment?
if (fragment == null) {
fragment = DispatchResultFragment()
fragmentManager.beginTransaction().add(fragment, TAG).commitAllowingStateLoss()
fragmentManager.executePendingTransactions()
}
fragment
}
}
}
Copy the code
If you find the Activity Result API more complex, you can also copy this. But the requestCode doesn’t handle it well enough, and many of the features need to be implemented on their own, which may not be as convenient to use.
We will cover packaging in the next article
- To encapsulate and use ViewBinding gracefully, it’s time to replace Kotlin Synthetic and ButterKnife.
- ViewBinding is a smart way to wrap things around BRVAH.
conclusion
This article covers the basic use of the ActivityResult API. While it can replace startActivityForResult() and onActivityResult(), it doesn’t work well enough. Some people also prefer to stick with startActivityForResult(). After that, I shared my personal packaging ideas and introduced my library ActivityResultLauncher, which makes the ActivityResult API more concise and easy to use, and can perfectly replace startActivityForResult().
If you find it helpful, I hope you can click a star to support yo ~ I will share more articles related to encapsulation to you.