You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Android中RxJava+Retrofit下载:页面销毁后仍监听完成的实现方案

嘿,刚好我在Android项目里折腾过类似的需求,用RxJava+Retrofit实现后台下载,就算Activity销毁了也能拿到完成状态,给你分享下具体的实现思路和代码示例~

核心思路

问题的关键在于把下载任务的执行和UI的监听解耦——Activity销毁时只取消UI层的订阅,而下载任务本身继续在后台运行;当Activity重新创建(或其他页面需要监听)时,能重新订阅到下载状态的更新。

方案一:单例下载任务管理器(全局状态分发)

这个方案适合需要全局监听下载状态的场景,比如多个页面都要关注同一个下载任务的进度或结果。

1. 实现单例DownloadManager

我们用一个单例类来管理下载任务,内部用PublishSubject来分发下载状态(进度、成功、失败),同时维护下载任务的Disposable确保任务能在后台持续运行:

object DownloadManager {
    // 定义下载状态的密封类,包含进度、成功、失败三种情况
    sealed class DownloadState {
        data class Progress(val progress: Int) : DownloadState()
        data class Success(val filePath: String) : DownloadState()
        data class Error(val throwable: Throwable) : DownloadState()
    }

    private val downloadStateSubject = PublishSubject.create<DownloadState>()
    private var currentDownloadDisposable: Disposable? = null

    // 启动下载的方法
    fun startDownload(fileUrl: String, saveFilePath: String) {
        // 如果已有未完成的下载,先取消避免重复任务
        currentDownloadDisposable?.dispose()

        // 调用Retrofit的下载接口(假设你的API定义了返回ResponseBody的接口)
        val downloadObservable = RetrofitClient.downloadApi.downloadFile(fileUrl)
            .subscribeOn(Schedulers.io()) // 在IO线程执行下载
            .flatMap { response ->
                // 保存文件到本地,并实时发送进度
                saveFileWithProgress(response.body(), saveFilePath)
            }
            .onErrorReturn { DownloadState.Error(it) } // 捕获下载过程中的错误

        // 订阅下载Observable,把结果发送到Subject
        currentDownloadDisposable = downloadObservable.subscribe(
            { state -> downloadStateSubject.onNext(state) },
            { error -> downloadStateSubject.onNext(DownloadState.Error(error)) }
        )
    }

    // 对外提供订阅状态的Observable(用hide()防止外部随意发送事件)
    fun getDownloadStateObservable(): Observable<DownloadState> {
        return downloadStateSubject.hide()
    }

    // 保存文件并实时计算进度的私有方法
    private fun saveFileWithProgress(body: ResponseBody?, savePath: String): Observable<DownloadState> {
        return Observable.create { emitter ->
            body?.let { responseBody ->
                val inputStream = responseBody.byteStream()
                val outputFile = File(savePath)
                val outputStream = FileOutputStream(outputFile)
                val buffer = ByteArray(4096)
                var bytesRead: Int
                var totalBytesRead = 0L
                val totalFileSize = responseBody.contentLength()

                try {
                    while (inputStream.read(buffer).also { bytesRead = it } != -1) {
                        outputStream.write(buffer, 0, bytesRead)
                        totalBytesRead += bytesRead
                        // 计算进度并发送
                        val progress = ((totalBytesRead * 100) / totalFileSize).toInt()
                        emitter.onNext(DownloadState.Progress(progress))
                    }
                    // 保存完成,发送成功状态
                    emitter.onNext(DownloadState.Success(savePath))
                    emitter.onComplete()
                } catch (e: Exception) {
                    emitter.onNext(DownloadState.Error(e))
                    emitter.onComplete()
                } finally {
                    inputStream.close()
                    outputStream.flush()
                    outputStream.close()
                }
            } ?: run {
                emitter.onNext(DownloadState.Error(IllegalArgumentException("ResponseBody is null")))
                emitter.onComplete()
            }
        }
    }
}

2. Activity中使用DownloadManager

在Activity里,我们只需要订阅状态更新,在onDestroy时取消UI层的订阅(但下载任务会继续在DownloadManager中运行):

