Kotlin的特性应用示例,原来还可以这么玩
今日科技快讯
昨日,腾讯发布2018年全年财报:第四季度腾讯实现营收848.96亿元,同比增长28%,2018年全年实现总收入为3126.94亿元(455.61亿美元 ),同比增长32%;净利润774.69亿元(112.88亿美元),同比增长19%。
作者简介
又到了令人激动的周五了,提前祝大家周末愉快!
本篇文章来自 Tears丶残阳 的投稿,和大家分享了自己的Kotlin实践项目,希望对大家有所帮助!
Tears丶残阳博客地址:
https://blog.csdn.net/xiazunyang
正文
相信每一个做Android的程序猿都没少重写Activity的onActivityResult和onPermissionResult方法,随之而来的各种框架也是层出不穷。其中,我觉得这种使用Fragment来实现的方法是最好的。所以我思路也是这种,但是特点就是充分使用了Kotlin的各种特性,使逻辑清晰、层次分明,代码也更简洁。
在开始之前,最好先阅读这篇博客:
https://blog.csdn.net/gdutxiaoxu/article/details/86498647
以了解实现基本思路。
思路简介:
利用在Fragment中调用startActivityForResult方法后,会回调到同Fragment中onActivityResult方法、以及调用onRequestPermissionsResult方法后,会回调到同Fragment中onRequestPermissionsResult方法的特性,在调用startActivityForResult和requestPermission方法时,传入回调后保存起来,等onActivityResult和onRequestPermissionsResult方法调用时,通过requestCode取出回调并调用。
以上思路已经有基于Java的具体实现了,上面的博客中有源码,直接抄走就能用。
那么相比原生的写法,我们少干了什么呢
1.不用定义requestCode。
2.不用重写onActivityResult和onRequestPermissionsResult方法。
那么相比以上方法,使用Kotlin后还能有什么新的玩法吗?我先整理一下:
1.使用Kotlin的SAM特性,传入Lambda作为回调函数,写法简单。
2.Builder设计模式,链式调用设置其它回调,层次清晰。
3.使用Kotlin的扩展函数特性,直接在Activity或Fragment中调用。
4.使用Kotlin的inline特性和具名参数特性,仿Anko的写法,语法短小精悍。
接下来先用Kotlin将基本思路实现一下,共由四个主要部分组成:
一、Fragment的创建
private const val FRAGMENT_TAG = "EmptyFragment"
/**
* 查找Activity中有没有EmptyFragment,如果没有则创建EmptyFragment并添加到Activity中
* @receiver FragmentManager
* @return EmptyFragment 已创建并已添加到Activity中的Fragment
*/
private fun findOrCreateEmptyFragment(manager: FragmentManager): EmptyFragment {
return manager.findFragmentByTag(FRAGMENT_TAG) as? EmptyFragment ?: EmptyFragment().also {
manager.beginTransaction().replace(android.R.id.content, it, FRAGMENT_TAG).commitNowAllowingStateLoss()
}
}
这是一个私有的包级函数,在FragmentManager中,通过FRAGMENT_TAG来查找Fragment,如果找到了,则强转为EmptyFragment,如果没有找到,则创建一个新的,添加到FragmentManager后,返回EmptyFragment对象,此方法结束。
二、requestCode的创建
private val codeGenerator = Random()
private val resultHolder = LinkedHashMap<Int, LambdaHolder<Intent>>()
private val permissionHolder = LinkedHashMap<Int, LambdaHolder<Unit>>()
private fun <M : Map<Int, *>> codeGenerate(map: M): Int {
var requestCode: Int
do {
requestCode = codeGenerator.nextInt(0xFFFF)
} while (requestCode in map.keys)
return requestCode
}
resultHolder为startActivityForResult的回调持有类,permissionHolder为requestPermission的回调持有类,都是一个Map集合,codeGenerate方法也是一个包级函数,参数接收以上两个Map之一,然后创建出一个与已有Key不同的新编码,然后返回,此方法结束。
以上,都是Java中有的实现。
三、构建者模式对象的定义
class LambdaHolder<T>(val onSuccess: (T) -> Unit) {
private var onBefore: () -> Unit = {}
private var onDefined: (T?) -> Unit = {}
private var onCanceled: () -> Unit = {}
private var onDenied: (List<String>) -> Unit = {}
/**
* 设置在startActivityForResult后,新Activity返回时,resultCode为CANCELED时的回调
* @param callback () -> Unit
* @return LambdaHolder<T> 返回自己,用于继续设置其它回调
*/
fun setOnCanceledCallback(callback: () -> Unit): LambdaHolder<T> {
onCanceled = callback
return this
}
/**
* 设置在requestPermission后,申请的权限被拒绝时的回调
* @param callback (List<String>) -> Unit 向外提供被拒绝的权限列表
* @return LambdaHolder<T> 返回自己,用于继续设置其它回调
*/
fun setOnDeniedCallback(callback: (List<String>) -> Unit): LambdaHolder<T> {
onDenied = callback
return this
}
/**
* 设置在onActivityResult后,调用其它回调之前执行的回调
* @param callback () -> Unit
* @return LambdaHolder<T>
*/
fun setBeforeCallback(callback: () -> Unit): LambdaHolder<T> {
onBefore = callback
return this
}
/**
* 设置在startActivityForResult后,新Activity返回时,resultCode为USER_DEFINED时的回调
* @param callback (T?) -> Unit 用户自定义的操作可能会用到Intent对象
* @return LambdaHolder<T> 返回自己,用于继续设置其它回调
*/
fun setOnDefinedCallback(callback: (T?) -> Unit): LambdaHolder<T> {
onDefined = callback
return this
}
/**
* 当申请权限被拒绝时,调用此方法
* @param list List<String> 被拒绝的权限列表
* @return Unit
*/
fun onDenied(list: List<String>) = this.onDenied.invoke(list)
/**
* 当从新Activity返回时,resultCode为USER_DEFINED时调用此方法
* @return Unit
*/
fun onDefined(t: T?) = this.onDefined.invoke(t)
/**
* 当从新Activity返回时,先执行此回调
* @return Unit
*/
fun before() = this.onBefore.invoke()
/**
* 当从新Activity返回时,resultCode为CANDELED时调用此方法
* @return Unit
*/
fun onCanceled() = this.onCanceled.invoke()
}
这个类中,构造函数中接收一个完成、成功时的Lambda回调,有一个泛型参数,这个泛型主要是为了后续的扩展而存在的。另外还有4个私有的Lambda属性,它们都有一个默认的空实现,还可以分别通过setOnCanceledCallback、setOnDeniedCallback、setBeforeCallback以及setOnDefinedCallback方法来设置新的实现,并且设置后,还会将自己返回作为返回值。每个Lambda都有一个对外暴露用来调用回调的方法,它们会在适当的时机由EmptyFragment的onActivityResult和onRequestPermissionsResult方法来调用。
四、EmptyFragment的实现
internal class EmptyFragment : Fragment() {
internal fun startActivityForResult(requestCode: Int, intent: Intent, options: Bundle? = null, callback: (Intent) -> Unit): LambdaHolder<Intent> {
return LambdaHolder(callback).also {
resultHolder[requestCode] = it
startActivityForResult(intent, requestCode, options)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
//取出与requestCode对应的对象,然后执行与resultCode对应的回调
resultHolder.remove(requestCode)?.let {
it.before()
when (resultCode) {
FragmentActivity.RESULT_OK -> it.onSuccess(data ?: Intent())
FragmentActivity.RESULT_CANCELED -> it.onCanceled()
else -> it.onDefined(data)
}
}
}
internal fun requestPermissions(requestCode: Int, vararg permissions: String, onRequestDone: (Unit) -> Unit): LambdaHolder<Unit> {
return LambdaHolder(onRequestDone).also {
//如果系统版本大于Android6.0并且未授予此权限,则申请权限
if (Build.VERSION.SDK_INT > 22 && !checkPermissions(*permissions)) {
//将回调加入待调用Map存起来,然后申请权限
permissionHolder[requestCode] = it
requestPermissions(permissions, requestCode)
} else {
//否则当作申请成功处理
it.onSuccess(Unit)
}
}
}
private fun checkPermissions(vararg permission: String): Boolean {
return permission.all {
ActivityCompat.checkSelfPermission(ctx, it) == PackageManager.PERMISSION_GRANTED
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
//取出与requestCode对应的回执记录,如果为空,则结束此方法
val lambdaHolder = permissionHolder.remove(requestCode) ?: return
//当有正在申请的权限未结束时,permissions和grantResults会是空的,此时为申请失败,做中断处理
if (permissions.isEmpty() && grantResults.isEmpty()) return
//将未授予的权限加入到一个列表中
grantResults.toList().mapIndexedNotNull { index, result ->
if (result != PackageManager.PERMISSION_GRANTED) permissions[index] else null
}.let {
//通过列表是否为空来判断权限是否授予,然后执行对应的回调
if (it.isEmpty()) lambdaHolder.onSuccess(Unit) else lambdaHolder.onDenied(it)
}
}
}
此类中新定义了startActivityForResult方法和requestPermissions方法,接收的参数除了原生方法的几个参数以外,还多了一个Lambda类型的参数,此Lambda为成功时的回调。要注意的是,这两个方法都有一个LambdaHolder类型的返回值,暂时先不解释。
1.我们先来看startActivityForResult方法与onActivityResult方法,我们先是创建了一个LambdaHolder对象,将成功时的Lambda回调保存起来了,在返回LambdaHolder对象之前,我们将requestCode作为Key、LambdaHolder对象作为Value存入了resultHolder中,然后调用了Fragment中原生的startActivityForResult方法。直到启动的Activity返回参数后,onActivityResult方法会回调,此时,我们先通过requestCode取出并移除保存的LambdaHolder对象,然后通过区分resultCode来调用LambdaHolder中不同的回调方法。其中,before回调无论resultCode为何值时,都会优先其它回调先执行一次,这个回调用于忽略返回结果的情况下,执行某些动作。
2.再来看其它的几个方法,在申请权限之前,我们还是先创建了一个LambdaHolder对象,然后判断系统版本以及判断是否已经授予权限,只有当系统版本大于Android6.0并且未授予权限时,我们才继续申请权限,否则当作申请成功处理,直接调用成功时的回调Lambda即可。申请权限前,还是先将requestCode与LambdaHolder对象存入permissionHolder中保存起来,再调用原生的requestPermissions方法来申请权限。直到onRequestPermissionsResult方法回调时,我们先通过requestCode取出LambdaHolder对象,做一下判空处理,为空时我们自然无法处理接下来的事情,所以直接结束方法。再判断一下permissions与grantResults是否是空数组,因为在已经有权限在申请过程中又发起新的权限申请,就会导致这两个数组都是空的情况发生,此时我们也直接结束方法。之后我们通过mapIndexedNotNull高阶函数取出未授予的权限,通过列表是否为空来判断所有的权限是否已经授予,最后执行成功或失败的回调方法。
3.新定义的startActivityForResult方法和requestPermissions方法都有一个LambdaHolder类型的返回值,我们可以调LambdaHolder中的setOnCanceledCallback、setBeforeCallback、setOnDefinedCallback以及setOnDeniedCallback方法来继续设置其它回调。
五、使用Kotlin扩展函数封装
因为EmptyFragment只是模块只可调用(internal),其它方法都是私有的,所以还需要进一步封装,将所有步骤连起来。添加以下两个包级函数。
/**
* 启动Activity并接收Intent的扩展方法,接收回调时不需要重写[Activity#onActivityResult]方法
* @receiver F 基于[FragmentActivity]的扩展方法
* @param intent Intent [#startActivity]必需的参数
* @param options Bundle? 动画参数
* @param callback (data: Intent) -> Unit 返回此界面时,当[#resultCode]为 RESULT_OK时的回调
* @return LambdaHolder<Intent> 可以在此对象上继续调用 [LambdaHolder#onCanceled]或
* [LambdaHolder#onDefined] 方法来设置 ResultCode 为 RESULT_CANCELED 或 RESULT_FIRST_USER 时的回调
*/
fun <F : FragmentActivity> F.startActivityForResult(
intent: Intent,
options: Bundle? = null,
callback: (Intent) -> Unit = {}): LambdaHolder<Intent> {
//获取一个与已有编码不重复的编码
val requestCode = codeGenerate(resultHolder)
//获取或创建Fragment
val emptyFragment = findOrCreateEmptyFragment(supportFragmentManager)
//调用Fragment的startActivityForResult方法,并传入回调
return emptyFragment.startActivityForResult(requestCode, intent, options, callback)
}
/**
* 申请权限的扩展方法,通过lambda传入回调,不需要重写[Activity#onRequestPermissionsResult]方法
* @receiver F 基于[FragmentActivity]的扩展方法
* @param permission Array<out String> 要申请的权限
* @param onRequestDone () -> Unit 申请成功时的回调
* @return LambdaHolder<Unit> 可以在此对象上继续调用[#onDenied]方法来设置申请失败时的回调
*/
fun <F : FragmentActivity> F.requestPermissions(
vararg permission: String,
onRequestDone: () -> Unit): LambdaHolder<Unit> {
//获取一个与已有编码不重复的编码
val requestCode = codeGenerate(permissionHolder)
//查找Activity中有没有空的Fragment,如果没有则创建空的Fragment并添加到Activity中
val emptyFragment = findOrCreateEmptyFragment(supportFragmentManager)
//使用Fragment的requestPermissions方法申请权限
//onRequestDone的类型是 () -> Unit,requestPermissions接收的类型是(Unit) -> Unit
//所以不能直接传入,需要做个中转
return emptyFragment.requestPermissions(requestCode, *permission) {
onRequestDone()
}
}
1.startActivityForResult方法就是最终调用的方法了,它是一个包级扩展方法,可以在FragmentActivity中直接调用,我们要传入的参数只有三个,其中options参数是可选的,没有动画需求的时候,可以忽略掉它。callback参数也是可选的,它作为接收返回成功时参数的回调,在部分情况下,也可能会不需要,此时往往要用到setBeforeCallback方法来设置一个总的回调,这个回调会在调用onDefined、onCanceled或onSuccess之前就会调用。
2.requestPermissions方法是申请权限时要调用的方法了,只需要传入一个可变的权限参数以及一个成功时的回调即可,后面还可以链式使用setOnDeniedCallback方法来设置失败时的回调,这个与上一个方法就简单得多了。
六、实践及效果展示
接下来,我通过调用相机拍照,然后将照片展示在界面上的这么一个简单功能,来展示这种写法到底是怎样的。
先创建一个Activity,布局长这样:
然后在res/xml下创建一个名为"file_paths.xml"的文件,输入以下内容:
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path path="." name="sdcard" />
</paths>
再打开AndroidManifest.xml文件,在application节点内,添加以下代码:
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.FileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"/>
</provider>
注意,因为我引入了AndroidX,所以android:name这一行是"androidx.core.content.FileProvider",没有引入AndroidX的话,请导入正确的FileProvider。
再添加要使用的摄像头权限:
<uses-permission android:name="android.permission.CAMERA"/>
我们打开Activity,添加一个toUri方法:
private fun File.toUri(): Uri {
return if (Build.VERSION.SDK_INT > 23) {
FileProvider.getUriForFile(this@MainActivity, "$packageName.FileProvider", this)
} else {
Uri.fromFile(this)
}
}
一个Activity内的扩展方法,与大多数人所理解的不太一样的是,它并不是一个静态的包级函数。
之后,为按钮设置点击事件,在onClick方法内添加以下代码:
override fun onClick(v: View?) {
//先申请权限
requestPermissions(Manifest.permission.CAMERA) {
//把照片保存到不需要权限就能使用的缓存目录下
val photoPath = externalCacheDir ?: cacheDir
if (!photoPath.exists()) photoPath.mkdirs()
//分配一个尽量不重复的名称
val photoFile = File(photoPath, "${System.currentTimeMillis()}.jpg")
if (photoFile.exists()) photoFile.delete()
//转换为uri
val outputUri = photoFile.toUri()
//装载Intent
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
.putExtra(MediaStore.EXTRA_OUTPUT, outputUri)
//启动Activity并接收结果
startActivityForResult(intent) {
//显示图片
imageView.setImageURI(outputUri)
}.setOnCanceledCallback {
toast("取消了拍照")
}
//设置申请权限失败时的回调
}.setOnDeniedCallback {
AlertDialog.Builder(this)
.setTitle("提示")
.setCancelable(false)
.setMessage("摄像头权限被拒绝,无法调用相机功能。")
.setPositiveButton("我知道了") { d, _ ->
d.dismiss()
}
.setNegativeButton("再次申请") { d, _ ->
//直接调用此方法来继续发起申请权限操作
onClick(null)
d.dismiss()
}
.show()
}
}
以上就是基于Kotlin特性的使用方法了,可以看到,先申请了摄像头权限,后面还跟了一个setOnDeniedCallback方法传入了在申请权限失败时回调的Lambda,代码中是弹出对话框提示用户权限被拒绝了。当权限申请成功时,我们先是进行一堆设置照片路径的操作,然后调用了startActivityForResult方法,传入Intent对象以及成功时的Lambda回调,然后后面又跟了一个用于设置取消拍照时回调Lambda的setOnCanceledCallback方法,那么现在,我们来启动程序来看看效果如果。
申请权限时:
当权限被拒绝时:
当拍照取消时:
当拍照成功时:
七、使用inline特性实现仿Anko写法
新创建一个kt文件,输入以下代码:
/**
* 启动Activity并接收Intent的扩展方法,不需要重写[#onActivityResult]方法
* @receiver 基于[FragmentActivity]的扩展方法
* @param params Array<out Pair<String, *>> 要携带的参数
* @param options Bundle? 动画参数
* @param callback (Intent) -> Unit 返回此界面时,当ResultCode为RESULT_OK时的回调
* @return LambdaHolder<Intent> 可以在此对象上继续调用 [LambdaHolder#onCanceled]或
* [LambdaHolder#onDefined]方法来设置ResultCode为RESULT_CANCELED或RESULT_FIRST_USER时的回调
*/
inline fun <reified F : FragmentActivity> FragmentActivity.startActivityForResult(
vararg params: Pair<String, *>,
options: Bundle? = null,
noinline callback: (Intent) -> Unit = {}): LambdaHolder<Intent> {
return startActivityForResult(intentFor<F>(*params), options, callback)
}
/**
* 基于[Fragment]的扩展方法
*/
inline fun <reified F : FragmentActivity> Fragment.startActivityForResult(
vararg params: Pair<String, *>,
options: Bundle? = null,
noinline callback: (Intent) -> Unit = {}): LambdaHolder<Intent> {
return requireActivity().startActivityForResult(intentFor<F>(*params), options, callback)
}
分别可以在FragmentActivity中以及Fragment中调用:
startActivityForResult<OtherActivity>("name" to "Kotlin", "size" to 1024) { intent ->
toast("返回的数据:${intent.getStringExtra("name")}")
}.setOnCanceledCallback {
toast("取消了操作")
}.setOnDefinedCallback {
toast("自定义的动作")
}
或者是不管resultCode是什么都要执的操作:
startActivityForResult<OtherActivity>("name" to "Kotlin", "size" to
startActivityForResult<OtherActivity>().setBeforeCallback {
toast("回来了,加载数据")
//do something...
}
所有代码看这里:
https://github.com/xiazunyang/ChainLambda
注意:本示例中,使用了AndroidX、Android Ktx以及Anko等扩展包、工具包,如需使用本篇中的代码,请按需调整代码。
推荐阅读:
欢迎关注我的公众号,学习技术或投稿
长按上图,识别图中二维码即可关注