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

Kotlin MVVM蓝牙应用运行时权限请求及自动扫描问题求助

MVVM架构下蓝牙扫描权限与自动扫描的正确实现方案

核心问题分析

你遇到的卡顿和崩溃,本质是权限请求、蓝牙状态检查与扫描操作的时序混乱,加上在生命周期高频回调(onResume)里执行耗时操作导致的。下面结合MVVM的分层思想给出具体实现方案:


1. 权限请求与前置检查的正确时机

  • 放弃在onResume()里直接执行扫描:onResume会频繁触发(比如切后台再回来),重复执行扫描逻辑必然导致卡顿。
  • 统一在首屏初始化完成后(比如Fragment的onViewCreated)触发前置检查,确保权限、蓝牙状态就绪后再启动扫描。
  • 用Jetpack的ActivityResultContracts处理权限请求,替代旧的requestPermissions,避免回调时序混乱。

2. MVVM分层实现代码示例

(1)View层(MainFragment)

负责触发检查、接收状态回调、请求权限/蓝牙开启:

class MainFragment : Fragment() {
    private val viewModel: BluetoothViewModel by viewModels()
    // 权限请求Launcher
    private val requestBluetoothPermissionsLauncher = registerForActivityResult(
        ActivityResultContracts.RequestMultiplePermissions()
    ) { permissions ->
        val allGranted = permissions.all { it.value }
        if (allGranted) {
            viewModel.checkBluetoothEnabled()
        } else {
            Toast.makeText(requireContext(), "蓝牙权限未开启,无法扫描设备", Toast.LENGTH_SHORT).show()
        }
    }

    // 请求开启蓝牙的Launcher
    private val enableBluetoothLauncher = registerForActivityResult(
        ActivityResultContracts.StartActivityForResult()
    ) { result ->
        if (result.resultCode == Activity.RESULT_OK) {
            viewModel.startBluetoothScan()
        } else {
            Toast.makeText(requireContext(), "蓝牙未开启,无法扫描设备", Toast.LENGTH_SHORT).show()
        }
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // 首屏初始化完成后,触发前置检查
        viewModel.initBluetoothCheck()

        // 观察ViewModel的权限检查结果
        viewModel.permissionCheckResult.observe(viewLifecycleOwner) { needRequest ->
            if (needRequest) {
                // 按需请求权限(Android 12+需要BLUETOOTH_SCAN,低版本用BLUETOOTH)
                val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                    arrayOf(Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT)
                } else {
                    arrayOf(Manifest.permission.BLUETOOTH, Manifest.permission.BLUETOOTH_ADMIN)
                }
                requestBluetoothPermissionsLauncher.launch(permissions)
            } else {
                viewModel.checkBluetoothEnabled()
            }
        }

        // 观察ViewModel的蓝牙开启请求
        viewModel.enableBluetoothEvent.observe(viewLifecycleOwner) {
            val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
            enableBluetoothLauncher.launch(enableBtIntent)
        }

        // 观察扫描结果
        viewModel.scanResults.observe(viewLifecycleOwner) { devices ->
            // 更新UI显示扫描到的设备
        }
    }
}

(2)ViewModel层(BluetoothViewModel)

封装所有蓝牙相关逻辑,确保扫描操作仅在前置条件满足时执行:

class BluetoothViewModel : ViewModel() {
    private val _permissionCheckResult = MutableLiveData<Boolean>()
    val permissionCheckResult: LiveData<Boolean> = _permissionCheckResult

    private val _enableBluetoothEvent = MutableLiveData<Unit>()
    val enableBluetoothEvent: LiveData<Unit> = _enableBluetoothEvent

    private val _scanResults = MutableLiveData<List<BluetoothDevice>>()
    val scanResults: LiveData<List<BluetoothDevice>> = _scanResults

    private val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter()
    private var scanCallback: ScanCallback? = null

    fun initBluetoothCheck() {
        // 先检查权限是否已授予
        val hasPermissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            ContextCompat.checkSelfPermission(
                getApplication(),
                Manifest.permission.BLUETOOTH_SCAN
            ) == PackageManager.PERMISSION_GRANTED &&
            ContextCompat.checkSelfPermission(
                getApplication(),
                Manifest.permission.BLUETOOTH_CONNECT
            ) == PackageManager.PERMISSION_GRANTED
        } else {
            ContextCompat.checkSelfPermission(
                getApplication(),
                Manifest.permission.BLUETOOTH
            ) == PackageManager.PERMISSION_GRANTED &&
            ContextCompat.checkSelfPermission(
                getApplication(),
                Manifest.permission.BLUETOOTH_ADMIN
            ) == PackageManager.PERMISSION_GRANTED
        }
        _permissionCheckResult.postValue(!hasPermissions)
    }

    fun checkBluetoothEnabled() {
        if (bluetoothAdapter == null) {
            Toast.makeText(getApplication(), "设备不支持蓝牙", Toast.LENGTH_SHORT).show()
            return
        }
        if (!bluetoothAdapter.isEnabled) {
            _enableBluetoothEvent.postValue(Unit)
        } else {
            startBluetoothScan()
        }
    }

    fun startBluetoothScan() {
        if (bluetoothAdapter == null) return
        // 避免重复扫描
        stopScan()
        // 扫描操作放在IO线程,避免阻塞主线程
        viewModelScope.launch(Dispatchers.IO) {
            scanCallback = object : ScanCallback() {
                override fun onScanResult(callbackType: Int, result: ScanResult) {
                    super.onScanResult(callbackType, result)
                    val currentList = _scanResults.value.orEmpty().toMutableList()
                    if (!currentList.contains(result.device)) {
                        currentList.add(result.device)
                        _scanResults.postValue(currentList)
                    }
                }

                override fun onScanFailed(errorCode: Int) {
                    super.onScanFailed(errorCode)
                    // 处理扫描失败逻辑
                }
            }
            // 启动扫描(Android 12+需要设置ScanSettings)
            val scanSettings = ScanSettings.Builder().build()
            bluetoothAdapter.startScan(null, scanSettings, scanCallback)
        }
    }

    private fun stopScan() {
        scanCallback?.let {
            bluetoothAdapter?.stopScan(it)
            scanCallback = null
        }
    }

    override fun onCleared() {
        super.onCleared()
        stopScan()
    }
}

3. 关键注意事项

  • 避免主线程阻塞:扫描操作必须放在IO线程执行,ViewModel用viewModelScope.launch(Dispatchers.IO)保证。
  • 时序严格性:必须遵循「权限检查→蓝牙状态检查→启动扫描」的顺序,任何一步不满足都不能触发扫描。
  • 资源释放:在ViewModel销毁时停止扫描,避免内存泄漏;同时不要重复启动扫描,每次启动前先停止之前的扫描。
  • 版本兼容:Android 12(API 31)之后的蓝牙权限和低版本不同,要做版本判断。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 22:00:58