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

Android Clean Code MVVM架构下Foreground Service结合通话状态变更的实时通话日志展示问题

Android Clean Code MVVM架构下Foreground Service结合通话状态变更的实时通话日志展示问题

嘿,我来帮你梳理一下这个问题的解决方案,完全贴合你的Clean Code MVVM架构要求,核心思路是通过事件驱动+解耦的组件通信来实现实时更新,避免硬耦合的“直来直去”写法。

首先,我们先明确你的现有流程:通话状态变更→广播接收器触发Service→Service调用同步UseCase→同步本地通话日志到远程API。现在需要在同步完成后,自动拉取API最新通话日志并更新UI,同时要符合Clean架构的单一职责、依赖倒置原则。

第一步:定义事件契约(解耦组件通信)

为了让Service/UseCase层和ViewModel层之间不直接依赖,我们先定义一个密封类来标准化事件类型,这样所有组件都基于这个契约通信,避免硬编码:

sealed class CallLogEvents {
    object SyncCompleted : CallLogEvents() // 同步完成事件
    data class SyncFailed(val errorMsg: String?) : CallLogEvents() // 可选:同步失败事件,用于错误处理
}

第二步:实现事件总线(事件发布/订阅中心)

用Kotlin协程的SharedFlow做一个轻量的事件总线(比传统EventBus更符合Kotlin生态,也更易测试),它负责在各个组件之间传递事件:

class CallLogEventBus {
    // MutableSharedFlow用于发布事件,对外暴露只读的SharedFlow
    private val _events = MutableSharedFlow<CallLogEvents>()
    val events: SharedFlow<CallLogEvents> = _events

    // 发布事件的方法,用suspend确保协程安全
    suspend fun postEvent(event: CallLogEvents) {
        _events.emit(event)
    }
}

注意:这个类最好通过依赖注入(比如Hilt)提供单例,避免重复创建实例。

第三步:改造同步UseCase,添加事件发布

在你的SyncCallLogUseCase中,完成同步逻辑后,通过事件总线发布“同步完成”事件——这里要依赖注入事件总线,符合依赖倒置原则:

class SyncCallLogUseCase(
    private val callLogRepository: CallLogRepository,
    private val eventBus: CallLogEventBus // 构造注入事件总线
) {
    suspend operator fun invoke() {
        try {
            // 执行你的同步逻辑:上传本地通话日志到远程API
            callLogRepository.syncRemoteCallLog()
            // 同步成功,发布事件
            eventBus.postEvent(CallLogEvents.SyncCompleted)
        } catch (e: Exception) {
            // 可选:同步失败时发布错误事件,方便UI层提示
            eventBus.postEvent(CallLogEvents.SyncFailed(e.message))
        }
    }
}

第四步:ViewModel监听事件,触发数据拉取

ViewModel需要监听事件总线的同步完成事件,一旦收到就调用拉取远程通话日志的UseCase,然后更新UI数据源:

class CallLogViewModel(
    private val getRemoteCallLogUseCase: GetRemoteCallLogUseCase, // 拉取API日志的UseCase
    private val eventBus: CallLogEventBus,
    private val coroutineScope: CoroutineScope = viewModelScope
) : ViewModel() {
    // 供UI观察的通话日志LiveData(也可以用StateFlow更贴合Kotlin协程)
    private val _callLogList = MutableLiveData<List<CallLog>>()
    val callLogList: LiveData<List<CallLog>> = _callLogList

    // 可选:错误提示LiveData
    private val _errorMsg = MutableLiveData<String>()
    val errorMsg: LiveData<String> = _errorMsg

    init {
        // 初始化时先拉一次数据,确保UI有初始内容
        fetchLatestCallLogs()
        // 监听事件总线的事件
        coroutineScope.launch {
            eventBus.events.collect { event ->
                when (event) {
                    CallLogEvents.SyncCompleted -> fetchLatestCallLogs()
                    is CallLogEvents.SyncFailed -> _errorMsg.postValue(event.errorMsg ?: "同步通话日志失败")
                }
            }
        }
    }

    // 拉取最新远程通话日志的私有方法
    private suspend fun fetchLatestCallLogs() {
        val latestLogs = getRemoteCallLogUseCase()
        _callLogList.postValue(latestLogs)
    }
}

第五步:UI层观察ViewModel数据,更新界面

在你的Activity/Fragment中,观察ViewModel的callLogList,一旦数据变化就更新UI(比如RecyclerView的Adapter):

class CallLogFragment : Fragment() {
    private lateinit var viewModel: CallLogViewModel
    private val callLogAdapter = CallLogAdapter() // 你的RecyclerView适配器

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // 初始化ViewModel(如果用Hilt可以用by viewModels())
        viewModel = ViewModelProvider(this)[CallLogViewModel::class.java]
        
        // 观察通话日志数据,更新Adapter
        viewModel.callLogList.observe(viewLifecycleOwner) { logs ->
            callLogAdapter.submitList(logs)
        }

        // 可选:观察错误提示,显示Snackbar/Toast
        viewModel.errorMsg.observe(viewLifecycleOwner) { msg ->
            Snackbar.make(view, msg, Snackbar.LENGTH_SHORT).show()
        }
    }
}

第六步:保持原有Service和广播接收器逻辑

你的CallLogSyncService和PhoneStateReceiver逻辑基本不变,只需要确保Service通过依赖注入拿到SyncCallLogUseCase,在收到广播触发时调用即可:

class CallLogSyncService : Service() {
    // 用依赖注入初始化UseCase(比如Hilt的@Inject)
    @Inject lateinit var syncCallLogUseCase: SyncCallLogUseCase

    override fun onCreate() {
        super.onCreate()
        // 如果用Hilt,这里需要初始化注入,比如DaggerCallLogComponent.create().inject(this)
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        // 启动协程执行同步逻辑
        lifecycleScope.launch {
            syncCallLogUseCase()
        }
        // 注意:Android O及以上需要启动前台Service,避免被系统杀死
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            startForeground(NOTIFICATION_ID, createForegroundNotification())
        }
        return START_NOT_STICKY
    }

    // 生成前台Service通知的方法(省略实现)
    private fun createForegroundNotification(): Notification {
        // ...
    }

    override fun onBind(intent: Intent?): IBinder? = null
}

为什么这样符合Clean Code?

  1. 单一职责:每个组件只做一件事——事件总线管事件传递,UseCase管业务逻辑,ViewModel管UI数据流转,Service管后台任务。
  2. 依赖倒置:所有依赖都是通过构造注入,没有硬编码创建实例,方便单元测试(比如可以给UseCase传入Mock的Repo和EventBus)。
  3. 解耦:Service/UseCase和ViewModel之间没有直接依赖,通过事件契约通信,后续修改任何一方都不会影响另一方。
  4. 可维护性:整个流程清晰,每个环节的职责明确,新人接手也能快速理解。

备注:内容来源于stack exchange,提问作者StonedCodingTom

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.21 12:19:31