综合实战:开发一个WiFi管家App

终于到了最后一章。说实话,前面29章的知识点,就像一堆散落的零件。今天我们要做的,就是把这些零件组装成一个能跑起来的完整产品——WiFi管家App。

这个App会包含扫描、连接、管理、信号监测和热点创建这五大核心功能。我个人习惯在动手前先画好架构图,这样写代码时心里才有底。来,先看看整体设计:

WiFi管家App 整体架构 UI层 (Activity/Fragment) ViewModel + LiveData / Flow WiFiManager + ConnectivityManager 核心功能模块 WiFi扫描 连接管理 信号监测 热点创建 (扫描结果缓存 / 连接状态机 / 信号强度记录 / AP热点配置) Android系统WiFi服务

项目结构怎么搭

我建议用单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(),然后注册广播接收器拿结果。但坑不少——

我曾经踩过的坑:Android 10以上,扫描结果里拿不到SSID了,除非你有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连接失败超时或认证错误
小技巧:连接WPA2企业网络时,记得用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()

注意:LocalOnlyHotspot的回调里拿到的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
        }
    }
}
重要提醒:LocalOnlyHotspot创建的SSID和密码是系统随机生成的,你不能自定义。如果想自定义,得用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置顶显示,用户体验会好很多。

权限处理——绕不开的坎

WiFi相关的权限,Android版本不同差异很大。我整理了一张表:

功能Android 9及以下Android 10-12Android 13+
扫描WiFiACCESS_FINE_LOCATIONACCESS_FINE_LOCATIONNEARBY_WIFI_DEVICES
连接WiFiACCESS_FINE_LOCATIONACCESS_FINE_LOCATIONNEARBY_WIFI_DEVICES
创建热点无特殊权限无特殊权限无特殊权限
获取连接信息ACCESS_FINE_LOCATIONACCESS_FINE_LOCATIONNEARBY_WIFI_DEVICES

我建议封装一个PermissionHelper,根据SDK版本自动请求对应的权限。别在Activity里写一堆if-else,维护起来太痛苦。

完整项目跑起来

好了,所有核心模块都讲完了。把这些代码组合起来,就是一个能用的WiFi管家App。我个人建议你按这个顺序开发:

  1. 先写扫描模块——这是基础,能看到WiFi列表才有后续操作
  2. 再写连接模块——能连上WiFi,App才算有用
  3. 然后写信号监测——这个相对独立,可以并行开发
  4. 最后写热点创建——因为涉及系统API变更,需要多测试

记得在每个模块完成后,写几个单元测试。我当年就是因为偷懒没写测试,结果热点模块在Android 12上崩溃了,用户反馈了一大堆。

嗯,整个课程到这里就结束了。从WiFi基础原理到系统API,再到这个综合实战,希望你能真正掌握Android WiFi开发的精髓。代码写得好不好,最终要看能不能解决实际问题。去试试吧,有问题随时翻翻前面的章节。


公众号:蓝海资料掘金营,微信deep3321