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

Jetpack Compose中避免生命周期相关函数重复调用的方案咨询

解决Jetpack Compose中handleBluetoothScanState重复调用的方案

核心问题分析

当前代码中,viewModel.handleBluetoothScanState被两个独立逻辑触发:

  • ComposableLifecycle的ON_RESUME事件回调
  • 监听bluetoothEnableState变化的LaunchedEffect

这两个触发源在场景重叠时(比如启动时蓝牙已开启、恢复前台时蓝牙状态刚好变化)会导致函数重复执行,进而重复启动/停止扫描逻辑。


方案1:统一触发逻辑,移除生命周期中的重复调用

修改思路

保留LaunchedEffect监听蓝牙状态变化的逻辑,同时将生命周期ON_RESUME的触发逻辑合并到LaunchedEffect中,通过组合key确保两种场景都能触发,但避免重复执行。

修改后的BluetoothConnectionContentStateful代码

@Composable
fun BluetoothConnectionContentStateful(
    context: Context = LocalContext.current,
    viewModel: PairViewModel = getViewModel()
) {
    var selectedIndexOfAvailableItem by remember { mutableStateOf(DEVICE_NOT_SELECTED_INDEX) }
    val rememberPairScreenState = rememberConnectionScreenState(context, viewModel)
    val bluetoothEnableState by remember { derivedStateOf { viewModel.isBluetoothEnabled } }
    
    // 监听当前是否处于前台状态
    var isInForeground by remember { mutableStateOf(true) }
    ComposableLifecycle { _, event ->
        isInForeground = event == Lifecycle.Event.ON_RESUME
        if (event == Lifecycle.Event.ON_PAUSE) {
            viewModel.stopScan(rememberPairScreenState.bluetoothAdapter)
        }
    }

    // 组合蓝牙状态和前台状态作为key,任一变化时触发
    LaunchedEffect(key1 = bluetoothEnableState, key2 = isInForeground) {
        // 仅当处于前台时处理扫描状态
        if (isInForeground) {
            viewModel.handleBluetoothScanState(rememberPairScreenState.bluetoothAdapter)
        }
    }

    LaunchedEffect(key1 = selectedIndexOfAvailableItem) {
        viewModel.handleTimeWarning(selectedIndexOfAvailableItem)
    }
}

方案优势

  • 统一了触发逻辑,避免两个独立源重复调用
  • 仅在前台状态下处理扫描逻辑,符合生命周期规范
  • 组合key确保蓝牙状态变化或从后台恢复时都会触发正确的扫描逻辑

方案2:ViewModel层添加防抖逻辑

修改思路

如果希望保留原有触发逻辑,可在ViewModel内部对handleBluetoothScanState添加防抖,确保相同蓝牙状态下不会重复执行扫描操作。

修改后的PairViewModel代码

class PairViewModel : BaseViewModel() {

    companion object {
        private const val WARNING_PERIOD_IN_MILLES: Long = 60000 // For 1 minute scan
        const val DEVICE_NOT_SELECTED_INDEX = -1
    }

    var isBluetoothEnabled by mutableStateOf(false)
        private set
        
    // 记录上次处理的蓝牙状态,初始值设为与当前状态相反
    private var lastHandledBluetoothState = !isBluetoothEnabled

    private var job = Job()
        get() {
            if (field.isCancelled) field = Job()
            return field
        }

    // ... 其他已有代码 ...

    fun handleBluetoothScanState(bluetoothAdapter: BluetoothAdapter) {
        // 防抖:如果当前状态与上次处理的一致,直接返回
        if (isBluetoothEnabled == lastHandledBluetoothState) {
            return
        }
        lastHandledBluetoothState = isBluetoothEnabled
        
        if (isBluetoothEnabled) {
            startScan(bluetoothAdapter)
        } else {
            cancelTimeWarning()
            stopScan(bluetoothAdapter)
        }
    }

    // ... 其他已有代码 ...
}

方案优势

  • 无需修改Composable层的触发逻辑,侵入性小
  • 从根源上避免相同状态下的重复执行,适用于多源触发的场景

方案3:移除LaunchedEffect,统一通过生命周期+蓝牙广播监听处理

修改思路

将蓝牙状态监听移到ViewModel层,结合生命周期回调统一处理扫描逻辑,完全避免Composable层的重复触发。

核心修改示例(ViewModel部分)

class PairViewModel(
    private val context: Context
) : BaseViewModel(), LifecycleObserver {

    // ... 已有代码 ...

    private val bluetoothReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            val adapter = BluetoothAdapter.getDefaultAdapter()
            isBluetoothEnabled = adapter?.isEnabled == true
            // 仅当处于前台时处理扫描
            if (isInForeground) {
                handleBluetoothScanState(adapter)
            }
        }
    }

    private var isInForeground = false

    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    fun onResume() {
        isInForeground = true
        val adapter = BluetoothAdapter.getDefaultAdapter()
        handleBluetoothScanState(adapter)
        // 注册蓝牙广播监听
        context.registerReceiver(bluetoothReceiver, IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED))
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
    fun onPause() {
        isInForeground = false
        val adapter = BluetoothAdapter.getDefaultAdapter()
        stopScan(adapter)
        // 注销广播
        context.unregisterReceiver(bluetoothReceiver)
    }

    // ... 其他已有代码 ...
}

方案优势

  • 逻辑完全收敛到ViewModel层,符合MVVM架构规范
  • 避免Composable层的状态监听与生命周期回调冲突
  • 更精准地控制扫描的启动与停止时机

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.10 19:10:35