以下是可直接复制的 Ketch 下载库完整 Demo,包含权限申请、下载/暂停/恢复/取消、通知配置与状态观察,基于 Ketch 1.0.3 版本。
一、准备工作(3步)
- 根 build.gradle(添加 JitPack)
allprojects {
repositories {
maven { url "https://jitpack.io" }
}
}
- 模块 build.gradle(添加依赖)
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
}
android {
defaultConfig {
minSdk 21
targetSdk 34
}
// 启用 ViewBinding
buildFeatures {
viewBinding true
}
}
dependencies {
implementation 'com.github.khushpanchal:Ketch:1.0.3'
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.7.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0'
implementation 'androidx.work:work-runtime-ktx:2.9.0'
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
}
- AndroidManifest.xml(声明权限)
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <!-- Android 13+ -->
二、布局文件(activity_main.xml)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"
android:gravity="center">
<EditText
android:id="@+id/etUrl"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="输入下载链接(如 APK/MP4)"
android:text="https://example.com/large-file.apk"/>
<TextView
android:id="@+id/tvStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="状态:等待开始"/>
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:max="100"
style="?android:attr/progressBarStyleHorizontal"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="horizontal"
android:gravity="center"
android:spacing="8dp">
<Button
android:id="@+id/btnStart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="开始下载"/>
<Button
android:id="@+id/btnPause"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="暂停"
android:enabled="false"/>
<Button
android:id="@+id/btnResume"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="恢复"
android:enabled="false"/>
<Button
android:id="@+id/btnCancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="取消"
android:enabled="false"/>
</LinearLayout>
</LinearLayout>
三、完整 MainActivity 代码(Kotlin)
package com.example.ketchdemo
import android.Manifest
import android.app.NotificationManager
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import com.example.ketchdemo.databinding.ActivityMainBinding
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import khushpanchal.ketch.Ketch
import khushpanchal.ketch.data.DownloadState
import khushpanchal.ketch.data.NotificationConfig
import okhttp3.OkHttpClient
import java.util.concurrent.TimeUnit
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private var currentDownloadId: Long = -1L
private val REQUEST_PERMISSIONS_CODE = 1001
private val requiredPermissions = mutableListOf(
Manifest.permission.INTERNET
).apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
add(Manifest.permission.POST_NOTIFICATIONS)
}
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
add(Manifest.permission.READ_EXTERNAL_STORAGE)
}
}.toTypedArray()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// 1. 初始化 Ketch 与配置
initKetch()
// 2. 检查权限
checkPermissions()
// 3. 绑定按钮事件
bindClickEvents()
}
private fun initKetch() {
// 自定义 OkHttp 客户端
val okHttpClient = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.build()
Ketch.setOkHttpClient(okHttpClient)
// 自定义通知配置
val notificationConfig = NotificationConfig(
channelId = "DOWNLOAD_CHANNEL",
channelName = "文件下载通知",
channelDescription = "显示下载进度与状态",
importance = NotificationManager.IMPORTANCE_HIGH,
smallIcon = R.drawable.ic_download, // 替换为你的通知图标
showSpeed = true,
showProgress = true
)
Ketch.initialize(applicationContext, notificationConfig)
}
private fun checkPermissions() {
val missingPermissions = requiredPermissions.filter {
ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
}
if (missingPermissions.isNotEmpty()) {
ActivityCompat.requestPermissions(
this, missingPermissions.toTypedArray(), REQUEST_PERMISSIONS_CODE
)
}
}
private fun bindClickEvents() {
binding.btnStart.setOnClickListener {
if (currentDownloadId != -1L) {
Toast.makeText(this, "已有任务在进行", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
val url = binding.etUrl.text.toString().trim()
if (url.isEmpty()) {
Toast.makeText(this, "请输入下载链接", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
startDownload(url)
}
binding.btnPause.setOnClickListener {
if (currentDownloadId != -1L) Ketch.pause(currentDownloadId)
}
binding.btnResume.setOnClickListener {
if (currentDownloadId != -1L) Ketch.resume(currentDownloadId)
}
binding.btnCancel.setOnClickListener {
if (currentDownloadId != -1L) {
Ketch.cancel(currentDownloadId)
resetState()
}
}
}
private fun startDownload(url: String) {
val saveDir = getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)!!
currentDownloadId = Ketch.download(
url = url,
fileName = "demo_file.apk", // 自定义文件名
saveDir = saveDir,
title = "Demo 文件下载",
description = "正在下载测试文件"
)
updateButtonStates(true, true, false, true)
observeDownloadState()
}
private fun observeDownloadState() {
lifecycleScope.launch {
Ketch.observeDownload(currentDownloadId).collectLatest { state ->
when (state) {
is DownloadState.Progress -> {
val progress = state.progress.toInt()
binding.progressBar.progress = progress
binding.tvStatus.text = "进度:{progress}% \| 速度:{state.speed} | 剩余:${state.timeRemaining}s"
Log.d("KetchDemo", "进度:progress% 速度:{state.speed}")
}
is DownloadState.Completed -> {
binding.tvStatus.text = "完成:${state.filePath}"
Toast.makeText(this@MainActivity, "下载完成", Toast.LENGTH_LONG).show()
resetState()
}
is DownloadState.Failed -> {
binding.tvStatus.text = "失败:${state.errorMessage}"
Toast.makeText(this@MainActivity, "下载失败:${state.errorMessage}", Toast.LENGTH_LONG).show()
resetState()
}
is DownloadState.Paused -> {
binding.tvStatus.text = "已暂停"
updateButtonStates(false, false, true, true)
}
is DownloadState.Queued -> binding.tvStatus.text = "等待中"
is DownloadState.Running -> binding.tvStatus.text = "下载中"
is DownloadState.Cancelled -> {
binding.tvStatus.text = "已取消"
resetState()
}
}
}
}
}
private fun updateButtonStates(
startEnabled: Boolean, pauseEnabled: Boolean,
resumeEnabled: Boolean, cancelEnabled: Boolean
) {
binding.btnStart.isEnabled = startEnabled
binding.btnPause.isEnabled = pauseEnabled
binding.btnResume.isEnabled = resumeEnabled
binding.btnCancel.isEnabled = cancelEnabled
}
private fun resetState() {
currentDownloadId = -1L
updateButtonStates(true, false, false, false)
binding.progressBar.progress = 0
binding.tvStatus.text = "状态:等待开始"
}
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<out String>, grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_PERMISSIONS_CODE) {
val allGranted = grantResults.all { it == PackageManager.PERMISSION_GRANTED }
if (!allGranted) {
Toast.makeText(this, "需要权限才能下载", Toast.LENGTH_LONG).show()
}
}
}
}
四、关键说明与使用
-
通知图标:将 R.drawable.ic_download 替换为你 App 的通知图标。
-
存储路径:示例用 App 私有目录( getExternalFilesDir ),无需动态存储权限(Android 13+ 推荐)。
-
断点续传:依赖服务器支持 HTTP Range 请求,否则无法断点。
-
状态观察:通过 Flow 监听下载状态,在主线程更新 UI。