优雅地封装 Activity Result API


/   今日科技快讯   /

近日,腾讯控股旗下游戏《王者荣耀》的健康系统升级。根据其发布的公告,在国家新闻出版署《关于防止未成年人沉迷网络游戏的通知》相关规定的基础上,腾讯为进一步加大未成年人的保护力度,将升级健康系统规则。未成年用户每日22时至次日8时禁玩,国家法定节假日每日限玩2小时,其他时间每日限玩1小时。

/   作者简介   /

本篇文章来自DylanCai的投稿,文章主要分享了他对Activity Result API的封装的整个过程和心得,相信会对大家有所帮助!

DylanCai的博客地址:

https://juejin.cn/user/4195392100243000

/   开始   /

之前写了两篇文章讲了 ViewBinding 的封装,这是 Jetpack 的一个组件,用于替代 findViewById、ButterKnife、KAE。不过用到了些 Kotlin 相对进阶点的用法,可能有些不太熟悉 Kotlin 的小伙伴看不太懂封装的代码。

所以这次来讲些简单一点的封装,来封装 Jetpack 的另一个组件——Activity Result API。这是官方用于替代 startActivityForResult() 和 onActivityResult() 的工具,能替代但是不够好用,有些小伙伴看了后还是选择写 startActivityForResult()。需要封装优化一下用法,但是推出大半年了个人没看到比较好用的封装。最初有很多人会用拓展函数进行封装,而在 activity-ktx:1.2.0-beta02 版本之后,调用注册方法的时机必须在 onStart() 之前,原来的拓展函数就不适用了,在这之后就没看到有人封装了。

个人对 Activity Result API 的封装思考了很久,已经尽量做到在 Kotlin 和 Java 都足够地好用,可以完美替代 startActivityForResult() 了。下面带着大家一起来封装 Activity Result API。

/   基础用法   /

首先要了解 Activity Result API 的用法。添加依赖:

dependencies {
    implementation "androidx.activity:activity-ktx:1.2.4"
}

在 ComponentActivity 或 Fragment 中调用 Activity Result API 提供的 registerForActivityResult() 方法注册结果回调(在 onStart() 之前调用)。该方法接收 ActivityResultContract 和 ActivityResultCallback 参数,返回可以启动另一个 activity 的 ActivityResultLauncher 对象。

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

ActivityResultContract 协议类定义生成结果所需的输入类型以及结果的输出类型,Activity Result API 已经提供了很多默认的协议类,方便大家实现请求权限、拍照等常见操作。

只是注册回调并不会启动另一个 activity ,还要调用 ActivityResultLauncher#launch() 方法才会启动。传入协议类定义的输入参数,当用户完成后续 activity 的操作并返回时,将执行 ActivityResultCallback 中的 onActivityResult()回调方法。

getContent.launch("image/*")

完整的使用代码:

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

override fun onCreate(savedInstanceState: Bundle?) {
  // ...
  selectButton.setOnClickListener {
    getContent.launch("image/*")
  }
}

ActivityResultContracts 提供了许多默认的协议类:

我们还可以自定义协议类,继承 ActivityResultContract,定义输入和输出类。如果不需要任何输入,可使用 Void 或 Unit 作为输入类型。需要实现两个方法,用于创建与 startActivityForResult() 配合使用的 Intent 和解析输出的结果。

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
    }
    return result?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
  }
}

自定义协议类实现后,就能调用 registerForActivityResult() 和 launch() 方法进行使用。

val pickRingtone = registerForActivityResult(PickRingtone()) { uri: Uri? ->
  // Handle the returned Uri
}

pickRingtone.launch(ringtoneType)

不想自定义协议类的话,可以使用通用的协议ActivityResultContracts.StartActivityForResult(),实现类似于之前 startActivityForResult() 的功能。

val startForResult = registerForActivityResult(StartActivityForResult()) { result: ActivityResult ->
  if (result.resultCode == Activity.RESULT_OK) {
      val intent = result.intent
      // Handle the Intent
  }
}

