Android 文件下载库ketch示例

以下是可直接复制的 Ketch 下载库完整 Demo,包含权限申请、下载/暂停/恢复/取消、通知配置与状态观察,基于 Ketch 1.0.3 版本。

一、准备工作(3步)

  1. 根 build.gradle(添加 JitPack)

allprojects {

repositories {

maven { url "https://jitpack.io" }

}

}

  1. 模块 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'

}

  1. 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()

}

}

}

}

四、关键说明与使用

  1. 通知图标:将 R.drawable.ic_download 替换为你 App 的通知图标。

  2. 存储路径:示例用 App 私有目录( getExternalFilesDir ),无需动态存储权限(Android 13+ 推荐)。

  3. 断点续传:依赖服务器支持 HTTP Range 请求,否则无法断点。

  4. 状态观察:通过 Flow 监听下载状态,在主线程更新 UI。

相关推荐
00后程序员张2 小时前
混合 App 怎么加密?分析混合架构下常见的安全风险
android·安全·小程序·https·uni-app·iphone·webview
城东米粉儿2 小时前
Glide BitmapPool 实现原理笔记
android
百***78753 小时前
gpt-image-1.5极速接入指南:3步上手+图像核心能力解析+避坑手册
android·java·gpt
撩得Android一次心动3 小时前
Android 四大组件——Service(服务)【基础篇2】
android·java·服务·四大组件·android 四大组件
是垚不是土3 小时前
MySQL8.0数据库GTID主从同步方案
android·网络·数据库·安全·adb
cnxy1883 小时前
MySQL地理空间数据完整使用指南
android·数据库·mysql
Digitally3 小时前
4种方法在电脑上查看安卓短信
android·电脑
_李小白3 小时前
【Android FrameWork】第四十天:SamplingProfilerService
android