You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何向Composable传递前台服务的实时运行进度?

问题:前台服务向UI实时传递进度的正确实现方式

我想实现前台服务运行时向UI返回实时进度,目前尝试用共享Hilt ViewModel传递进度百分比和结果,但在Service中注入ViewModel时遇到错误:

Injection of an @HiltViewModel class is prohibited since it does not create a ViewModel instance correctly.
Access the ViewModel via the Android APIs (e.g. ViewModelProvider) instead.

我的代码

Composable 代码

Button(onClick = {
    val intent = Intent(context, TestService::class.java).also {
        it.action = TestService.Actions.START.toString()
        it.putExtra("imageUri", imageUri) // 传递图片URI给服务
    }

    ContextCompat.startForegroundService(context, intent)
}) {
    Text(text = "Start Service")
}

ProgressBar()


@Composable
fun ProgressBar() {
    val serviceViewModel: ServiceViewModel = viewModel()

    val progress by serviceViewModel.progress.collectAsState()
    val result by serviceViewModel.result.collectAsState()

    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        LinearProgressIndicator(
            progress = { progress },
        ) // 使用状态
        Text("Progress: ${progress}%")
    }
}

Service 代码

@AndroidEntryPoint
class TestService : Service(){


    @Inject
    lateinit var serviceViewModel: ServiceViewModel

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        when(intent?.action) {
            Actions.STOP.toString() -> stopSelf()
            Actions.START.toString() -> start(intent)
        }
        return START_NOT_STICKY 
    }
//.... 其他代码

    val progressCallback: (Float) -> Unit = { progress ->
        serviceViewModel.updateProgress(progress)
    }

//.... 调用progressCallback

ViewModel 代码

@HiltViewModel
class ServiceViewModel @Inject constructor() : ViewModel(){
    private val _progress = MutableStateFlow(0f)
    val progress: StateFlow<Float> = _progress.asStateFlow()

    private val _result = MutableStateFlow<Extracted?>(null)
    val result: StateFlow<Extracted?> = _result.asStateFlow()

    fun updateProgress(newProgress: Float) {
        viewModelScope.launch { 
            _progress.value = newProgress
        }
    }

    fun updateResult(newResult: Extracted) {
        viewModelScope.launch {
            _result.value = newResult
        }
    }
}

已尝试的方案

  • 使用ViewModelProvider(this)[ServiceViewModel::class.java],但方法已过时,且运行时会创建多个ViewModel实例
  • 尝试广播接收器,但Composable无法接收广播

解决方案

核心问题

Hilt的@HiltViewModel是专为UI组件(Activity/Fragment/Composable)设计的,必须通过ViewModelProvider获取,直接在Service中注入会破坏ViewModel的生命周期管理逻辑,导致实例不共享或生命周期异常。

正确实现方式:单例数据流容器 + SharedFlow

用独立的单例数据流容器在Service和UI之间传递进度,避开ViewModel的生命周期限制。

步骤1:创建单例进度管理类

class ProgressManager @Inject constructor() {
    // replay=1确保新订阅者能获取最新值
    private val _progressFlow = MutableSharedFlow<Float>(replay = 1)
    val progressFlow: SharedFlow<Float> = _progressFlow.asSharedFlow()

    private val _resultFlow = MutableSharedFlow<Extracted?>(replay = 1)
    val resultFlow: SharedFlow<Extracted?> = _resultFlow.asSharedFlow()

    suspend fun updateProgress(progress: Float) {
        _progressFlow.emit(progress)
    }

    suspend fun updateResult(result: Extracted) {
        _resultFlow.emit(result)
    }

    // 可选:任务结束后重置状态
    suspend fun reset() {
        _progressFlow.emit(0f)
        _resultFlow.emit(null)
    }
}

给这个类添加Hilt单例绑定,在AppModule中:

@Module
@InstallIn(SingletonComponent::class)
object AppModule {
    @Provides
    @Singleton
    fun provideProgressManager(): ProgressManager {
        return ProgressManager()
    }
}

步骤2:修改Service注入ProgressManager

@AndroidEntryPoint
class TestService : Service(){

    @Inject
    lateinit var progressManager: ProgressManager

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        when(intent?.action) {
            Actions.STOP.toString() -> stopSelf()
            Actions.START.toString() -> start(intent)
        }
        return START_NOT_STICKY 
    }

    private fun start(intent: Intent) {
        // 启动前台服务逻辑(略)
        val imageUri = intent.getParcelableExtra<Uri>("imageUri")
        lifecycleScope.launch {
            // 模拟进度更新逻辑
            for (i in 0..100) {
                progressManager.updateProgress(i.toFloat())
                delay(100)
            }
            // 任务完成后发送结果
            progressManager.updateResult(Extracted())
        }
    }

    // 其他代码...
}

步骤3:在Composable中监听数据流

@Composable
fun ProgressBar(progressManager: ProgressManager) {
    val progress by progressManager.progressFlow.collectAsState(initial = 0f)
    val result by progressManager.resultFlow.collectAsState(initial = null)

    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        LinearProgressIndicator(progress = progress / 100f)
        Text("Progress: ${progress.toInt()}%")
        result?.let {
            Text("任务完成:${it.toString()}")
        }
    }
}

// 在主Composable中注入并传递ProgressManager
@Composable
fun MainScreen() {
    val context = LocalContext.current
    val progressManager = remember {
        (context.applicationContext as MyApplication).appComponent.provideProgressManager()
    }

    Button(onClick = {
        val intent = Intent(context, TestService::class.java).also {
            it.action = TestService.Actions.START.toString()
            // it.putExtra("imageUri", imageUri)
        }
        ContextCompat.startForegroundService(context, intent)
    }) {
        Text(text = "启动服务")
    }

    ProgressBar(progressManager = progressManager)
}

方案优势

  1. SharedFlow天然支持跨组件通信,Service和UI共享同一个数据流实例
  2. 单例ProgressManager由Hilt管理生命周期,确保全局唯一
  3. 完全符合Android组件设计规范,避开ViewModel的使用限制

替代方案:WorkManager(一次性任务场景)

如果你的前台服务是执行一次性耗时任务,推荐用WorkManager,它自带进度更新API,UI可通过WorkInfo监听进度,更贴合Jetpack组件最佳实践。


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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 09:10:55