遵循Android架构规范,数据层中BroadcastReceiver使用困惑
问题描述
我是移动开发新手,正在使用Kotlin和Jetpack Compose开发首个Android应用,功能为扫描可见WiFi接入点并列表展示。为遵循Android官方推荐架构,我划分了UI层(含Composable与ViewModel状态持有者)和数据层(处理业务逻辑)。
当前遇到的核心问题:
- 执行WiFi扫描时需注册BroadcastReceiver并发起扫描请求,但ViewModel和Repository类均无Context,无法完成BroadcastReceiver注册及WifiManager初始化;
- 不清楚如何将扫描结果传递至ViewModel。
附上现有代码实现:
UI层(MainActivity)
class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MyTheme { Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { AccessPointsListScreen() } } } } @Composable fun AccessPointsListScreen( accessPointsViewModel: AccessPointsViewModel = viewModel()) ) { // State to hold the scan results val accessPointsUiState by accessPointsViewModel.uiState.collectAsState() AccessPoints( accessPointsList = accessPointsUiState.accessPointsList ) } @Composable private fun AccessPoints( accessPointsList: List<AccessPoint> ) { // Omit implementation of UI }
ViewModel类
/** * UI state for the Access Points */ data class AccessPointsUiState( val accessPointsList: List<AccessPoint> = emptyList() ) /** * ViewModel that handles the logic of the access points screen */ class AccessPointsViewModel( private val accessPointsRepository: AccessPointsRepository ): ViewModel() { private val _uiState = MutableStateFlow(AccessPointsUiState()) val uiState: StateFlow<AccessPointsUiState> = _uiState.asStateFlow() init { refreshAccessPoints() } /** * Refresh access points and update the UI state */ fun refreshAccessPoints() { viewModelScope.launch { val result = accessPointsRepository.scanAccessPoints() // Omit logic to update state } } }
数据层(Repository类)
class AccessPointsRepository { suspend fun scanAccessPoints(): List<ScanResult> { // How do I proceed here?? val wifiScanReceiver: BroadcastReceiver = WifiScanReceiver() // Should I instantiate this here?? // Register a broadcast listener for SCAN_RESULTS_AVAILABLE_ACTION, which is called when scan requests are completed registerReceiver(wifiScanReceiver, IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) // This is incorrect, I need the context val success = wifiManager.startScan() // This won't work without initializing wifiManager, for which I need the context } } class WifiScanReceiver: BroadcastReceiver() { var scanResultList = mutableListOf<ScanResult>() lateinit var wifiManager: WifiManager // Where do I initialize this if I need the context?? override fun onReceive(context: Context, intent: Intent) { val success = intent.getBooleanExtra(WifiManager.EXTRA_RESULTS_UPDATED, false) if (!success) { // Omit } scanResultList = wifiManager.scanResults // wifiManager is not initialized // Where and how do I return the scanResultList?? } }
符合Android架构规范的解决方案
核心架构原则
- Repository不直接持有Context:Repository仅负责封装业务逻辑,依赖传入的Context(用Application级Context避免内存泄漏)获取系统服务;
- BroadcastReceiver归属于数据流而非UI层:通过
callbackFlow将扫描结果转为Flow,自动处理Receiver的注册/注销,避免手动管理生命周期; - ViewModel用AndroidViewModel获取Context:仅用于初始化Repository,不直接操作系统组件。
分步实现
1. 调整ViewModel为AndroidViewModel
修改ViewModel继承AndroidViewModel,获取Application Context并传入Repository:
class AccessPointsViewModel( application: Application ) : AndroidViewModel(application) { private val accessPointsRepository = AccessPointsRepository(application) private val _uiState = MutableStateFlow(AccessPointsUiState()) val uiState: StateFlow<AccessPointsUiState> = _uiState.asStateFlow() init { refreshAccessPoints() } fun refreshAccessPoints() { viewModelScope.launch { accessPointsRepository.scanAccessPoints().collect { scanResults -> _uiState.update { it.copy(accessPointsList = scanResults.map { scanResult -> // 转换为UI层需要的AccessPoint实体 AccessPoint( ssid = scanResult.SSID, bssid = scanResult.BSSID, signalStrength = scanResult.level ) }) } } } } }
2. 重构Repository用Flow封装扫描逻辑
使用callbackFlow将BroadcastReceiver的结果转为Flow,自动处理注册/注销:
class AccessPointsRepository(private val context: Context) { private val wifiManager by lazy { context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager } fun scanAccessPoints(): Flow<List<ScanResult>> = callbackFlow { // 临时Receiver,仅用于本次扫描 val scanReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { if (intent.action == WifiManager.SCAN_RESULTS_AVAILABLE_ACTION) { val scanSuccess = intent.getBooleanExtra(WifiManager.EXTRA_RESULTS_UPDATED, false) val results = if (scanSuccess) wifiManager.scanResults else emptyList() // 将结果发送到Flow trySend(results).isSuccess // 完成扫描后注销Receiver并关闭Flow context.unregisterReceiver(this) close() } } } // 注册Receiver context.registerReceiver( scanReceiver, IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION) ) // 发起扫描请求 val scanStarted = wifiManager.startScan() if (!scanStarted) { // 扫描启动失败,清理资源 context.unregisterReceiver(scanReceiver) trySend(emptyList()).isSuccess close() } // Flow取消时自动注销Receiver,防止内存泄漏 awaitClose { runCatching { context.unregisterReceiver(scanReceiver) } } } }
3. 调整UI层处理权限与状态收集
在Activity中处理动态权限,并用collectAsStateWithLifecycle保证状态收集与生命周期绑定:
class MainActivity : ComponentActivity() { private val requestPermissionLauncher = registerForActivityResult( ActivityResultContracts.RequestMultiplePermissions() ) { permissions -> val allGranted = permissions.values.all { it } if (allGranted) { viewModel.refreshAccessPoints() } else { Toast.makeText(this, "需要WiFi相关权限才能扫描", Toast.LENGTH_SHORT).show() } } private lateinit var viewModel: AccessPointsViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) viewModel = ViewModelProvider(this)[AccessPointsViewModel::class.java] // 检查并申请权限 val requiredPermissions = arrayOf( Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_WIFI_STATE, Manifest.permission.CHANGE_WIFI_STATE ) if (ContextCompat.checkSelfPermission(this, requiredPermissions[0]) != PackageManager.PERMISSION_GRANTED) { requestPermissionLauncher.launch(requiredPermissions) } setContent { MyTheme { Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { AccessPointsListScreen(viewModel) } } } } } @Composable fun AccessPointsListScreen( accessPointsViewModel: AccessPointsViewModel ) { // 用collectAsStateWithLifecycle确保在页面后台时停止收集 val accessPointsUiState by accessPointsViewModel.uiState.collectAsStateWithLifecycle() AccessPoints( accessPointsList = accessPointsUiState.accessPointsList, onRefresh = { accessPointsViewModel.refreshAccessPoints() } ) } @Composable private fun AccessPoints( accessPointsList: List<AccessPoint>, onRefresh: () -> Unit ) { // 示例UI:下拉刷新列表 SwipeRefresh( state = rememberSwipeRefreshState(isRefreshing = false), onRefresh = onRefresh ) { LazyColumn { items(accessPointsList) { ap -> ListItem( headlineContent = { Text(ap.ssid) }, supportingContent = { Text("信号强度: ${ap.signalStrength} dBm") } ) } } } }
4. 补充权限声明
在AndroidManifest.xml中添加必要权限:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
关键注意事项
- 内存泄漏防护:始终使用
applicationContext,并在awaitClose中注销Receiver; - 权限要求:Android 6.0+必须动态申请
ACCESS_FINE_LOCATION,否则无法获取扫描结果; - 单次扫描逻辑:上述实现为单次扫描,若需要持续扫描,可修改Flow不关闭,定期调用
startScan,但需注意功耗问题。
内容的提问来源于stack exchange,提问作者dACEvid
相关产品推荐
相关产品推荐

