Ketch - Android中文件下载的一体化解决方案

社区移动开发Android

picture.image

在Android中管理文件下载的一体化解决方案 - Ketch

我要向你展示一个用于在Android系统中下载文件的神奇库. 它基本上是一体化的解决方案, 可以处理WorkManager,Notifications,Local Database和许多其他功能, 你不需要自己实现这些功能!

picture.image

简介

该库本身名为 Ketch. 它完全使用 Kotlin 编程语言构建. 同样重要的是, 即使你关闭了应用或销毁了Activity, 它也能保证你的文件会被下载.

这是因为该库还能持续任何已开始下载的文件, 这意味着你可以Pause, Resume, Cancel, 甚至Retry下载文件.

它有一个非常简单和用户友好的UI, 我现在就向你展示!

依赖与权限

首先, 你需要确保添加一个依赖项. 然后, 我们需要声明一些权限. 比如 POST_NOTIFICATIONS, 如果你使用的是 33 级或更高的 API, 就需要使用该权限. 此外, 还需要FOREGORUND_SERVICEFOREGROUND_SERVICE_DATA_SYNC, 以防万一.

对于第一个, 你只需要运行时权限.

不过, 我将在 MainActivity 中快速添加权限检查, 这样我们就可以在应用启动时立即请求该权限.

 if (ContextCompat.checkSelfPermission(
                this,
                android.Manifest.permission.POST_NOTIFICATIONS
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                ActivityCompat.requestPermissions(
                    this,
                    arrayOf(android.Manifest.permission.POST_NOTIFICATIONS),
                    1
                )
            }
        }

初始化

接下来, 我们要初始化 Ketch 对象. 有一个 setNotificationConfig()函数允许我们启用并自定义通知的外观. 开始下载后, 通知会自动出现. 它默认提供大量信息, 但你也可以修改.

第二个函数是setDownloadConfig(), 它允许我们自定义connectionTimeoutreadTimeout. 默认情况下, 这些值被设置为10 秒, 但我们可以轻松增加它们.

最后, 调用一个构建函数, 我们就可以开始了. 这个对象是一个单例, 所以你不必担心会出现多个实例.

    private lateinit var ketch: Ketch

    @RequiresApi(Build.VERSION_CODES.P)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()

       ...

        ketch = Ketch.builder()
            .setNotificationConfig(
                NotificationConfig(
                    enabled = true,
                    smallIcon = R.drawable.ic_launcher_foreground
                )
            )
            .setDownloadConfig(
                DownloadConfig(
                    connectTimeOutInMs = 15000,
                    readTimeOutInMs = 15000
                )
            )
            .build(this)
    }

文件信息

在这个MainScreen可组合文件中, 我已经为这个演示项目创建了所有UI组件, 以及三个常量.

第一个常量FILE_TAG将允许我们为要下载的文件设置一个唯一标识符, 这样以后我们就可以使用相同的标记对同一文件执行Pause, Resume, Cancel等操作. 当你下载多个文件并执行并行下载时, 这一点也很重要, 因为它可以让你更轻松地管理整个过程.

const val FILE_TAG = "video"

第二个标签是文件名(FILE_NAME), 它将显示在UI和通知对话框中, 同时也是我们要保存在文件系统中的文件的实际名称.

const val FILE_NAME = "MyVideo.mp4"

第三个是我们要下载的文件的DOWNLOAD_URL. 该库支持各种文件扩展名: 包括视频, 图片, GIF 动画, PDF, APK 文件等.

const val DOWNLOAD_URL = "https://file-examples.com/storage/fe45dfa76e66c6232a111c9/2017/04/file_example_MP4_1920_18MG.mp4"

实现该逻辑

首先, 我要快速将 Ketch 对象传递给我们的主屏幕可组合器. 然后, 我将声明三个属性, 当下载开始时, 我将观察并更新它们. 另外还有一个属性将指示何时开始和何时停止收集数据.

@Composable
fun MainScreen(ketch: Ketch) {
    val scope = rememberCoroutineScope()
    var status by remember { mutableStateOf(Status.DEFAULT) }
    var progress by remember { mutableIntStateOf(0) }
    var total by remember { mutableLongStateOf(0L) }
    var isCollecting by remember { mutableStateOf(false) }
  
    ...
}

之后, 在LaunchedEffect中, 我们将观察下载进程, 该进程与我们已定义的FILE_TAG相同.

Copy LaunchedEffect(isCollecting) {
        if (isCollecting) {
            ketch.observeDownloadByTag(tag = FILE_TAG)
                .collect { downloadModel ->
                    for (model in downloadModel) {
                        status = model.status
                        progress = model.progress
                        total = model.total
                    }
                }
        }
    }