class DownloadActivity : AppCompatActivity() {
    private var stateSubscription: Disposable? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_download)

        // 点击按钮启动下载
        btn_start_download.setOnClickListener {
            val savePath = getExternalFilesDir(null)?.absolutePath + "/my_download.apk"
            DownloadManager.startDownload("https://example.com/large-file.apk", savePath)
        }

        // 订阅下载状态,更新UI
        stateSubscription = DownloadManager.getDownloadStateObservable()
            .observeOn(AndroidSchedulers.mainThread()) // 切换到主线程更新UI
            .subscribe({ state ->
                when (state) {
                    is DownloadManager.DownloadState.Progress -> {
                        progress_bar.progress = state.progress
                    }
                    is DownloadManager.DownloadState.Success -> {
                        Toast.makeText(this, "下载完成:${state.filePath}", Toast.LENGTH_LONG).show()
                    }
                    is DownloadManager.DownloadState.Error -> {
                        Toast.makeText(this, "下载失败:${state.throwable.message}", Toast.LENGTH_LONG).show()
                    }
                }
            }, { error ->
                // 处理订阅过程中的异常
                Toast.makeText(this, "订阅出错:${error.message}", Toast.LENGTH_SHORT).show()
            })
    }

    override fun onDestroy() {
        super.onDestroy()
        // 取消UI层的订阅,避免内存泄漏,但下载任务不受影响
        stateSubscription?.dispose()
    }
}
方案二:结合ViewModel(利用生命周期优势)

如果你的下载任务只和当前页面关联,用ViewModel更合适——ViewModel的生命周期比Activity长,即使Activity销毁(比如旋转屏幕),ViewModel也会保留,下载任务可以继续运行。

1. 实现DownloadViewModel

class DownloadViewModel : ViewModel() {
    private val downloadStateSubject = PublishSubject.create<DownloadManager.DownloadState>()
    private var downloadDisposable: Disposable? = null

    fun startDownload(fileUrl: String, saveFilePath: String) {
        downloadDisposable?.dispose()
        val downloadObservable = RetrofitClient.downloadApi.downloadFile(fileUrl)
            .subscribeOn(Schedulers.io())
            .flatMap { response ->
                DownloadManager.saveFileWithProgress(response.body(), saveFilePath)
            }
            .onErrorReturn { DownloadManager.DownloadState.Error(it) }

        downloadDisposable = downloadObservable.subscribe(
            { state -> downloadStateSubject.onNext(state) },
            { error -> downloadStateSubject.onNext(DownloadManager.DownloadState.Error(error)) }
        )
    }

    fun getDownloadStateObservable(): Observable<DownloadManager.DownloadState> {
        return downloadStateSubject.hide()
    }

    // ViewModel销毁时取消下载任务
    override fun onCleared() {
        super.onCleared()
        downloadDisposable?.dispose()
    }
}

2. Activity中使用ViewModel

class DownloadActivity : AppCompatActivity() {
    private val viewModel by viewModels<DownloadViewModel>()
    private var stateSubscription: Disposable? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_download)

        btn_start_download.setOnClickListener {
            val savePath = getExternalFilesDir(null)?.absolutePath + "/my_download.apk"
            viewModel.startDownload("https://example.com/large-file.apk", savePath)
        }

        stateSubscription = viewModel.getDownloadStateObservable()
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe({ state ->
                // 处理状态更新,和方案一一致
            }, {})
    }

    override fun onDestroy() {
        super.onDestroy()
        stateSubscription?.dispose()
    }
}
关键注意事项
  1. 内存泄漏防范:一定要在Activity的onDestroy取消UI层的订阅,ViewModel的onCleared取消下载任务的订阅。
  2. 进程被杀的情况:如果App进程被杀,上述方案的下载任务会中断。如果需要持久化的下载(比如重启App后继续),可以结合WorkManager——把下载逻辑放在Worker中,再用RxJava监听Worker的状态变化。
  3. 权限处理:Android 10及以上建议用Scoped Storage保存文件,避免申请WRITE_EXTERNAL_STORAGE权限;如果是旧版本,记得动态申请权限后再启动下载。

内容的提问来源于stack exchange,提问作者Amir Ebrahimi

火山引擎 最新活动