我要向你展示一个用于在Android系统中下载文件的神奇库. 它基本上是一体化的解决方案, 可以处理WorkManager,Notifications,Local Database和许多其他功能, 你不需要自己实现这些功能!
简介
该库本身名为 Ketch. 它完全使用 Kotlin 编程语言构建. 同样重要的是, 即使你关闭了应用或销毁了Activity, 它也能保证你的文件会被下载.
这是因为该库还能持续任何已开始下载的文件, 这意味着你可以Pause, Resume, Cancel, 甚至Retry下载文件.
它有一个非常简单和用户友好的UI, 我现在就向你展示!
依赖与权限
首先, 你需要确保添加一个依赖项. 然后, 我们需要声明一些权限. 比如 POST_NOTIFICATIONS, 如果你使用的是 33 级或更高的 API, 就需要使用该权限. 此外, 还需要FOREGORUND_SERVICE和FOREGROUND_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()
, 它允许我们自定义connectionTimeout
和readTimeout
. 默认情况下, 这些值被设置为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!