进度值将显示在文件名的右侧. 我们也将使用该值来计算线性进度指示器的百分比.

Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(all = 48.dp),
        verticalArrangement = Arrangement.Center
    ) {
        Text(
            modifier = Modifier.fillMaxWidth(),
            text = status.name,
            textAlign = TextAlign.Center,
            fontSize = MaterialTheme.typography.titleLarge.fontSize,
            fontWeight = FontWeight.Medium
        )
        Spacer(modifier = Modifier.height(48.dp))
        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.SpaceBetween
        ) {
            Text(text = FILE_NAME)
            Text(text = "$progress%")
        }
        Spacer(modifier = Modifier.height(6.dp))

       ...
}

在进度指示器下方, 我们还将使用进度以及文件的总大小(Byte), 将其转换为MB.

之后, 我们还将Byte转换为MB, 并从结果中只获取前两位小数.

        LinearProgressIndicator(
            modifier = Modifier.fillMaxWidth(),
            progress = { progress / 100f }
        )
        Spacer(modifier = Modifier.height(6.dp))
        Text(
            modifier = Modifier.fillMaxWidth(),
            text = "${
                calculateDownloadedMegabytes(
                    progress,
                    total
                )
            }MB / ${getTwoDecimals(value = total / (1024.0 * 1024.0))}MB",
            fontSize = MaterialTheme.typography.bodySmall.fontSize,
            textAlign = TextAlign.End
        )
        Spacer(modifier = Modifier.height(24.dp))

下面是我们要进行的计算的公式. 非常简单. 如果你想查看这个项目, 我也会留下链接

fun calculateDownloadedMegabytes(progress: Int, totalBytes: Long): String {
    val downloadedBytes = progress / 100.0 * totalBytes
    return getTwoDecimals(value = downloadedBytes / (1024.0 * 1024.0))
}

fun getTwoDecimals(value: Double): String {
    return String.format(Locale.ROOT, "%.2f", value)
}

现在是这些按钮的逻辑. 因此, 当我们点击Download按钮时, 我们要将isCollecting设置为true, 调用下载函数并传递这些常量. 对于文件路径, 我在这里传递的是下载目录. 但你也可以选择任何文件路径.

        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.Center
        ) {
            Button(
                onClick = {
                    isCollecting = true
                    ketch.download(
                        tag = FILE_TAG,
                        url = DOWNLOAD_URL,
                        fileName = FILE_NAME,
                        path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).path
                    )
                }
            ) {
                Text(text = "Download")
            }
            Spacer(modifier = Modifier.width(8.dp))
            Button(onClick = { ketch.cancel(tag = FILE_TAG) }) {
                Text(text = "Cancel")
            }
        }

最后, 对于Pause, Resume, Cancel和Retry, 我们可以调用相应的函数并传递相同的FILE_TAG.

        Spacer(modifier = Modifier.height(10.dp))
        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.Center
        ) {
            Button(onClick = { ketch.pause(tag = FILE_TAG) }) {
                Text(text = "Pause")
            }
            Spacer(modifier = Modifier.width(8.dp))
            Button(onClick = { ketch.resume(tag = FILE_TAG) }) {
                Text(text = "Resume")
            }
            Spacer(modifier = Modifier.width(8.dp))
            Button(onClick = { ketch.retry(tag = FILE_TAG) }) {
                Text(text = "Retry")
            }
        }

只有Delete按钮的逻辑略有不同. 我们将调用 cleardb()函数, 该函数不仅会从存储器中删除文件, 还会清除有关该文件的数据库信息. 正如我在开头提到的, 该库将所有文件信息保存在本地数据库中, 以便执行Pause, Resume, Retry等各种操作.

        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.Center
        ) {
            Button(
                onClick = {
                    ketch.clearDb(tag = FILE_TAG)
                    scope.launch {
                        delay(100)
                        status = Status.DEFAULT
                        progress = 0
                        total = 0L
                        isCollecting = false
                    }
                }
            ) {
                Text(text = "Delete")
            }
        }

删除文件后, 我们会稍作延迟后重置这些属性, 以避免 UI 闪烁.

总结一下

今天主要介绍了 Android 端的管理文件下载的一体化解决方案 - Ketch, 并提供了一些核心代码. 总之是一个很优秀的下载管理库!

今天的内容就分享到这里啦!

一家之言, 欢迎斧正!

Happy Coding! Stay GOLDEN!

0
0
0
0
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论