startForResult.launch(Intent(this, InputTextActivity::class.java))

/   封装思路   /

为什么要封装?

看完上面的用法,不知道大家会不会和我初次了解的时候一样,感觉比原来复杂很多。

主要是引入的新概念比较多,原来只需要了解 startActivityForResult() 和 onActivityResult() 的用法,现在要了解一大堆类是做什么的,学习成本高了不少。

用法也有些奇怪,比如官方示例用注册方法得到一个叫 getContent 对象,这更像是函数的命名,还要用这个对象去调用 launch() 方法,代码阅读起来总感觉怪怪的。

而且有个地方个人觉得不是很好,callback 居然在 registerForActivityResult() 方法里传。个人觉得 callback 在 launch() 方法里传更符合习惯,逻辑也更加连贯,代码阅读性更好。最好改成下面的用法,启动后就接着处理结果的逻辑。

getContent.launch("image/*") { uri: Uri? ->
  // Handle the returned Uri
}

所以还是有必要对 Activity Result API 进行封装的。

怎么封装?

首先是修改 callback 传参的位置,实现思路也比较简单,重载 launch() 方法加一个 callback 参数,用个变量缓存起来。在回调的时候拿缓存的 callback 对象去执行。

private var callback: ActivityResultCallback<O>? = null

fun launch(input: I?, callback: ActivityResultCallback<O>) {
  this.callback = callback
  launcher.launch(input)
}

由于需要缓存 callback 对象,还要写一个类来持有该缓存变量。

有一个不好处理的问题是 registerForActivityResult() 需要的 onStart() 之前调用。可以通过 lifecycle 在 onCreate() 的时候自动注册,但是个人思考了好久并没有想到更优的实现方式。就是获取 lifecycleOwner 观察声明周期自动注册,也是需要在 onStart() 之前调用,那为什么不直接执行注册方法呢?所以个人改变了思路,不纠结于自动注册,而是简化注册的代码。

前面说了需要再写一个类缓存 callback 对象,使用一个类的时候有个方法基本会用到,就是构造函数。我们可以在创建对象的时候进行注册。

注册方法需要 callback 和协议类对象两个参数,callback 是从 launch() 方法得到,而协议类对象就需要传了。这样用起来个人觉得还不够友好,综合考虑后决定用继承的方式把协议类对象给“隐藏”了。

最终得到以下的基类。

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

改用了 Java 代码来实现,返回的结果可以判空也可以不判空,比如返回数组的时候一定不为空,只是数组大小为 0 。用 Kotlin 实现的话要写两个不同名的方法来应对这个情况,使用起来并不是很方便。

这是多增加一个封装的步骤来简化后续的使用,原本只是继承 ActivityResultContract 实现协议类,现在还需要再写一个启动器类继承 BaseActivityResultLauncher。比如用前面获取图片的示例,我们再封装一个 GetContentLauncher 类。

class GetContentLauncher(caller: ActivityResultCaller) :
  BaseActivityResultLauncher<String, Uri>(caller, GetContent())

只需这么简单的继承封装,后续使用就更加简洁易用了。

val getContentLauncher = GetContentLauncher(this)

override fun onCreate(savedInstanceState: Bundle?) {
  // ...
  selectButton.setOnClickListener {
    getContentLauncher.launch("image/*") { uri: Uri? ->
      // Handle the returned Uri
    }
  }
}

再封装一个 Launcher 类的好处是,能更方便地重载 launch() 方法,比如在类里增加一个方法在获取图片之前会先授权读取权限。如果改用 Kotlin 拓展函数来实现,在 Java 会更加难用。Launcher 类能对 Java 用法进行兼顾。

