Compose Multiplatform+kotlin Multiplatfrom第二弹
前言
上篇文章kmp实战处理基础业务,更多都是实现shared模块下的一套代码多平台共用的业务功能,但是遇到平台特性不同依然摆脱不了原生实现。
本文罗列下双端都要实现的功能:
- 文件系统统一管理,Android是有分内部存储私有,外部储存有公共目录和私有目录,私有其他应用无法查看,内部存储连手机文件管理也看不到,我们手机的文件肯定存在用户需要对文件进行分享、复制等操作,所以要归类好文件目录。iOS端每次保存的后的路径文本是被沙盒隔离的,就是你下载一图片保存后,你记录下他的保存路径,下次你用完整的路径查不到,或者说你规定下载一个文件目录,每次下载到里面,不同时期路径中的文件目录会自动改变,当然也能处理,后面细谈。
- 多端生命周期监听,主要用来监听应用退出和进入,业务逻辑需要放到shared一处修改多端共用。
- 兼容长图的水印图片生成保存到图册,根据媒体文件的路径保存到图册,支持gif,图片png,jpg,jepg,视频mov,mp4,m4v。
- 文档预览功能,Android系统不支持直接预览文档,类似doc/xls/ppt等,所以Android用的最新付费腾讯云浏览服务sdk,iOS端是支持系统预览文档,这里是通过ktor下载文件到本地目录,然后通过路径作为参数打开预览窗口。
- ktor队列下载文件,支持单线程的断点下载,可暂停和恢复下载,可监控下载进度和下载速度,回调文件保存地址,这里下载的文件会根据格式自动先下载到内部缓存目录,如果是图片或需要添加水印的图片会重绘图后生成到图册。
文件目录管理
文件目录在iOS和Android是不同,如果在kotlin multiplatform compose(kmp)项目下,shared模块里是没有公共的文件类,我们的Java.io.File是Android特有的,也没FileInputStream去读写文件,一开始我看coil3源码如何保存图片保存,我看内部使用okio的FileSystem的处理,但是只能内部私有目录。
shared模块:
//在shared各业务调用创建文件
expect fun createPlatformFile(filePath: String)
//所有要处理文件前都要先创建文件夹,才能创文件
expect fun createPlatformRootDir()
//缓存图片、视频文件的内部目录
expect fun getDevDCIM(userId: Long, isCopy: Boolean = false): String
//Android端可对外查看的目录,iOS只有内部
expect fun getGlobalDirPath(userId: Long): String
fun printLogW(msg: String, tag: String) {
if (true) {
var log = msg
val segmentSize = 3 * 1024
val length = log.length
if (length <= segmentSize) {
Logger.w(tag) {
msg }
} else {
while (log.length > segmentSize) {
val logContent = log.substring(0, segmentSize)
log = log.replace(logContent, "")
// Logger.w(tag) { "\n$log" }
println("\n$log")
}
}
}
}
object GlobalCode {
//文件和目录创建是不一样的,这里Path其实就是String
fun createFile(file: Path, mustCreate: Boolean = false) {
if (mustCreate) {
FileSystem.SYSTEM.sink(file, mustCreate).close()
} else {
FileSystem.SYSTEM.appendingSink(file).close()
}
}
fun createDirectory(dir: Path) {
if (!FileSystem.SYSTEM.exists(dir)) {
FileSystem.SYSTEM.createDirectories(dir, true)
}
}
fun getFileCacheDir(): Path {
return FileSystem.SYSTEM_TEMPORARY_DIRECTORY / "ark_file/${
getCacheLong(
KmmConfig.USER_ID,
0
)
}"
}
fun getMediaCacheDir(): Path {
return FileSystem.SYSTEM_TEMPORARY_DIRECTORY / "ark_media/${
getCacheLong(
KmmConfig.USER_ID,
0
)
}"
}
//私有目录,手机也查看不到
fun getDownloadDir(): Path {
return FileSystem.SYSTEM_TEMPORARY_DIRECTORY / "ark_download/${
getCacheLong(
KmmConfig.USER_ID,
0
)
}"
}
//这里需要平台特性,图片和文档目录区分,高版本安卓系统又无法直接下载到图册
fun getGlobalDCIMPath(
url: String?,
userId: Long = 0,
isCopy: Boolean = false
): String {
if (canPreview2DCIM(getFileTypeByUrl(url + "").toUpperCase())) {
return getDevDCIM(userId, isCopy) //图册目录、图片的缓存目录
} else {
return getGlobalDirPath(userId) //公共目录
}
}
//下载到相册的文件类型
fun canPreview2DCIM(fileType: String?): Boolean {
return when (fileType) {
"JPG", "PNG", "GIF", "JPEG",
"MP4", "M4V", "MOV",
-> {
true
}
else -> {
false
}
}
}
fun getFileTypeByUrl(url:String):String{
//这里fileType 就是 PNG , JEPG ,简单处理,我的是跟业务url规则有关,自己修改
if(url.contains(".png")){
return "PNG" }
if(url.contains(".jpeg")){
return "JPEG"}
return "JPG"
}
//是否可以预览
fun canPreviewFile(fileType: String?): String? {
fileType?.let {
if (it.contains("JPG") || it.contains("PNG") || it.contains("GIF") || it.contains("JPEG") ||
it.contains("MP4") || it.contains("M4V") || it.contains("MOV") ||
it.contains("PPTX") || it.contains("PPT") || it.contains("DOC") || it.contains("DOCX") || it.contains(
"PDF"
) || it.contains("TXT") ||
it.contains("XLS") || it.contains("XLSX") || it.contains("DWG")
) {
return fileType
}
}
return null
}
fun fileExist(filePath: String?): Boolean {
if (filePath == null) return false
return FileSystem.SYSTEM.exists(filePath.toPath())
}
fun getCacheLong():Long{
return 0} //这是我内部业务缓存API,随机放吧,用来隔离不同账号数据的
}
androidMain模块
class MainApplication : Application() {
companion object {
lateinit var appContext: Context
val SAVE_DIR = Environment.DIRECTORY_DCIM
/****私有目录,卸载被删除*****/
@JvmStatic
fun getAppCacheDir(): File? {
return appContext.getExternalFilesDir("ark_cache")
}
//预览文件(图片、视频、文档)-都是要先下载好,先放私有目录,使用时复制到共有,不同账号数据隔离
@JvmStatic
fun getUserCacheDir(): File? {
return appContext.getExternalFilesDir("file_cache")
}
@JvmStatic
fun getImageCacheDir(): File? {
return appContext.getExternalFilesDir("ark_img")
}
//如果每次同链接都下载都这,然后要下载时复制到共有目录
@JvmStatic
fun getDownLoadDir(): File {
// return appContext.getExternalFilesDir("ark_downLoad") //外部存储的私有目录 还是可以对外看见
return File(appContext.filesDir.absolutePath + File.separator + "ark_download") //内部存储,手机也无法看见
}
@JvmStatic
fun getUserDownLoadDir(userId: Long=getCacheLong(KmmConfig.USER_ID,0)): File? {
return appContext.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS + "/" + userId)
}
/****公有目录,要权限 目前所有的手动下载都到这里,允许重复下载****/
@JvmStatic
fun getGlobalDir(userId: Long = getCacheLong(KmmConfig.USER_ID,0)): String {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
//android10开始分区存储
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath + File.separator + "ark_download" + File.separator + userId + File.separator
} else {
Environment.getExternalStorageDirectory().absolutePath + File.separator + "ark_download" + File.separator + userId + File.separator
}
}
/** 文件到图册*/
@JvmStatic
fun getDevDCIM(userid: Long = getCacheLong(KmmConfig.USER_ID,0), isCopy: Boolean = false): String {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (isCopy) {
// if (isHarmonyOs()) //临时缓存文件夹,Android10后是先下载到外部再复制到图册,鸿蒙必须是应用内部再复制,这里特意分开不然不容易发现
getUserDownLoadDir(userid)?.absolutePath + ""
} else {
Environment.getExternalStorageDirectory().absolutePath + File.separator + SAVE_DIR + File.separator + "ark_download" + File.separator + userid + File.separator
}
} else {
Environment.getExternalStoragePublicDirectory(SAVE_DIR).absolutePath + File.separator + "ark_download" + File.separator + userid + File.separator
}
}
}
}
actual fun createPlatformFile(filePath: String) {
createPlatformRootDir()
createFile(filePath)
}
private fun createFile(fileName: String, dir: String?): String {
var file: File
if (dir.isNullOrEmpty()) {
file = File(fileName)
} else {
file = File(dir, fileName)
printLogW("create??$file")
}
if (!file.exists()) {
file.createNewFile()
}
return file.absolutePath
}
actual fun createPlatformRootDir() {
//指定在相册内外部目录 /storage/emulated/0/DCIM/ark_download/3515
//Android10 bug 不加这个requestLegacyExternalStorage,无法创建
val DCIMDir = MainApplication.getDevDCIM()
val d1 = File(DCIMDir).apply {
printLogW(this.absolutePath) }.mkdirs()
//普通公共目录 /storage/emulated/0/Download/ark_download/3515
val globalDir = MainApplication.getGlobalDir()
val d2 = File(globalDir).apply {
printLogW(this.absolutePath) }.mkdirs()
//内部目录,手机看不到 /data/user/0/com.lyentech.ark/files/ark_download
val downloadDir = MainApplication.getDownLoadDir()
val d3 = downloadDir.apply {
printLogW(this.absolutePath) }.mkdirs()
//Android10后下载的图片不能直接到图册,中转文件夹再复制到图册
// /storage/emulated/0/Android/data/com.lyentech.ark/files/Download/3515
val DCIMCacheDir = MainApplication.getUserDownLoadDir()
val d4 = DCIMCacheDir!!.apply {
printLogW(this.absolutePath) }.mkdirs()
// printLogW("exists>$d1 $d2 $d3 $d4")
GlobalCode.createDirectory(GlobalCode.getFileCacheDir())
GlobalCode.createDirectory(GlobalCode.getMediaCacheDir())
GlobalCode.createDirectory(GlobalCode.getDownloadDir())
// printLogW("exists>${File(DCIMDir).exists()} ${File(globalDir)} ${downloadDir.exists()} ${DCIMCacheDir.exists()}")
}
actual fun getDevDCIM(userId: Long, isCopy: Boolean): String {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (isCopy) {
MainApplication.getUserDownLoadDir(userId)?.absolutePath + ""
} else {
Environment.getExternalStorageDirectory().absolutePath + File.separator +
MainApplication.SAVE_DIR + File.separator + "ark_download" +
File.separator + userId + File.separator
}
} else {
Environment.getExternalStoragePublicDirectory(MainApplication.SAVE_DIR).absolutePath +
File.separator + "ark_download" + File.separator + userId + File.separator
}
}
//公共目录
actual fun getGlobalDirPath(userId: Long): String {
return MainApplication.getGlobalDir(userId)
}
iosMain模块
//iOS的保存目录是动态运算时路径自动变化的,但是系统提供API去获取
fun getIOSRootDir(): String

最低0.47元/天 解锁文章
4129






