目录
1. 项目概述
1.1 项目背景
随着移动应用功能的不断丰富,网络通信需求日益复杂:
- 多服务器架构(API、CDN、上传、下载服务分离)
- 大文件下载/上传需要断点续传
- 用户体验要求高(进度可视化、操作可控)
- 代码可维护性和扩展性要求
1.2 技术选型
技术 | 版本 | 作用 |
---|---|---|
Retrofit | 2.9.0 | HTTP客户端,API接口定义 |
OkHttp | 4.9.0 | 底层网络库,拦截器支持 |
Kotlin协程 | 1.6.0 | 异步处理,替代回调 |
LiveData | 2.5.0 | 响应式数据流 |
Hilt | 2.44 | 依赖注入 |
Room | 2.5.0 | 本地数据库缓存 |
1.3 项目目标
- 统一网络请求管理
- 支持多BaseUrl动态切换
- 实现断点续传和多线程下载
- 提供友好的进度框交互
- 完善的错误处理和重试机制
2. 技术架构
2.1 分层架构
UI层(Activity/Fragment/Compose)
│
ViewModel层(业务逻辑、状态管理、进度反馈)
│
Repository层(数据获取、缓存、网络请求)
│
Network层(Retrofit+OkHttp+拦截器+多BaseUrl+下载管理)
│
Data层(数据模型、数据库、缓存)
2.2 设计原则
- 单一职责原则:每个类只负责一个功能
- 依赖倒置原则:高层模块不依赖低层模块
- 开闭原则:对扩展开放,对修改关闭
- 接口隔离原则:使用多个专门的接口
3. 核心功能实现
3.1 网络配置管理器
@Singleton
class NetworkManager @Inject constructor(
private val context: Context
) {
private val networkConfigs = mutableMapOf<NetworkType, NetworkConfig>()
private val okHttpClients = mutableMapOf<NetworkType, OkHttpClient>()
private val retrofitInstances = mutableMapOf<NetworkType, Retrofit>()
enum class NetworkType {
API_SERVER, // 主API服务器
CDN_SERVER, // CDN服务器
UPLOAD_SERVER, // 上传服务器
DOWNLOAD_SERVER // 下载服务器
}
data class NetworkConfig(
val baseUrl: String,
val timeout: Long = 30L,
val enableLogging: Boolean = true,
val enableAuth: Boolean = true,
val customHeaders: Map<String, String> = emptyMap()
)
init {
initializeNetworkConfigs()
}
private fun initializeNetworkConfigs() {
networkConfigs[NetworkType.API_SERVER] = NetworkConfig(
baseUrl = "https://api.example.com/",
timeout = 30L,
enableLogging = true,
enableAuth = true,
customHeaders = mapOf(
"Accept" to "application/json",
"User-Agent" to "AndroidApp/1.0"
)
)
networkConfigs[NetworkType.CDN_SERVER] = NetworkConfig(
baseUrl = "https://cdn.example.com/",
timeout = 60L,
enableLogging = false,
enableAuth = false,
customHeaders = mapOf(
"Cache-Control" to "max-age=3600"
)
)
networkConfigs[NetworkType.UPLOAD_SERVER] = NetworkConfig(
baseUrl = "https://upload.example.com/",
timeout = 120L,
enableLogging = true,
enableAuth = true,
customHeaders = mapOf(
"Content-Type" to "multipart/form-data"
)
)
networkConfigs[NetworkType.DOWNLOAD_SERVER] = NetworkConfig(
baseUrl = "https://download.example.com/",
timeout = 300L,
enableLogging = false,
enableAuth = false,
customHeaders = mapOf(
"Accept-Ranges" to "bytes"
)
)
}
fun getOkHttpClient(type: NetworkType): OkHttpClient {
return okHttpClients.getOrPut(type) {
createOkHttpClient(type)
}
}
fun getRetrofit(type: NetworkType): Retrofit {
return retrofitInstances.getOrPut(type) {
createRetrofit(type)
}
}
private fun createOkHttpClient(type: NetworkType): OkHttpClient {
val config = networkConfigs[type] ?: throw IllegalArgumentException("Unknown network type: $type")
return OkHttpClient.Builder().apply {
connectTimeout(config.timeout, TimeUnit.SECONDS)
readTimeout(config.timeout, TimeUnit.SECONDS)
writeTimeout(config.timeout, TimeUnit.SECONDS)
if (config.enableLogging) {
addInterceptor(LoggingInterceptor())
}
if (config.enableAuth) {
addInterceptor(AuthInterceptor())
}
if (config.customHeaders.isNotEmpty()) {
addInterceptor(CustomHeadersInterceptor(config.customHeaders))
}
addInterceptor(RetryInterceptor())
addInterceptor(CacheInterceptor(context))
}.build()
}
private fun createRetrofit(type: NetworkType): Retrofit {
val config = networkConfigs[type] ?: throw IllegalArgumentException("Unknown network type: $type")
return Retrofit.Builder()
.baseUrl(config.baseUrl)
.client(getOkHttpClient(type))
.addConverterFactory(GsonConverterFactory.create())
.build()
}
}
3.2 拦截器体系
// 日志拦截器
class LoggingInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
Log.d("Network", "Request: ${request.url}")
val response = chain.proceed(request)
Log.d("Network", "Response: ${response.code}")
return response
}
}
// 认证拦截器
class AuthInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val newRequest = originalRequest.newBuilder()
.addHeader("Authorization", "Bearer $token")
.addHeader("Content-Type", "application/json")
.build()
return chain.proceed(newRequest)
}
}
// 自定义头部拦截器
class CustomHeadersInterceptor(
private val headers: Map<String, String>
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val newRequest = originalRequest.newBuilder().apply {
headers.forEach { (key, value) ->
addHeader(key, value)
}
}.build()
return chain.proceed(newRequest)
}
}
// 重试拦截器
class RetryInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
var response: Response? = null
var exception: Exception? = null
repeat(3) { attempt ->
try {
response = chain.proceed(request)
if (response?.isSuccessful == true) {
return response!!
}
} catch (e: Exception) {
exception = e
if (attempt == 2) throw e
}
}
return response ?: throw exception ?: Exception("Request failed")
}
}
// 缓存拦截器
class CacheInterceptor(context: Context) : Interceptor {
private val cache = Cache(
directory = File(context.cacheDir, "http_cache"),
maxSize = 10 * 1024 * 1024 // 10MB
)
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
return response.newBuilder()
.header("Cache-Control", "public, max-age=3600")
.build()
}
}
3.3 API接口定义
// 主API服务
interface ApiService {
@GET("users")
suspend fun getUsers(): Response<List<User>>
@POST("users")
suspend fun createUser(@Body user: User): Response<User>
@GET("posts/{id}")
suspend fun getPost(@Path("id") id: Int): Response<Post>
}
// CDN服务
interface CdnService {
@GET("images/{imageId}")
suspend fun getImage(@Path("imageId") imageId: String): Response<ResponseBody>
@GET("videos/{videoId}")
suspend fun getVideo(@Path("videoId") videoId: String): Response<ResponseBody>
}
// 上传服务
interface UploadService {
@Multipart
@POST("upload")
suspend fun uploadFile(
@Part file: MultipartBody.Part,
@Part("description") description: RequestBody
): Response<UploadResponse>
@Multipart
@POST("upload/multiple")
suspend fun uploadMultipleFiles(
@Part files: List<MultipartBody.Part>
): Response<UploadResponse>
}
// 下载服务
interface DownloadService {
@Streaming
@GET
suspend fun downloadFile(@Url url: String): Response<ResponseBody>
@HEAD
suspend fun getFileInfo(@Url url: String): Response<Unit>
}
3.4 数据模型
// 用户模型
data class User(
val id: Int,
val name: String,
val email: String,
val avatar: String? = null
)
// 帖子模型
data class Post(
val id: Int,
val title: String,
val content: String,
val userId: Int,
val createdAt: String
)
// 上传响应
data class UploadResponse(
val success: Boolean,
val url: String?,
val message: String?
)
// 统一响应格式
data class ApiResponse<T>(
val code: Int,
val message: String,
val data: T?
)
4. 多BaseUrl管理
4.1 依赖注入配置
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideNetworkManager(@ApplicationContext context: Context): NetworkManager {
return NetworkManager(context)
}
@Provides
@Singleton
fun provideMultiThreadDownloader(
networkManager: NetworkManager,
fileManager: FileManager
): MultiThreadDownloader {
return MultiThreadDownloader(networkManager, fileManager)
}
@Provides
@Singleton
fun provideFileManager(@ApplicationContext context: Context): FileManager {
return FileManager(context)
}
// 提供不同类型的API服务
@Provides
@Singleton
fun provideApiService(networkManager: NetworkManager): ApiService {
return networkManager.getRetrofit(NetworkType.API_SERVER).create(ApiService::class.java)
}
@Provides
@Singleton
fun provideCdnService(networkManager: NetworkManager): CdnService {
return networkManager.getRetrofit(NetworkType.CDN_SERVER).create(CdnService::class.java)
}
@Provides
@Singleton
fun provideUploadService(networkManager: NetworkManager): UploadService {
return networkManager.getRetrofit(NetworkType.UPLOAD_SERVER).create(UploadService::class.java)
}
@Provides
@Singleton
fun provideDownloadService(networkManager: NetworkManager): DownloadService {
return networkManager.getRetrofit(NetworkType.DOWNLOAD_SERVER).create(DownloadService::class.java)
}
}
4.2 Repository层实现
class UserRepository @Inject constructor(
private val apiService: ApiService,
private val userDao: UserDao
) {
suspend fun getUsers(): Result<List<User>> {
return try {
// 先尝试从缓存获取
val cachedUsers = userDao.getAllUsers()
if (cachedUsers.isNotEmpty()) {
return Result.success(cachedUsers)
}
// 从网络获取
val response = apiService.getUsers()
if (response.isSuccessful) {
val users = response.body() ?: emptyList()
// 缓存到数据库
userDao.insertUsers(users)
Result.success(users)
} else {
Result.failure(Exception("Network error: ${response.code()}"))
}
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun createUser(user: User): Result<User> {
return try {
val response = apiService.createUser(user)
if (response.isSuccessful) {
val createdUser = response.body()!!
// 更新本地缓存
userDao.insertUser(createdUser)
Result.success(createdUser)
} else {
Result.failure(Exception("Create user failed"))
}
} catch (e: Exception) {
Result.failure(e)
}
}
}
4.3 ViewModel层实现
@HiltViewModel
class UserViewModel @Inject constructor(
private val userRepository: UserRepository
) : ViewModel() {
private val _users = MutableLiveData<List<User>>()
val users: LiveData<List<User>> = _users
private val _loading = MutableLiveData<Boolean>()
val loading: LiveData<Boolean> = _loading
private val _error = MutableLiveData<String>()
val error: LiveData<String> = _error
fun loadUsers() {
viewModelScope.launch {
_loading.value = true
_error.value = null
userRepository.getUsers()
.onSuccess { users ->
_users.value = users
}
.onFailure { exception ->
_error.value = exception.message
}
_loading.value = false
}
}
fun createUser(user: User) {
viewModelScope.launch {
_loading.value = true
userRepository.createUser(user)
.onSuccess { newUser ->
// 更新用户列表
val currentUsers = _users.value?.toMutableList() ?: mutableListOf()
currentUsers.add(newUser)
_users.value = currentUsers
}
.onFailure { exception ->
_error.value = exception.message
}
_loading.value = false
}
}
}
5. 断点续传实现
5.1 断点续传原理
断点续传的核心原理是HTTP的Range请求头:
- 检查服务器支持:通过HEAD请求检查Accept-Ranges: bytes
- 获取文件大小:通过Content-Length头获取文件总大小
- 记录已下载:保存已下载的字节数
- Range请求:使用Range: bytes=已下载字节数-继续下载
- 追加写入:将新下载的内容追加到临时文件
5.2 断点续传管理器
@Singleton
class ResumableDownloader @Inject constructor(
private val apiService: ApiService,
private val fileManager: FileManager
) {
// 下载任务状态
sealed class DownloadState {
object Idle : DownloadState()
data class Downloading(val progress: Int, val downloadedBytes: Long, val totalBytes: Long) : DownloadState()
data class Paused(val downloadedBytes: Long, val totalBytes: Long) : DownloadState()
data class Completed(val file: File) : DownloadState()
data class Error(val message: String) : DownloadState()
}
// 下载任务信息
data class DownloadTask(
val id: String,
val url: String,
val fileName: String,
val filePath: String,
var downloadedBytes: Long = 0,
var totalBytes: Long = 0,
var state: DownloadState = DownloadState.Idle
)
// 开始下载
suspend fun downloadFile(
url: String,
fileName: String,
onProgress: (Int, Long, Long) -> Unit,
onComplete: (File) -> Unit,
onError: (String) -> Unit
) {
try {
// 1. 检查服务器是否支持断点续传
val rangeSupport = checkRangeSupport(url)
if (!rangeSupport) {
// 不支持断点续传,使用普通下载
downloadWithoutResume(url, fileName, onProgress, onComplete, onError)
return
}
// 2. 获取文件总大小
val totalSize = getFileSize(url)
// 3. 获取已下载大小
val downloadedSize = fileManager.getDownloadedSize(fileName)
// 4. 创建临时文件
val tempFile = fileManager.createTempFile(fileName)
// 5. 执行断点续传下载
downloadWithResume(
url = url,
tempFile = tempFile,
downloadedSize = downloadedSize,
totalSize = totalSize,
onProgress = onProgress,
onComplete = {
val finalFile = fileManager.renameTempFile("$fileName.tmp", fileName)
onComplete(finalFile)
},
onError = onError
)
} catch (e: Exception) {
onError(e.message ?: "Download failed")
}
}
// 检查服务器是否支持断点续传
private suspend fun checkRangeSupport(url: String): Boolean {
return try {
val response = apiService.head(url)
val acceptRanges = response.headers()["Accept-Ranges"]
acceptRanges == "bytes"
} catch (e: Exception) {
false
}
}
// 获取文件大小
private suspend fun getFileSize(url: String): Long {
val response = apiService.head(url)
return response.headers()["Content-Length"]?.toLong() ?: -1L
}
// 断点续传下载实现
private suspend fun downloadWithResume(
url: String,
tempFile: File,
downloadedSize: Long,
totalSize: Long,
onProgress: (Int, Long, Long) -> Unit,
onComplete: () -> Unit,
onError: (String) -> Unit
) {
try {
// 创建带Range头的请求
val request = Request.Builder()
.url(url)
.addHeader("Range", "bytes=$downloadedSize-")
.build()
val response = apiService.downloadWithRange(request)
if (!response.isSuccessful) {
onError("Download failed: ${response.code}")
return
}
// 获取响应流
val inputStream = response.body()?.byteStream()
val outputStream = FileOutputStream(tempFile, true) // 追加模式
val buffer = ByteArray(8192)
var bytesRead: Int
var totalDownloaded = downloadedSize
while (inputStream?.read(buffer).also { bytesRead = it ?: -1 } != -1) {
outputStream.write(buffer, 0, bytesRead)
totalDownloaded += bytesRead
// 计算进度
val progress = ((totalDownloaded * 100) / totalSize).toInt()
onProgress(progress, totalDownloaded, totalSize)
}
outputStream.close()
inputStream?.close()
onComplete()
} catch (e: Exception) {
onError(e.message ?: "Download failed")
}
}
// 普通下载(不支持断点续传)
private suspend fun downloadWithoutResume(
url: String,
fileName: String,
onProgress: (Int, Long, Long) -> Unit,
onComplete: (File) -> Unit,
onError: (String) -> Unit
) {
try {
val response = apiService.download(url)
val totalSize = response.body()?.contentLength() ?: -1L
val tempFile = fileManager.createTempFile(fileName)
val inputStream = response.body()?.byteStream()
val outputStream = FileOutputStream(tempFile)
val buffer = ByteArray(8192)
var bytesRead: Int
var totalDownloaded = 0L
while (inputStream?.read(buffer).also { bytesRead = it ?: -1 } != -1) {
outputStream.write(buffer, 0, bytesRead)
totalDownloaded += bytesRead
if (totalSize > 0) {
val progress = ((totalDownloaded * 100) / totalSize).toInt()
onProgress(progress, totalDownloaded, totalSize)
}
}
outputStream.close()
inputStream?.close()
val finalFile = fileManager.renameTempFile("$fileName.tmp", fileName)
onComplete(finalFile)
} catch (e: Exception) {
onError(e.message ?: "Download failed")
}
}
}
5.3 文件管理器
@Singleton
class FileManager @Inject constructor(
private val context: Context
) {
// 获取下载目录
fun getDownloadDirectory(): File {
return context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
?: File(context.filesDir, "downloads")
}
// 创建临时文件
fun createTempFile(fileName: String): File {
val downloadDir = getDownloadDirectory()
if (!downloadDir.exists()) {
downloadDir.mkdirs()
}
return File(downloadDir, "$fileName.tmp")
}
// 检查文件是否存在
fun isFileExists(fileName: String): Boolean {
val file = File(getDownloadDirectory(), fileName)
return file.exists()
}
// 获取已下载的文件大小
fun getDownloadedSize(fileName: String): Long {
val tempFile = File(getDownloadDirectory(), "$fileName.tmp")
return if (tempFile.exists()) tempFile.length() else 0L
}
// 重命名临时文件为最终文件
fun renameTempFile(tempFileName: String, finalFileName: String): File {
val tempFile = File(getDownloadDirectory(), tempFileName)
val finalFile = File(getDownloadDirectory(), finalFileName)
tempFile.renameTo(finalFile)
return finalFile
}
// 删除文件
fun deleteFile(fileName: String): Boolean {
val file = File(getDownloadDirectory(), fileName)
return file.delete()
}
// 获取文件大小
fun getFileSize(fileName: String): Long {
val file = File(getDownloadDirectory(), fileName)
return if (file.exists()) file.length() else 0L
}
// 格式化文件大小
fun formatFileSize(bytes: Long): String {
return when {
bytes < 1024 -> "$bytes B"
bytes < 1024 * 1024 -> "${bytes / 1024} KB"
bytes < 1024 * 1024 * 1024 -> "${bytes / (1024 * 1024)} MB"
else -> "${bytes / (1024 * 1024 * 1024)} GB"
}
}
}
6. 多线程下载
6.1 多线程下载原理
多线程下载的核心思想是将大文件分割成多个小块,然后同时下载这些小块:
- 获取文件大小:通过HEAD请求获取文件总大小
- 分片计算:根据线程数计算每个分片的大小和范围
- 并发下载:多个协程同时下载不同的分片
- 进度统计:实时统计所有分片的下载进度
- 文件合并:所有分片下载完成后合并成完整文件
6.2 多线程下载管理器
@Singleton
class MultiThreadDownloader @Inject constructor(
private val networkManager: NetworkManager,
private val fileManager: FileManager
) {
private val downloadJobs = mutableMapOf<String, Job>()
private val downloadTasks = mutableMapOf<String, DownloadTask>()
// 下载分片信息
data class DownloadChunk(
val startByte: Long,
val endByte: Long,
val index: Int,
var downloadedBytes: Long = 0,
var isCompleted: Boolean = false
)
// 多线程下载配置
data class MultiThreadConfig(
val threadCount: Int = 3,
val chunkSize: Long = 1024 * 1024, // 1MB per chunk
val bufferSize: Int = 8192,
val enableSpeedLimit: Boolean = false,
val maxSpeed: Long = 1024 * 1024 // 1MB/s
)
// 开始多线程下载
suspend fun startMultiThreadDownload(
url: String,
fileName: String,
config: MultiThreadConfig = MultiThreadConfig(),
onProgress: (DownloadState) -> Unit,
onComplete: (File) -> Unit,
onError: (String) -> Unit
) {
val taskId = generateTaskId(url, fileName)
try {
// 1. 检查服务器是否支持Range请求
val rangeSupport = checkRangeSupport(url)
if (!rangeSupport) {
// 不支持Range,使用单线程下载
startSingleThreadDownload(url, fileName, onProgress, onComplete, onError)
return
}
// 2. 获取文件大小
val totalSize = getFileSize(url)
if (totalSize <= 0) {
onError("Cannot get file size")
return
}
// 3. 创建下载任务
val task = DownloadTask(
id = taskId,
url = url,
fileName = fileName,
totalBytes = totalSize
)
downloadTasks[taskId] = task
// 4. 分片下载
val chunks = createDownloadChunks(totalSize, config.chunkSize)
val tempFiles = chunks.map { chunk ->
File(fileManager.getDownloadDirectory(), "${fileName}_chunk_${chunk.index}")
}
// 5. 启动多线程下载
val jobs = chunks.mapIndexed { index, chunk ->
CoroutineScope(Dispatchers.IO).launch {
downloadChunk(
url = url,
chunk = chunk,
tempFile = tempFiles[index],
config = config,
onChunkProgress = { downloaded ->
updateTaskProgress(taskId, downloaded, totalSize, onProgress)
}
)
}
}
// 6. 等待所有分片下载完成
jobs.joinAll()
// 7. 合并文件
val finalFile = mergeChunkFiles(tempFiles, fileName)
// 8. 清理临时文件
tempFiles.forEach { it.delete() }
// 9. 完成回调
onComplete(finalFile)
updateTaskCompleted(taskId, finalFile, onProgress)
} catch (e: Exception) {
onError(e.message ?: "Download failed")
updateTaskError(taskId, e.message ?: "Download failed", onProgress)
}
}
// 创建下载分片
private fun createDownloadChunks(totalSize: Long, chunkSize: Long): List<DownloadChunk> {
val chunks = mutableListOf<DownloadChunk>()
var startByte = 0L
var index = 0
while (startByte < totalSize) {
val endByte = minOf(startByte + chunkSize - 1, totalSize - 1)
chunks.add(DownloadChunk(startByte, endByte, index))
startByte = endByte + 1
index++
}
return chunks
}
// 下载单个分片
private suspend fun downloadChunk(
url: String,
chunk: DownloadChunk,
tempFile: File,
config: MultiThreadConfig,
onChunkProgress: (Long) -> Unit
) {
val client = networkManager.getOkHttpClient(NetworkType.DOWNLOAD_SERVER)
val request = Request.Builder()
.url(url)
.addHeader("Range", "bytes=${chunk.startByte}-${chunk.endByte}")
.build()
val response = client.newCall(request).execute()
if (!response.isSuccessful) {
throw Exception("Download chunk failed: ${response.code}")
}
val inputStream = response.body?.byteStream()
val outputStream = FileOutputStream(tempFile)
val buffer = ByteArray(config.bufferSize)
var bytesRead: Int
var totalDownloaded = 0L
while (inputStream?.read(buffer).also { bytesRead = it ?: -1 } != -1) {
outputStream.write(buffer, 0, bytesRead)
totalDownloaded += bytesRead
chunk.downloadedBytes = totalDownloaded
onChunkProgress(totalDownloaded)
// 速度限制
if (config.enableSpeedLimit) {
delay(calculateDelay(bytesRead, config.maxSpeed))
}
}
outputStream.close()
inputStream?.close()
chunk.isCompleted = true
}
// 合并分片文件
private suspend fun mergeChunkFiles(chunkFiles: List<File>, fileName: String): File {
val finalFile = File(fileManager.getDownloadDirectory(), fileName)
val outputStream = FileOutputStream(finalFile)
chunkFiles.forEach { chunkFile ->
val inputStream = FileInputStream(chunkFile)
inputStream.copyTo(outputStream)
inputStream.close()
}
outputStream.close()
return finalFile
}
// 暂停下载
fun pauseDownload(taskId: String) {
downloadJobs[taskId]?.cancel()
val task = downloadTasks[taskId]
task?.let {
it.state = DownloadState.Paused(it.downloadedBytes, it.totalBytes)
}
}
// 恢复下载
fun resumeDownload(taskId: String) {
val task = downloadTasks[taskId]
task?.let {
// 重新开始下载,支持断点续传
startMultiThreadDownload(
url = it.url,
fileName = it.fileName,
onProgress = { state -> /* 处理进度 */ },
onComplete = { file -> /* 处理完成 */ },
onError = { error -> /* 处理错误 */ }
)
}
}
// 取消下载
fun cancelDownload(taskId: String) {
downloadJobs[taskId]?.cancel()
downloadJobs.remove(taskId)
downloadTasks.remove(taskId)
}
// 更新任务进度
private fun updateTaskProgress(
taskId: String,
downloaded: Long,
total: Long,
onProgress: (DownloadState) -> Unit
) {
val task = downloadTasks[taskId] ?: return
task.downloadedBytes = downloaded
val progress = ((downloaded * 100) / total).toInt()
val speed = calculateSpeed(downloaded, task.startTime)
val remainingTime = calculateRemainingTime(downloaded, total, speed)
val state = DownloadState.Downloading(progress, downloaded, total, speed, remainingTime)
task.state = state
onProgress(state)
}
// 计算下载速度
private fun calculateSpeed(downloaded: Long, startTime: Long): Long {
val elapsed = System.currentTimeMillis() - startTime
return if (elapsed > 0) (downloaded * 1000) / elapsed else 0
}
// 计算剩余时间
private fun calculateRemainingTime(downloaded: Long, total: Long, speed: Long): Long {
return if (speed > 0) (total - downloaded) / speed else 0
}
// 计算延迟时间(用于速度限制)
private fun calculateDelay(bytesRead: Int, maxSpeed: Long): Long {
return if (maxSpeed > 0) (bytesRead * 1000) / maxSpeed else 0
}
private fun generateTaskId(url: String, fileName: String): String {
return "${url.hashCode()}_${fileName.hashCode()}"
}
}
6.3 下载状态管理
sealed class DownloadState {
object Idle : DownloadState()
data class Downloading(
val progress: Int,
val downloadedBytes: Long,
val totalBytes: Long,
val speed: Long, // bytes per second
val remainingTime: Long // seconds
) : DownloadState()
data class Paused(val downloadedBytes: Long, val totalBytes: Long) : DownloadState()
data class Completed(val file: File) : DownloadState()
data class Error(val message: String) : DownloadState()
}
7. 进度框交互
7.1 进度框状态管理
// 进度框状态
sealed class ProgressDialogState {
object Hidden : ProgressDialogState()
data class Loading(
val title: String = "加载中...",
val message: String = "请稍候",
val progress: Int = 0,
val maxProgress: Int = 100,
val isIndeterminate: Boolean = true,
val showCancelButton: Boolean = false
) : ProgressDialogState()
data class Downloading(
val title: String = "下载中...",
val fileName: String,
val progress: Int,
val downloadedBytes: Long,
val totalBytes: Long,
val speed: String,
val remainingTime: String,
val showCancelButton: Boolean = true
) : ProgressDialogState()
data class Uploading(
val title: String = "上传中...",
val fileName: String,
val progress: Int,
val uploadedBytes: Long,
val totalBytes: Long,
val speed: String,
val remainingTime: String,
val showCancelButton: Boolean = true
) : ProgressDialogState()
data class Error(
val title: String = "错误",
val message: String,
val showRetryButton: Boolean = true
) : ProgressDialogState()
data class Success(
val title: String = "完成",
val message: String,
val showOpenButton: Boolean = false,
val filePath: String? = null
) : ProgressDialogState()
}
// 进度管理器
@Singleton
class ProgressManager @Inject constructor() {
private val _progressState = MutableLiveData<ProgressDialogState>()
val progressState: LiveData<ProgressDialogState> = _progressState
private val _downloadProgress = MutableLiveData<DownloadProgress>()
val downloadProgress: LiveData<DownloadProgress> = _downloadProgress
private val _uploadProgress = MutableLiveData<UploadProgress>()
val uploadProgress: LiveData<UploadProgress> = _uploadProgress
// 下载进度数据类
data class DownloadProgress(
val fileName: String,
val progress: Int,
val downloadedBytes: Long,
val totalBytes: Long,
val speed: String,
val remainingTime: String,
val isPaused: Boolean = false
)
// 上传进度数据类
data class UploadProgress(
val fileName: String,
val progress: Int,
val uploadedBytes: Long,
val totalBytes: Long,
val speed: String,
val remainingTime: String,
val isPaused: Boolean = false
)
// 显示加载进度
fun showLoading(
title: String = "加载中...",
message: String = "请稍候",
showCancelButton: Boolean = false
) {
_progressState.value = ProgressDialogState.Loading(
title = title,
message = message,
showCancelButton = showCancelButton
)
}
// 显示下载进度
fun showDownloading(
fileName: String,
progress: Int,
downloadedBytes: Long,
totalBytes: Long,
speed: String,
remainingTime: String
) {
_progressState.value = ProgressDialogState.Downloading(
fileName = fileName,
progress = progress,
downloadedBytes = downloadedBytes,
totalBytes = totalBytes,
speed = speed,
remainingTime = remainingTime
)
_downloadProgress.value = DownloadProgress(
fileName = fileName,
progress = progress,
downloadedBytes = downloadedBytes,
totalBytes = totalBytes,
speed = speed,
remainingTime = remainingTime
)
}
// 显示上传进度
fun showUploading(
fileName: String,
progress: Int,
uploadedBytes: Long,
totalBytes: Long,
speed: String,
remainingTime: String
) {
_progressState.value = ProgressDialogState.Uploading(
fileName = fileName,
progress = progress,
uploadedBytes = uploadedBytes,
totalBytes = totalBytes,
speed = speed,
remainingTime = remainingTime
)
_uploadProgress.value = UploadProgress(
fileName = fileName,
progress = progress,
uploadedBytes = uploadedBytes,
totalBytes = totalBytes,
speed = speed,
remainingTime = remainingTime
)
}
// 显示错误
fun showError(
title: String = "错误",
message: String,
showRetryButton: Boolean = true
) {
_progressState.value = ProgressDialogState.Error(
title = title,
message = message,
showRetryButton = showRetryButton
)
}
// 显示成功
fun showSuccess(
title: String = "完成",
message: String,
showOpenButton: Boolean = false,
filePath: String? = null
) {
_progressState.value = ProgressDialogState.Success(
title = title,
message = message,
showOpenButton = showOpenButton,
filePath = filePath
)
}
// 隐藏进度框
fun hideProgress() {
_progressState.value = ProgressDialogState.Hidden
}
// 格式化文件大小
fun formatFileSize(bytes: Long): String {
return when {
bytes < 1024 -> "$bytes B"
bytes < 1024 * 1024 -> "${bytes / 1024} KB"
bytes < 1024 * 1024 * 1024 -> "${bytes / (1024 * 1024)} MB"
else -> "${bytes / (1024 * 1024 * 1024)} GB"
}
}
// 格式化速度
fun formatSpeed(bytesPerSecond: Long): String {
return when {
bytesPerSecond < 1024 -> "$bytesPerSecond B/s"
bytesPerSecond < 1024 * 1024 -> "${bytesPerSecond / 1024} KB/s"
bytesPerSecond < 1024 * 1024 * 1024 -> "${bytesPerSecond / (1024 * 1024)} MB/s"
else -> "${bytesPerSecond / (1024 * 1024 * 1024)} GB/s"
}
}
// 格式化剩余时间
fun formatRemainingTime(seconds: Long): String {
return when {
seconds < 60 -> "${seconds}秒"
seconds < 3600 -> "${seconds / 60}分钟"
else -> "${seconds / 3600}小时${(seconds % 3600) / 60}分钟"
}
}
}
7.2 自定义进度对话框
class CustomProgressDialog @JvmOverloads constructor(
context: Context,
theme: Int = R.style.CustomProgressDialog
) : Dialog(context, theme) {
private lateinit var binding: DialogProgressBinding
private var onCancelClick: (() -> Unit)? = null
private var onRetryClick: (() -> Unit)? = null
private var onOpenClick: (() -> Unit)? = null
init {
initDialog()
}
private fun initDialog() {
binding = DialogProgressBinding.inflate(layoutInflater)
setContentView(binding.root)
// 设置对话框属性
setCancelable(false)
setCanceledOnTouchOutside(false)
// 设置窗口属性
window?.apply {
setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
setGravity(Gravity.CENTER)
setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
}
setupListeners()
}
private fun setupListeners() {
binding.btnCancel.setOnClickListener {
onCancelClick?.invoke()
}
binding.btnRetry.setOnClickListener {
onRetryClick?.invoke()
}
binding.btnOpen.setOnClickListener {
onOpenClick?.invoke()
}
}
fun updateState(state: ProgressDialogState) {
when (state) {
is ProgressDialogState.Hidden -> {
dismiss()
}
is ProgressDialogState.Loading -> {
showLoadingState(state)
}
is ProgressDialogState.Downloading -> {
showDownloadingState(state)
}
is ProgressDialogState.Uploading -> {
showUploadingState(state)
}
is ProgressDialogState.Error -> {
showErrorState(state)
}
is ProgressDialogState.Success -> {
showSuccessState(state)
}
}
}
private fun showLoadingState(state: ProgressDialogState.Loading) {
binding.apply {
tvTitle.text = state.title
tvMessage.text = state.message
if (state.isIndeterminate) {
progressBar.isIndeterminate = true
progressBar.progress = 0
} else {
progressBar.isIndeterminate = false
progressBar.max = state.maxProgress
progressBar.progress = state.progress
}
tvProgress.text = "${state.progress}%"
// 显示/隐藏取消按钮
btnCancel.visibility = if (state.showCancelButton) View.VISIBLE else View.GONE
// 隐藏其他按钮
btnRetry.visibility = View.GONE
btnOpen.visibility = View.GONE
}
if (!isShowing) show()
}
private fun showDownloadingState(state: ProgressDialogState.Downloading) {
binding.apply {
tvTitle.text = state.title
tvMessage.text = state.fileName
progressBar.isIndeterminate = false
progressBar.max = 100
progressBar.progress = state.progress
tvProgress.text = "${state.progress}%"
tvSpeed.text = state.speed
tvRemainingTime.text = state.remainingTime
// 显示取消按钮
btnCancel.visibility = if (state.showCancelButton) View.VISIBLE else View.GONE
// 隐藏其他按钮
btnRetry.visibility = View.GONE
btnOpen.visibility = View.GONE
}
if (!isShowing) show()
}
private fun showUploadingState(state: ProgressDialogState.Uploading) {
binding.apply {
tvTitle.text = state.title
tvMessage.text = state.fileName
progressBar.isIndeterminate = false
progressBar.max = 100
progressBar.progress = state.progress
tvProgress.text = "${state.progress}%"
tvSpeed.text = state.speed
tvRemainingTime.text = state.remainingTime
// 显示取消按钮
btnCancel.visibility = if (state.showCancelButton) View.VISIBLE else View.GONE
// 隐藏其他按钮
btnRetry.visibility = View.GONE
btnOpen.visibility = View.GONE
}
if (!isShowing) show()
}
private fun showErrorState(state: ProgressDialogState.Error) {
binding.apply {
tvTitle.text = state.title
tvMessage.text = state.message
// 隐藏进度条
progressBar.visibility = View.GONE
tvProgress.visibility = View.GONE
tvSpeed.visibility = View.GONE
tvRemainingTime.visibility = View.GONE
// 显示重试按钮
btnRetry.visibility = if (state.showRetryButton) View.VISIBLE else View.GONE
// 隐藏其他按钮
btnCancel.visibility = View.GONE
btnOpen.visibility = View.GONE
}
if (!isShowing) show()
}
private fun showSuccessState(state: ProgressDialogState.Success) {
binding.apply {
tvTitle.text = state.title
tvMessage.text = state.message
// 隐藏进度条
progressBar.visibility = View.GONE
tvProgress.visibility = View.GONE
tvSpeed.visibility = View.GONE
tvRemainingTime.visibility = View.GONE
// 显示打开按钮
btnOpen.visibility = if (state.showOpenButton) View.VISIBLE else View.GONE
// 隐藏其他按钮
btnCancel.visibility = View.GONE
btnRetry.visibility = View.GONE
}
if (!isShowing) show()
}
fun setOnCancelClickListener(listener: () -> Unit) {
onCancelClick = listener
}
fun setOnRetryClickListener(listener: () -> Unit) {
onRetryClick = listener
}
fun setOnOpenClickListener(listener: () -> Unit) {
onOpenClick = listener
}
}
7.3 进度框布局文件
<!-- dialog_progress.xml -->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="24dp"
android:background="@drawable/bg_progress_dialog"
android:orientation="vertical"
android:padding="24dp">
<!-- 标题 -->
<TextView
android:id="@+id/tvTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="加载中..."
android:textColor="@color/text_primary"
android:textSize="18sp"
android:textStyle="bold"
android:layout_marginBottom="8dp" />
<!-- 消息 -->
<TextView
android:id="@+id/tvMessage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="请稍候"
android:textColor="@color/text_secondary"
android:textSize="14sp"
android:layout_marginBottom="16dp" />
<!-- 进度条 -->
<ProgressBar
android:id="@+id/progressBar"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp" />
<!-- 进度文本 -->
<TextView
android:id="@+id/tvProgress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="0%"
android:textColor="@color/text_secondary"
android:textSize="12sp"
android:gravity="center"
android:layout_marginBottom="8dp" />
<!-- 速度信息 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="16dp">
<TextView
android:id="@+id/tvSpeed"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="0 KB/s"
android:textColor="@color/text_secondary"
android:textSize="12sp" />
<TextView
android:id="@+id/tvRemainingTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="剩余时间: --"
android:textColor="@color/text_secondary"
android:textSize="12sp" />
</LinearLayout>
<!-- 按钮容器 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end">
<Button
android:id="@+id/btnCancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="取消"
android:textColor="@color/text_secondary"
android:background="?android:attr/selectableItemBackground"
android:visibility="gone"
android:layout_marginEnd="8dp" />
<Button
android:id="@+id/btnRetry"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="重试"
android:textColor="@color/colorPrimary"
android:background="?android:attr/selectableItemBackground"
android:visibility="gone"
android:layout_marginEnd="8dp" />
<Button
android:id="@+id/btnOpen"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="打开"
android:textColor="@color/colorPrimary"
android:background="?android:attr/selectableItemBackground"
android:visibility="gone" />
</LinearLayout>
</LinearLayout>
7.4 Activity/Fragment使用示例
@AndroidEntryPoint
class DownloadActivity : AppCompatActivity() {
@Inject
lateinit var enhancedDownloadManager: EnhancedDownloadManager
@Inject
lateinit var progressManager: ProgressManager
private lateinit var progressDialog: CustomProgressDialog
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_download)
progressDialog = CustomProgressDialog(this)
progressDialog.setOnCancelClickListener {
// 取消下载
enhancedDownloadManager.cancelDownload(currentTaskId)
}
progressDialog.setOnRetryClickListener {
// 重试下载
enhancedDownloadManager.resumeDownload(currentTaskId)
}
progressDialog.setOnOpenClickListener {
// 打开文件
openDownloadedFile()
}
// 观察进度状态
progressManager.progressState.observe(this) { state ->
progressDialog.updateState(state)
}
// 启动下载
findViewById<Button>(R.id.btnDownload).setOnClickListener {
val url = "https://example.com/large-file.zip"
val fileName = "large-file.zip"
enhancedDownloadManager.startDownload(
url = url,
fileName = fileName,
onComplete = { file -> /* 处理完成 */ },
onError = { error -> /* 处理错误 */ }
)
}
}
private fun openDownloadedFile() {
// 实现文件打开逻辑
}
}
8. 错误处理机制
8.1 统一错误处理
sealed class NetworkResult<T> {
data class Success<T>(val data: T) : NetworkResult<T>()
data class Error<T>(val message: String, val code: Int? = null) : NetworkResult<T>()
class Loading<T> : NetworkResult<T>()
}
class NetworkBoundResource<T>(
private val query: () -> LiveData<T>,
private val fetch: suspend () -> T,
private val saveFetchResult: suspend (T) -> Unit,
private val shouldFetch: (T) -> Boolean = { true }
) {
fun asLiveData(): LiveData<NetworkResult<T>> = liveData {
emit(NetworkResult.Loading())
val dbValue = query().value
if (shouldFetch(dbValue)) {
try {
val fetchedValue = fetch()
saveFetchResult(fetchedValue)
emit(NetworkResult.Success(fetchedValue))
} catch (e: Exception) {
emit(NetworkResult.Error(e.message ?: "Unknown error"))
}
} else {
emit(NetworkResult.Success(dbValue))
}
}
}
8.2 网络状态监听
class NetworkStateMonitor @Inject constructor(
private val context: Context
) {
private val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
fun isNetworkAvailable(): Boolean {
val network = connectivityManager.activeNetwork
val capabilities = connectivityManager.getNetworkCapabilities(network)
return capabilities?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) == true
}
fun getNetworkType(): String {
val network = connectivityManager.activeNetwork
val capabilities = connectivityManager.getNetworkCapabilities(network)
return when {
capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true -> "WiFi"
capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true -> "Cellular"
else -> "Unknown"
}
}
}
9. 性能优化
9.1 连接池与缓存优化
private fun createOptimizedOkHttpClient(config: NetworkConfig): OkHttpClient {
return OkHttpClient.Builder().apply {
// 连接池配置
connectionPool(ConnectionPool(5, 5, TimeUnit.MINUTES))
// 超时配置
connectTimeout(config.timeout, TimeUnit.SECONDS)
readTimeout(config.timeout, TimeUnit.SECONDS)
writeTimeout(config.timeout, TimeUnit.SECONDS)
// 缓存配置
cache(Cache(File(context.cacheDir, "http_cache"), 10 * 1024 * 1024))
// 压缩
addInterceptor { chain ->
val request = chain.request().newBuilder()
.header("Accept-Encoding", "gzip, deflate")
.build()
chain.proceed(request)
}
}.build()
}
9.2 内存优化
class MemoryOptimizedDownloader {
private val bufferSize = 8192
private val maxMemoryUsage = 50 * 1024 * 1024 // 50MB
suspend fun downloadWithMemoryOptimization(
url: String,
fileName: String,
onProgress: (Int) -> Unit
) {
val file = File(fileName)
val totalSize = getFileSize(url)
var downloadedBytes = 0L
val buffer = ByteArray(bufferSize)
val inputStream = getInputStream(url)
val outputStream = FileOutputStream(file)
try {
while (true) {
val bytesRead = inputStream.read(buffer)
if (bytesRead == -1) break
outputStream.write(buffer, 0, bytesRead)
downloadedBytes += bytesRead
val progress = ((downloadedBytes * 100) / totalSize).toInt()
onProgress(progress)
// 内存使用检查
if (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory() > maxMemoryUsage) {
System.gc()
}
}
} finally {
inputStream.close()
outputStream.close()
}
}
}
9.3 并发控制
class ConcurrentDownloadManager {
private val downloadSemaphore = Semaphore(3) // 最多3个并发下载
private val downloadJobs = mutableMapOf<String, Job>()
suspend fun startDownload(
url: String,
fileName: String,
onProgress: (Int) -> Unit,
onComplete: (File) -> Unit,
onError: (String) -> Unit
) {
downloadSemaphore.acquire()
try {
val job = CoroutineScope(Dispatchers.IO).launch {
try {
downloadFile(url, fileName, onProgress, onComplete)
} catch (e: Exception) {
onError(e.message ?: "Download failed")
} finally {
downloadSemaphore.release()
}
}
downloadJobs[fileName] = job
} catch (e: Exception) {
downloadSemaphore.release()
onError(e.message ?: "Failed to start download")
}
}
fun cancelDownload(fileName: String) {
downloadJobs[fileName]?.cancel()
downloadJobs.remove(fileName)
}
}
10. 最佳实践
10.1 代码组织
- 分层清晰:UI、ViewModel、Repository、Network、Data各层职责明确
- 依赖注入:使用Hilt统一管理依赖,便于测试和替换
- 单一职责:每个类只负责一个功能,便于维护和扩展
- 接口隔离:使用多个专门的接口,避免大而全的接口
10.2 错误处理
- 统一错误格式:所有错误都有明确的错误码和错误信息
- 分级处理:网络错误、业务错误、系统错误分别处理
- 用户友好:错误信息对用户友好,便于理解和操作
- 重试机制:关键操作支持自动重试
10.3 性能优化
- 连接复用:使用OkHttp连接池复用连接
- 缓存策略:合理使用HTTP缓存和本地缓存
- 内存管理:大文件下载时分片处理,避免OOM
- 并发控制:限制并发数量,避免资源争抢
10.4 用户体验
- 进度反馈:所有耗时操作都有进度反馈
- 操作可控:支持取消、暂停、恢复等操作
- 状态清晰:用户能清楚知道当前操作的状态
- 错误友好:错误信息对用户友好,提供解决建议
10.5 扩展性
- 多BaseUrl支持:支持动态切换不同的服务器
- 插件化设计:拦截器、下载器等都支持插件化扩展
- 配置化:网络配置、下载配置等都支持动态配置
- 版本兼容:支持不同版本的API和协议
11. 扩展功能
11.1 上传功能
class UploadManager @Inject constructor(
private val networkManager: NetworkManager,
private val fileManager: FileManager
) {
suspend fun uploadFile(
file: File,
uploadUrl: String,
onProgress: (Int) -> Unit,
onComplete: (UploadResponse) -> Unit,
onError: (String) -> Unit
) {
try {
val requestBody = RequestBody.create("multipart/form-data".toMediaTypeOrNull(), file)
val multipartBody = MultipartBody.Part.createFormData("file", file.name, requestBody)
val uploadService = networkManager.getRetrofit(NetworkType.UPLOAD_SERVER)
.create(UploadService::class.java)
val response = uploadService.uploadFile(multipartBody, RequestBody.create("text/plain".toMediaTypeOrNull(), ""))
if (response.isSuccessful) {
onComplete(response.body()!!)
} else {
onError("Upload failed: ${response.code()}")
}
} catch (e: Exception) {
onError(e.message ?: "Upload failed")
}
}
}
11.2 任务队列
class DownloadTaskQueue @Inject constructor() {
private val taskQueue = mutableListOf<DownloadTask>()
private val isProcessing = AtomicBoolean(false)
fun addTask(task: DownloadTask) {
taskQueue.add(task)
if (!isProcessing.get()) {
processNextTask()
}
}
private fun processNextTask() {
if (taskQueue.isEmpty()) {
isProcessing.set(false)
return
}
isProcessing.set(true)
val task = taskQueue.removeAt(0)
// 执行下载任务
// ...
// 处理下一个任务
processNextTask()
}
}
11.3 数据库持久化
@Entity(tableName = "download_tasks")
data class DownloadTaskEntity(
@PrimaryKey val id: String,
val url: String,
val fileName: String,
val filePath: String,
val downloadedBytes: Long,
val totalBytes: Long,
val state: String, // 序列化的状态
val createdAt: Long = System.currentTimeMillis()
)
@Dao
interface DownloadTaskDao {
@Query("SELECT * FROM download_tasks")
suspend fun getAllTasks(): List<DownloadTaskEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTask(task: DownloadTaskEntity)
@Update
suspend fun updateTask(task: DownloadTaskEntity)
@Delete
suspend fun deleteTask(task: DownloadTaskEntity)
}
12. 总结
12.1 技术亮点
- 现代Android开发最佳实践:协程+LiveData+Hilt+分层架构
- 高可扩展性:多BaseUrl、多服务类型、动态配置
- 极致用户体验:断点续传、多线程下载、进度框交互
- 健壮性:完善的错误处理、重试机制、状态管理
- 易维护:分层清晰、依赖注入、单一职责
12.2 应用场景
- 普通API请求:RESTful接口调用
- 文件上传/下载:大文件传输,支持断点续传
- 多服务器架构:API、CDN、上传、下载服务分离
- 离线缓存:本地数据库缓存,支持离线访问
- 进度可视化:实时进度反馈,用户操作可控
12.3 团队协作
- 统一接口:所有网络请求都通过统一的接口
- 统一状态管理:进度、错误、取消等状态统一管理
- 统一错误处理:所有错误都有统一的处理方式
- 统一配置:网络配置、下载配置等统一管理
12.4 未来扩展
- WebSocket支持:实时通信功能
- GraphQL支持:更灵活的API查询
- 离线同步:支持离线操作,网络恢复后同步
- 智能缓存:根据网络状况智能调整缓存策略
- 性能监控:网络性能监控和分析
13. 常见问题与答疑
13.1 断点续传相关
Q: 服务器不支持Range请求怎么办?
A: 降级为普通下载,不支持断点续传功能。
Q: 断点信息如何持久化?
A: 使用数据库或本地文件记录下载进度。
Q: 如何防止文件损坏?
A: 下载完成前使用.tmp后缀,合并后重命名。
13.2 多线程下载相关
Q: 进度如何统计?
A: 每个chunk单独统计,合并后汇总。
Q: 失败重试如何处理?
A: 每个chunk可单独重试,不影响其他chunk。
Q: 合并文件如何保证原子性?
A: 合并后校验MD5,确保文件完整性。
13.3 性能优化相关
Q: 内存使用过高怎么办?
A: 使用分片下载,流式写入,定期GC。
Q: 网络请求频繁怎么办?
A: 使用连接池复用连接,合理使用缓存。
Q: 并发下载过多怎么办?
A: 使用信号量控制并发数量。
13.4 用户体验相关
Q: 进度不流畅怎么办?
A: 使用主线程post更新,避免频繁UI更新。
Q: 进度丢失怎么办?
A: 断点续传时恢复进度,持久化进度信息。
Q: 多任务进度如何管理?
A: 使用Map<TaskId, State>管理多个任务状态。
14. 结语
通过本套网络框架封装,开发者可以专注于业务逻辑,无需重复造轮子,极大提升开发效率和App专业度。
核心价值:
- 提升开发效率
- 改善用户体验
- 增强代码可维护性
- 支持业务快速扩展
技术特色:
- 现代化架构设计
- 完善的错误处理
- 友好的用户交互
- 高性能的网络请求