最后总结一下,对比原本 Activity Result API 的用法,改善了什么问题:

  • 简化冗长的注册代码,改成简单地创建一个对象;

  • 改善对象的命名,比如官方示例命名为 getContent 对象就很奇怪,这通常是函数的命名。优化后很自然地用类名来命名为 getContentLauncher,使用一个启动器对象调用 launch() 方法会更加合理;

  • 改变回调的位置,使其更加符合使用习惯,逻辑更加连贯,代码阅读性更好;

  • 输入参数和输出参数不会限制为一个对象,可以重载方法简化用法;

  • 能更方便地整合多个启动器的功能,比如获取读取权限后再跳转相册选择图片;

/   最终用法   /

由于 Activity Result API 已有很多的协议类,如果每一个协议都去封装一个启动器类会有点麻烦,所以个人已经写好一个库 ActivityResultLauncher 。

https://github.com/DylanCaiCoding/ActivityResultLauncher

方便大家使用。还新增和完善了一些功能,有以下特点:

  • 完美替代 startActivityForResult()

  • 支持 Kotlin 和 Java 用法

  • 支持请求权限

  • 支持拍照(已适配 Android 10)

  • 支持录像(已适配 Android 10)

  • 支持选择图片或视频

  • 支持裁剪图片(已适配 Android11)

  • 支持打开蓝牙

  • 支持打开定位

  • 支持使用存储访问框架 SAF

  • 支持选择联系人

个人写了个 Demo 给大家来演示有什么功能,完整的代码在 Github 里。

下面来介绍 Kotlin 的用法,Java 的用法可以查看 Wiki 文档。

https://github.com/DylanCaiCoding/ActivityResultLauncherhttps://github.com/DylanCaiCoding/ActivityResultLauncher/wiki/%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95-(Java)

在根目录的 build.gradle 添加:

allprojects {
    repositories {
        // ...
        maven { url 'https://www.jitpack.io' }
    }
}

添加依赖:

dependencies {
    implementation 'com.github.DylanCaiCoding:ActivityResultLauncher:1.0.1'
}

用法也只有简单的两步:

第一步,在 ComponentActivity 或 Fragment 创建对应的对象,需要注意创建对象的时机要在 onStart() 之前。例如创建通用的启动器:

private val startActivityLauncher = StartActivityLauncher(this)

提供以下默认的启动器类:

第二步,调用启动器对象的 launch() 方法。比如跳转一个输入文字的页面,点击保存按钮回调结果。我们用 StartActivityLauncher 替换掉原来 startActivityForResult() 的写法。

val intent = Intent(this, InputTextActivity::class.java)
intent.putExtra(KEY_NAME, "nickname")
startActivityLauncher.launch(intent) { activityResult ->
  if (activityResult.resultCode == RESULT_OK) {
    activityResult.data?.getStringExtra(KEY_VALUE)?.let { toast(it) }
  }
}

为了方便使用,有些启动器会增加一些更易用的 launch() 方法。比如这个例子能改成下面更简洁的写法。

startActivityLauncher.launch<InputTextActivity>(KEY_NAME to "nickname") { resultCode, data ->
  if (resultCode == RESULT_OK) {
    data?.getStringExtra(KEY_VALUE)?.let { toast(it) }
  }
}

由于输入文字页面可能有多个地方需要跳转复用,我们可以用前面的封装思路,自定义实现一个 InputTextLauncher 类,进一步简化调用的代码,只关心输入值和输出值,不用再处理跳转和解析过程。

inputTextLauncher.launch("nickname") { value ->
  if (value != null) {
    toast(value)
  }
}

通常要对返回值进行判断,因为可能会有取消操作,要判断是不是被取消了。比如返回的 Boolean 要为 true,返回的 Uri 不为 null,返回的数组不为空数组等。

还有一些常用的功能,比如调用系统相机拍照,保存到外置存储的应用缓存目录。

takePictureLauncher.launch { uri ->
  if (uri != null) {
    // 拍照成功,上传或取消等操作后建议把缓存文件删除
  }
}

