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 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值