一、蓝牙与NFC:Kotlin高阶函数如何让硬件通信变得优雅
做Android开发这么多年,蓝牙和NFC一直是我觉得最"拧巴"的两个模块。为什么?因为它们的API设计太过程式化了。你想想看,一个简单的设备扫描,要写一堆回调、状态机、异常处理。代码量上去了,可读性却下来了。
我个人习惯用Kotlin的扩展函数和高阶函数来重构这些逻辑。说白了,就是把那些重复的模板代码藏起来,让调用方只关心"做什么",而不是"怎么做"。
1.1 蓝牙的DSL配置:让连接逻辑像写配置文件一样清晰
先看一个典型的蓝牙连接代码。我记得早期做项目时,光是蓝牙扫描就写了200多行。后来我用DSL重构了它,效果立竿见影。
// 传统写法
val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
val leScanner = bluetoothAdapter.bluetoothLeScanner
val scanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult?) {
// 处理扫描结果
}
override fun onScanFailed(errorCode: Int) {
// 处理失败
}
}
leScanner.startScan(scanCallback)
// DSL写法
bluetooth {
scan {
name contains "MyDevice"
rssi greaterThan -70
timeout 10.seconds
onFound { device ->
connect {
retry 3
timeout 5.seconds
onConnected { gatt ->
discoverServices()
}
}
}
}
}
你可能会问:DSL是怎么做到的?其实核心就是高阶函数和带接收者的lambda。我定义了一个BluetoothScope类,里面放各种配置方法,然后用bluetooth { }这个高阶函数接收它。
核心思路:把蓝牙的生命周期、状态回调、错误处理全部封装到DSL内部。调用方只需要写配置,不需要管底层细节。
1.2 NFC的扩展函数:让标签读写变成一行代码
NFC这块更有意思。Android的NFC API是基于Intent和Tag分发的,处理起来特别绕。我曾经在一个项目里被NFC的"前台调度系统"坑过——明明标签贴上去,就是没反应。
后来我用扩展函数把NFC的读写操作简化了。来看个例子:
// 传统写法
val tag = intent.getParcelableExtra<Tag>(NfcAdapter.EXTRA_TAG)
val ndef = Ndef.get(tag)
ndef?.connect()
val message = ndef?.ndefMessage
ndef?.close()
// 扩展函数写法
val text = intent.readNfcText() ?: "未读取到内容"
// 背后实现
fun Intent.readNfcText(): String? {
val tag = getParcelableExtra<Tag>(NfcAdapter.EXTRA_TAG) ?: return null
val ndef = Ndef.get(tag) ?: return null
return try {
ndef.connect()
ndef.ndefMessage?.records?.firstOrNull()?.toText()
} catch (e: Exception) {
null
} finally {
try { ndef.close() } catch (_: Exception) {}
}
}
嗯,这里要注意:扩展函数不是万能的。如果NFC操作涉及异步回调(比如写标签需要等待用户靠近),我建议用高阶函数配合回调参数来处理。
1.3 设备发现与连接的简化:用高阶函数消除回调地狱
蓝牙设备发现这个场景,简直是回调地狱的重灾区。你要监听扫描回调、连接状态回调、服务发现回调、特征值变化回调……嵌套个四五层是常事。
我是怎么解决的?用高阶函数把这些回调"拍平"。
// 传统回调嵌套
bluetoothLeScanner.startScan(object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult?) {
if (result?.device?.name == "MyDevice") {
result.device.connectGatt(context, false, object : BluetoothGattCallback() {
override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
gatt?.discoverServices()
}
}
override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
// 找到服务后继续...
}
})
}
}
})
// 高阶函数简化
bluetooth {
scanFor("MyDevice") { device ->
connect(device) { gatt ->
discoverServices(gatt) { services ->
// 直接使用services
}
}
}
}
我的经验:高阶函数的参数类型要设计得"窄"一点。比如connect的lambda只接收BluetoothGatt,而不是整个BluetoothDevice。这样调用方拿到的就是最直接可用的对象,少了很多判空和转换。
1.4 避坑指南:我曾经踩过的三个坑
讲到这里,我得分享几个实际项目中遇到的坑,希望能帮你少走弯路。
- 蓝牙扫描的线程问题:我曾经在子线程里调了
startScan,结果回调死活不触发。后来才发现,蓝牙扫描必须在主线程调用。所以我在DSL内部强制用Handler(Looper.getMainLooper())做线程切换。 - NFC的Tag丢失问题:有一次用户快速刷了两张卡,第二张卡的Intent把第一张覆盖了。我的扩展函数里没有做防重入处理,导致数据错乱。后来我加了一个
synchronized块,确保同一时间只处理一个Tag。 - DSL的链式调用内存泄漏:如果DSL内部持有Activity的引用,而Activity销毁时DSL还在执行,就会泄漏。我习惯在DSL的
onDestroy回调里主动释放资源。
特别注意:蓝牙和NFC的硬件资源是有限的。DSL虽然方便,但一定要在finally块或use函数中释放连接。否则用户会发现蓝牙连不上、NFC读不到——这种问题特别难排查。
1.5 知识体系图:扩展函数与高阶函数在蓝牙/NFC中的角色
下面这张图是我自己梳理的。它展示了Kotlin的扩展函数和高阶函数如何分层作用于蓝牙和NFC的通信流程中。
从这张图可以看得很清楚:扩展函数负责"加功能",高阶函数负责"串逻辑",DSL负责"配参数"。三层各司其职,互不干扰。
1.6 小结:什么时候该用,什么时候不该用
最后说点实在的。不是所有蓝牙和NFC的代码都适合用DSL或扩展函数。我个人总结了几条原则:
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 设备扫描配置 | DSL | 参数多、可读性差,DSL能清晰表达意图 |
| NFC标签读写 | 扩展函数 | 操作固定、重复性高,扩展函数能减少模板代码 |
| 多设备连接管理 | 高阶函数 | 需要编排多个异步操作,高阶函数能避免回调嵌套 |
| 简单的开关蓝牙 | 直接调用API | 没必要过度设计,一行代码能搞定的事别用DSL |
记住一句话:工具是为人服务的。Kotlin的扩展函数和高阶函数确实强大,但别为了用而用。我在项目里见过有人把BluetoothAdapter.getDefaultAdapter()都写成扩展函数——说实话,有点过了。
好了,这一章就到这里。下一章我们会深入蓝牙DSL的具体实现,包括如何设计BluetoothScope、如何处理权限请求、如何优雅地管理连接池。到时候我会把完整的源码贴出来,咱们一起拆解。
公众号:蓝海资料掘金营,微信 deep3321