或者拍照保存到系统相册,已适配 Android 10。

takePictureLauncher.launchForMediaImage { uri ->
  if (uri != null) {
    // 拍照成功
  }
}

调用系统相册选择图片,增加了申请读取权限的操作。

pickContentLauncher.launchForImage(
  onActivityResult = { uri ->
    if (uri != null) {
      // 处理 uri
    }
  },
  onPermissionDenied = {
    // 拒绝了读取权限且不再询问,可引导用户到设置里授权该权限
  },
  onExplainRequestPermission = {
    // 拒绝了一次读取权限,可弹框解释为什么要获取该权限
  }
)

能更方便地使用存储访问框架 SAF,在 Android 10 或以上访问共享储存空间的文档文件会用到。

个人也新增了些功能,比如裁剪头像,通常上传头像要裁剪成 1:1 比例,已适配 Android 11。

cropPictureLauncher.launch(inputUri) { uri ->
  if (uri != null) {
    // 裁剪成功
  }
}

还有开启蓝牙功能,能更容易地开启蓝牙并且确保蓝牙功能是可用的(需要授权定位权限和确保定位已打开)。

enableBluetoothLauncher.launchAndEnableLocation(
  "为保证蓝牙正常使用,请开启定位",  // 已授权权限但未开启定位,会跳转对应设置页面,并吐司该字符串
  onLocationEnabled= { enabled ->
    if (enabled) {
      // 已开启了蓝牙,并且授权了位置权限和打开了定位
    }
  },
  onPermissionDenied = {
    // 拒绝了位置权限且不再询问,可引导用户到设置里授权该权限
  },
  onExplainRequestPermission = {
    // 拒绝了一次位置权限,可弹框解释为什么要获取该权限
  }
)

更多的用法请查看 Wiki 文档 。

原本 Activity Result API 已经有很多默认的协议类,都封装了对应的启动器类。大家可能不会用到所有类,不过开了混淆会自动移除没有使用的类。

后续还会支持用 Kotlin Flow 回调数据,能更方便地写流式编程的代码。用法上我还在斟酌,有兴趣的可以关注一下。如果有其它使用场景或者别的想法可以在 Github 提 issue,我会继续完善的。

/   彩蛋   /

个人之前封装过一个 startActivityForResult() 拓展函数,可以直接在后面写回调逻辑。

startActivityForResult(intent, requestCode) { resultCode, data ->
  // Handle result
}

下面是实现的代码,使用一个 Fragment 来分发 onActivityResult 的结果。代码量不多,逻辑应该比较清晰,感兴趣的可以了解一下,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 {
      val fragmentManager = activity.supportFragmentManager
      var fragment = fragmentManager.findFragmentByTag(TAG) as DispatchResultFragment?
      if (fragment == null) {
        fragment = DispatchResultFragment()
        fragmentManager.beginTransaction().add(fragment, TAG).commitAllowingStateLoss()
        fragmentManager.executePendingTransactions()
      }
      return fragment
    }
  }
}

如果觉得 Activity Result API 比较复杂,也可以拷贝这个去用。不过 requestCode 处理得不够好,而且很多功能需要自己额外去实现,用起来可能没那么方便。

/   总结   /

本文讲了 Activity Result API 的基础用法,虽然能替代 startActivityForResult() 和 onActivityResult(),但没有做到足够地好用,有些人还宁愿继续使用 startActivityForResult()。之后分享了个人的封装思路,介绍了个人封装的库 ActivityResultLauncher,使 Activity Result API 更加简洁易用,能完美地替代 startActivityForResult()。

推荐阅读:

我的新书,《第一行代码 第3版》已出版!

使用OpenGL挑战抖音蓝线特效

PermissionX 1.5发布,支持申请Android特殊权限啦

欢迎关注我的公众号

学习技术或投稿

