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
相关产品推荐
相关产品推荐

