一、蓝牙与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的通信流程中。

扩展函数与高阶函数在蓝牙/NFC中的分层应用 调用方(Activity / Fragment) DSL配置层(bluetooth { } / nfc { }) 扩展函数层(Intent.readNfcText() / Device.toGatt()) 高阶函数层(scanFor / connect / discoverServices / writeData) Android 原生 API(BluetoothAdapter / NfcAdapter) 配置声明 功能扩展 逻辑编排

从这张图可以看得很清楚:扩展函数负责"加功能",高阶函数负责"串逻辑",DSL负责"配参数"。三层各司其职,互不干扰。

1.6 小结:什么时候该用,什么时候不该用

最后说点实在的。不是所有蓝牙和NFC的代码都适合用DSL或扩展函数。我个人总结了几条原则:

场景 推荐方式 原因
设备扫描配置 DSL 参数多、可读性差,DSL能清晰表达意图
NFC标签读写 扩展函数 操作固定、重复性高,扩展函数能减少模板代码
多设备连接管理 高阶函数 需要编排多个异步操作,高阶函数能避免回调嵌套
简单的开关蓝牙 直接调用API 没必要过度设计,一行代码能搞定的事别用DSL

记住一句话:工具是为人服务的。Kotlin的扩展函数和高阶函数确实强大,但别为了用而用。我在项目里见过有人把BluetoothAdapter.getDefaultAdapter()都写成扩展函数——说实话,有点过了。

好了,这一章就到这里。下一章我们会深入蓝牙DSL的具体实现,包括如何设计BluetoothScope、如何处理权限请求、如何优雅地管理连接池。到时候我会把完整的源码贴出来,咱们一起拆解。


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