长按上图,识别图中二维码即可关注

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: ThinkPHP 是一个开源的 PHP 开发框架,它可以帮助你快速开发 PHP 应用程序。写 API 的话,你可以利用 ThinkPHP 提供的控制器和路由功能,在控制器中编写代码,返回 JSON 数据作为 API 的响应。 具体实现方法如下: 1. 定义路由,确定 API 的 URL 地址。 2. 创建控制器,在控制器中实现 API 的业务逻辑。 3. 在控制器中使用 `$this->response()` 方法返回 JSON 数据。 示例代码: ``` // 定义路由 Route::get('api/test', 'index/test'); // 创建控制器 class Test extends Controller { public function index() { $data = [ 'code' => 0, 'message' => 'success', 'data' => [], ]; return $this->response($data, 'json'); } } ``` 这样,当请求 URL 为 `api/test` 时,就可以返回 JSON 格式的数据作为 API 的响应了。 ### 回答2: thinkphp 是一款流行的PHP开发框架,可用于构建高效、可扩展的Web应用程序。ResultAPI 是一个基于 thinkphp 的模块,用于处理应用程序的响应结果。 在 thinkphp 中,我们可以使用 ResultAPI 来处理控制器方法的返回结果。它可以将数据转换成 JSON、XML 或其他格式,并添加适当的响应头部。这样,我们就可以方便地将数据发送给前端或其他应用程序。 在编写 ResultAPI 时,我们可以定义统一的数据格式和错误码。例如,可以定义成功状态码为 200,错误状态码为 400,并在不同情况下返回相应的状态码和信息。这样,前端或其他应用程序就能根据状态码来判断请求是否成功或失败,并相应地进行处理。 另外,ResultAPI 还可以支持数据分页、异常处理和权限验证等功能。我们可以在 ResultAPI封装这些通用的处理逻辑,以提高代码的复用性和开发效率。 最后,通过在控制器方法中使用 ResultAPI,我们可以轻松地返回处理后的结果。例如,我们可以使用 `$this->result()` 方法将数据转换成 JSON 格式,并添加适当的响应头部。这样,我们就可以快速地实现数据的处理和返回。 总而言之,ResultAPI 是基于 thinkphp 的一个模块,用于处理应用程序的响应结果。它提供了方便的方法来处理数据格式转换、错误处理和权限验证等功能,使我们能够更高效地开发和管理 Web 应用程序。 ### 回答3: ThinkPHP 是一款基于 PHP 的轻量级开发框架,它提供了一种快速开发和简化编码的方式。在 ThinkPHP 中,我们可以使用 ResultAPI 进行结果的返回。 ResultAPI 是指返回给客户端的结果,可以是成功或失败的信息、数据等。在 ThinkPHP 中,可以通过以下步骤编写 ResultAPI。 首先,创建一个 ResultAPI 类,该类用于处理结果的返回。可以在应用目录的 common 文件夹下创建一个 ResultAPI.php 文件。 在 ResultAPI 类中,我们可以定义一些方法来处理不同类型的结果返回。比如,可以有一个 success 方法,用于返回成功的结果。在该方法中,可以传入一些参数,如状态码、提示信息等,并将它们封装成一个数组。 接下来,可以创建一个 error 方法,用于返回失败的结果。在该方法中,可以传入一些参数,如错误码、错误信息等,并将它们封装成一个数组。 除了这两种方法,还可以根据具体的需求创建其他方法来处理不同类型的结果返回,如重定向、异常等。 完成 ResultAPI 类的编写后,可以在控制器中调用该类来返回结果。可以根据不同的业务需求选择使用成功或失败的方法,然后将返回的结果赋值给一个变量。 最后,可以将该变量返回给客户端,方便客户端根据不同的结果进行相应的处理。 总之,使用 ThinkPHP 写 ResultAPI 需要创建一个 ResultAPI 类,并定义一些方法来处理不同类型的结果返回。然后,在控制器中调用该类,将结果返回给客户端。这样能够方便地进行结果的处理和返回。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值