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

遵循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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 23:42:07