综合实战:开发一个WiFi管家App
终于到了最后一章。说实话,前面29章的知识点,就像一堆散落的零件。今天我们要做的,就是把这些零件组装成一个能跑起来的完整产品——WiFi管家App。
这个App会包含扫描、连接、管理、信号监测和热点创建这五大核心功能。我个人习惯在动手前先画好架构图,这样写代码时心里才有底。来,先看看整体设计:
项目结构怎么搭
我建议用单Activity + 多Fragment的架构。别问为什么,问就是我在项目里吃过亏——多Activity跳转时,WiFi扫描回调经常丢失上下文。
包结构大概这样:
com.wifimanager.app/
├── ui/
│ ├── scan/ // 扫描页面
│ ├── connect/ // 连接页面
│ ├── monitor/ // 信号监测页面
│ ├── hotspot/ // 热点创建页面
│ └── settings/ // 设置页面
├── viewmodel/
│ ├── ScanViewModel.kt
│ ├── ConnectViewModel.kt
│ ├── MonitorViewModel.kt
│ └── HotspotViewModel.kt
├── service/
│ ├── WifiScanner.kt
│ ├── WifiConnector.kt
│ ├── SignalMonitor.kt
│ └── HotspotManager.kt
├── data/
│ ├── model/
│ │ ├── WifiApInfo.kt
│ │ └── ScanResultWrapper.kt
│ └── repository/
│ └── WifiRepository.kt
└── utils/
├── PermissionHelper.kt
└── NetworkUtils.kt
嗯,这里要注意:service层不是Android的Service组件,而是业务逻辑封装类。我习惯把WiFi操作都封装到这里,ViewModel只管调度,不直接碰系统API。
核心功能实现
1. WiFi扫描——别漏了权限检查
扫描这块,说白了就是调用WifiManager.startScan(),然后注册广播接收器拿结果。但坑不少——
ACCESS_FINE_LOCATION权限。而且Android 13又把权限改成了NEARBY_WIFI_DEVICES。所以权限适配得做好。
class WifiScanner(private val context: Context) {
private val wifiManager = context.applicationContext
.getSystemService(Context.WIFI_SERVICE) as WifiManager
fun startScan(): Flow<List<ScanResultWrapper>> = callbackFlow {
val receiver = object : BroadcastReceiver() {
override fun onReceive(ctx: Context, intent: Intent) {
if (intent.action == WifiManager.SCAN_RESULTS_AVAILABLE_ACTION) {
val success = intent.getBooleanExtra(
WifiManager.EXTRA_RESULTS_UPDATED, false)
if (success) {
val results = wifiManager.scanResults
.map { ScanResultWrapper(it) }
trySend(results)
}
}
}
}
context.registerReceiver(receiver,
IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION))
wifiManager.startScan()
awaitClose { context.unregisterReceiver(receiver) }
}
}
你想想看,为什么用callbackFlow?因为扫描结果会持续更新,用Flow可以优雅地处理流式数据。ViewModel那边收集起来也方便。
2. 连接管理——状态机是核心
连接WiFi可不是简单的enableNetwork就完事了。我建议维护一个连接状态机:
| 状态 | 说明 | 触发动作 |
|---|---|---|
| IDLE | 空闲状态 | 用户点击连接 |
| CONNECTING | 正在连接 | 调用connect() |
| CONNECTED | 已连接 | 收到CONNECTIVITY_ACTION |
| DISCONNECTED | 断开 | 网络丢失或用户主动断开 |
| ERROR | 连接失败 | 超时或认证错误 |
WifiEnterpriseConfig。我之前有个项目,用户连公司网络一直报错,查了半天发现是EAP方法没配对。
fun connectToNetwork(ssid: String, password: String): Flow<ConnectionState> {
// 构建WifiConfiguration
val config = WifiConfiguration().apply {
SSID = "\"$ssid\""
preSharedKey = "\"$password\""
status = WifiConfiguration.Status.ENABLED
}
// 添加网络
val netId = wifiManager.addNetwork(config)
// 启用并断开其他连接
wifiManager.disconnect()
wifiManager.enableNetwork(netId, true)
wifiManager.reconnect()
// 监听连接状态变化...
}
3. 信号监测——实时更新别卡UI
信号强度监测,我一般用WifiManager.calculateSignalLevel()把RSSI转成格数。但要注意,别在主线程里轮询。
class SignalMonitor(private val context: Context) {
private val wifiManager = context.applicationContext
.getSystemService(Context.WIFI_SERVICE) as WifiManager
fun observeSignalStrength(intervalMs: Long = 1000): Flow<Int> = flow {
while (true) {
val rssi = wifiManager.connectionInfo.rssi
val level = WifiManager.calculateSignalLevel(rssi, 5)
emit(level)
delay(intervalMs)
}
}.flowOn(Dispatchers.IO)
}
为什么用flowOn(Dispatchers.IO)?说白了,就是不让耗时操作卡住UI线程。你想想看,如果每秒钟在主线程里读一次RSSI,界面肯定掉帧。
4. 热点创建——Android 10以上的坑
创建热点,老API是WifiManager.setWifiApEnabled(),但Android 10以后废弃了。新API要用StartLocalOnlyHotspot()。
WifiManager.LocalOnlyHotspotReservation,必须保持引用,否则热点会自动关闭。我刚开始就犯过这个错——热点开了几秒就没了,排查了半天。
class HotspotManager(private val context: Context) {
private val wifiManager = context.applicationContext
.getSystemService(Context.WIFI_SERVICE) as WifiManager
private var reservation: WifiManager.LocalOnlyHotspotReservation? = null
fun startHotspot(ssid: String, password: String): Flow<HotspotState> = callbackFlow {
val callback = object : WifiManager.LocalOnlyHotspotCallback() {
override fun onStarted(res: WifiManager.LocalOnlyHotspotReservation) {
reservation = res
// 可以修改SSID和密码吗?不行!这是LocalOnly的限制
trySend(HotspotState.Started(res.wifiConfiguration.ssid))
}
override fun onFailed(reason: Int) {
trySend(HotspotState.Failed(reason))
}
override fun onStopped() {
trySend(HotspotState.Stopped)
}
}
wifiManager.startLocalOnlyHotspot(callback, null)
awaitClose {
reservation?.close()
reservation = null
}
}
}
WifiManager.addNetworkSuggestions()配合WifiNetworkSuggestion.Builder(),但那是Android 12以后的事了。
ViewModel层怎么组织
我个人习惯每个功能一个ViewModel,但共享的数据(比如当前连接的WiFi信息)放在父ViewModel或者Repository里。
class ScanViewModel(
private val scanner: WifiScanner,
private val repository: WifiRepository
) : ViewModel() {
private val _scanResults = MutableStateFlow<List<ScanResultWrapper>>(emptyList())
val scanResults: StateFlow<List<ScanResultWrapper>> = _scanResults.asStateFlow()
private var scanJob: Job? = null
fun startScanning() {
scanJob?.cancel()
scanJob = viewModelScope.launch {
scanner.startScan().collect { results ->
_scanResults.value = results
repository.cacheScanResults(results)
}
}
}
override fun onCleared() {
scanJob?.cancel()
super.onCleared()
}
}
你可能会问,为什么要把扫描结果缓存到Repository?嗯,因为用户可能在扫描页面和连接页面之间切换,缓存可以避免重复扫描,省电。
UI层——用RecyclerView展示扫描结果
扫描结果展示,我建议用DiffUtil配合ListAdapter。为什么?因为WiFi扫描结果变化频繁,用DiffUtil可以只更新变化的item,性能好很多。
class ScanResultAdapter : ListAdapter<ScanResultWrapper, ScanResultAdapter.ViewHolder>(
object : DiffUtil.ItemCallback<ScanResultWrapper>() {
override fun areItemsTheSame(old: ScanResultWrapper, new: ScanResultWrapper): Boolean {
return old.bssid == new.bssid // 用MAC地址判断是否同一个AP
}
override fun areContentsTheSame(old: ScanResultWrapper, new: ScanResultWrapper): Boolean {
return old.level == new.level && old.ssid == new.ssid
}
}
) {
// ViewHolder绑定...
}
权限处理——绕不开的坎
WiFi相关的权限,Android版本不同差异很大。我整理了一张表:
| 功能 | Android 9及以下 | Android 10-12 | Android 13+ |
|---|---|---|---|
| 扫描WiFi | ACCESS_FINE_LOCATION | ACCESS_FINE_LOCATION | NEARBY_WIFI_DEVICES |
| 连接WiFi | ACCESS_FINE_LOCATION | ACCESS_FINE_LOCATION | NEARBY_WIFI_DEVICES |
| 创建热点 | 无特殊权限 | 无特殊权限 | 无特殊权限 |
| 获取连接信息 | ACCESS_FINE_LOCATION | ACCESS_FINE_LOCATION | NEARBY_WIFI_DEVICES |
我建议封装一个PermissionHelper,根据SDK版本自动请求对应的权限。别在Activity里写一堆if-else,维护起来太痛苦。
完整项目跑起来
好了,所有核心模块都讲完了。把这些代码组合起来,就是一个能用的WiFi管家App。我个人建议你按这个顺序开发:
- 先写扫描模块——这是基础,能看到WiFi列表才有后续操作
- 再写连接模块——能连上WiFi,App才算有用
- 然后写信号监测——这个相对独立,可以并行开发
- 最后写热点创建——因为涉及系统API变更,需要多测试
记得在每个模块完成后,写几个单元测试。我当年就是因为偷懒没写测试,结果热点模块在Android 12上崩溃了,用户反馈了一大堆。
嗯,整个课程到这里就结束了。从WiFi基础原理到系统API,再到这个综合实战,希望你能真正掌握Android WiFi开发的精髓。代码写得好不好,最终要看能不能解决实际问题。去试试吧,有问题随时翻翻前面的章节。
公众号:蓝海资料掘金营,微信deep3321