权限这个东西一直是软件保存文件的一个痛点,
从Android 10以前的在manifest中写代码
到Android 11的使用Scope
再到Android 13的新申请方式(还没了解透)
申请方式越来越简单,局限性也越来越大,为了防止软件去篡改公共空间中的文件,官方一直在优化(缩小)自写软件的权限。
这次Android 13中的Compose,使用了一种rememberLauncherForActivityResult东西。
接下来稍微写一下它的用法,我也没怎么搞明白,之后有时间再说。
rememberLauncherForActivityResult的使用
在这之前还要说明一个问题:
我们为什么要申请权限?
因为我有如下需求:
小说写好后要导出到公共文件夹中,以防软件卸载导致数据丢失
那么这里又有新的问题:为何要在公共文件夹?官方优化权限的目的就是为了让软件在自己的私有文件夹下进行文件存取 ,防止软件对公共文件进行修改,我们为何还要反其道而行之呢?
因为在私有文件目录下保存的文件,会在你卸载软件时一并删除,这样就算你导出了小说,万一忘记将其中的文件及时拿出,那么卸载时就随着软件一并删除了。
好,基于上述需求的基础上,我才使用这种方法实现将文件导出到公共文件夹中。
1、编译版本设置
首先,使用这些新玩意要保证一件事:编译版本
在调试各种代码的过程中,有些函数只有到34版本才能支持,所以如果有不能用的函数,他可能会让你调高编译版本
2、具体使用方法
这个东西用起来很简单,在哪打算导出,就在那申请:
首先在对应页面,我这里是MainPage中进行创建:
/** 单权限启动器 */
val singlePermissionResultLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission(),
onResult = {})
这个之中
ActivityResultContracts有很多内容,但是我最近还没研究过,只实现了需要的功能
onResult = {}这里可以返回权限申请的结果,你可以整个弹窗,或者其他的东西,网上搜一下很多的,但是我觉得没必要弹提示,因为从我的使用来看,每次导出都要申请,都要弹提示,太麻烦。
随后在想要执行的地方:
singlePermissionResultLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
就行了,这样就有权限了,之后该干嘛干嘛
我这里将导出功能添加到了长按中
注意这里使用的是combinedClickable,百度搜的话能搜出使用Clickable的,但是现在好像改了。
3、导出功能实现
其实到这里权限申请完毕后就是一点导出时的代码了,比如获取小说内容,章节信息等。
可以看到上面图里有个exportDialog的变量,
这里提个注意的点:想要提示框AlertDialog在全局显示,就得和Scaffold并列放,而用exportDialog这种Boolean的值来控制它的显示和隐藏。
我觉得这是个缺点,但是目前不知道怎么改正,不用Compose的话就挺简单的,哪里需要在哪里创建Builder,现在只能在最外层创建,开闭全靠变量控制,着实有点逆天。
以下就是这个提示框的代码了,CustomAlertDialog是自定义的,前面的文章有。
if (exportDialog) {
CustomAlertDialog(
title = "提示",
message = "确认要导出吗?",
onConfirm = {
exportDialog = false
viewModel.getAllChaptersOfOneBook(selectedId, selectedName)
Toast
.makeText(context, "导出成功", Toast.LENGTH_SHORT)
.show()
selectedId = 0
selectedName = ""
},
onDismiss = {
exportDialog = false
}
)
}
他俩是并列关系!
其实这也说明一点:就是每个变量变化它都会刷新这里的UI界面,这个Composable函数就会重组一次。
在viewmodel里就很简单了创建一个函数就行了:
fun getAllChaptersOfOneBook(id: Int,fictionName:String) {
viewModelScope.launch {
var exportText = ""
val systemTimeNameString = DateFormat.format("yyyyMMddHHmmss", System.currentTimeMillis()).toString()
val jobMain = viewModelScope.async {
mainPageRepository.getAllChaptersOfOneBook(id)
}
val chapterList = jobMain.await()
for(i in chapterList){
exportText += "@" + "第" + i.chapterId.toString() + "章" + "#" + i.chapterName + "#" + i.chapterText + "\n"
}
exportText += "\nText Accomplished\n"
val root = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).path
val file = File(root, "$systemTimeNameString$fictionName.txt")
withContext(Dispatchers.IO) {
val fos = FileOutputStream(file)
fos.write(exportText.toByteArray())
fos.close()
}
}
}
这里还要几个要提醒的地方:
- systemTimeNameString =
DateFormat.format("yyyyMMddHHmmss", System.currentTimeMillis()).toString()
为了每次保存时都能正确保存其对应的修改时间,所以对这个变量进行了小修改
- val root = Environment
.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).path
这句话就是指定导出到Download这个文件夹,root里是一个文件路径,其实我想做一个选择文件夹的,然后导出到对应路径,但是好像有点麻烦,就先写死了,这个其实也还不错,好找。
- 接下来就是用FileOutputStream导出文本信息了,用别的方法好像也行,我记得之前试过的方法中有的导出文本的方法不能导出换行符之类的,你得注意一下。
- 至于那个withContext(Dispatchers.IO),这个是我看它报警提示这么做我才加的,什么意思不太明白,但是大概意思就是防止导出内容过长而影响UI的运行,造成线程堵塞之类的,但是说实话我想要的效果就是线程堵塞,之后再考虑一下,毕竟这样比较安全。
这样以后,基本就实现功能了,运行看看效果!
长按导出:
导出提示:
在后台查看:
时间是可以对的上的 !
打开就是这样:
为什么要加这些符号呢?因为之后导入的时候要用,不然它分辨不出来哪个是章节题目,哪个是正文内容,而且导出成这种txt格式也方便后期复制啥的,方便投稿。