蓝牙与 NFC
本教程共 100 篇 · 第 93 篇 · 更新于 2026-07-28 · 约 8 分钟阅读
93. 蓝牙与 NFC
本节目标:了解蓝牙基础(Classic 和 BLE)、BLE 扫描和 GATT 读写、NFC 标签读写和 intent-filter,能连接简单的物联网设备和读取 NFC 卡片。
蓝牙和 NFC 是两种近场通信方式。蓝牙传得远(10 米左右)、双向通信、能传大数据;NFC 传得近(4 厘米内)、触碰即用、数据量小但快。
蓝牙两种模式
- Classic Bluetooth:老式蓝牙,传文件、A2DP 音频、HFP 通话。带宽大,功耗高。
- Bluetooth Low Energy(BLE):低功耗蓝牙,物联网设备常用。带宽小,省电。
两种协议栈不同,API 也分开。下面主要讲 BLE,因为新设备都用 BLE。
权限
Android 12+ 蓝牙权限改了,要运行时请求:
<!-- 老版本(API 30 及以下) -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<!-- 新版本(API 31+) -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- BLE 必需 -->
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
<!-- 位置(扫描 BLE 在 Android 6-11 要位置权限) -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
WarningAndroid 12+ 扫描和连接要分别申请
BLUETOOTH_SCAN和BLUETOOTH_CONNECT运行时权限。Android 6-11 扫描 BLE 还要位置权限(奇葩设计)。
获取 BluetoothAdapter
val bluetoothManager = context.getSystemService(BluetoothManager::class.java)
val bluetoothAdapter = bluetoothManager?.adapter
if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled) {
// 设备不支持蓝牙,或者没开启
val enableIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
startActivity(enableIntent)
}
扫描 BLE 设备
val scanner = bluetoothAdapter.bluetoothLeScanner
// 扫描回调
val scanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
val device = result.device
val name = device.name
val address = device.address
Log.d("BLE", "发现:$name ($address)")
}
override fun onScanFailed(errorCode: Int) {
Log.e("BLE", "扫描失败:$errorCode")
}
}
// 开始扫描(5 秒后停)
scanner.startScan(scanCallback)
Handler(Looper.getMainLooper()).postDelayed({
scanner.stopScan(scanCallback)
}, 5000)
Warning扫描很耗电,限定时间停。别一直扫。
连接 GATT
BLE 设备用 GATT 协议通信。连接后能读写特征值(Characteristic)。
val gattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
when (newState) {
BluetoothProfile.STATE_CONNECTED -> {
Log.d("BLE", "已连接")
gatt.discoverServices() // 发现服务
}
BluetoothProfile.STATE_DISCONNECTED -> {
Log.d("BLE", "已断开")
gatt.close()
}
}
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
// 拿到所有 Service 和 Characteristic
val services = gatt.services
services.forEach { service ->
service.characteristics.forEach { char ->
Log.d("BLE", "Characteristic: ${char.uuid}")
}
}
}
override fun onCharacteristicRead(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray,
status: Int
) {
if (status == BluetoothGatt.GATT_SUCCESS) {
Log.d("BLE", "读取到:${value.toHexString()}")
}
}
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray
) {
// 订阅的特征值变化
Log.d("BLE", "通知:${value.toHexString()}")
}
}
// 连接
val gatt = device.connectGatt(context, false, gattCallback)
读写特征值
// 读
gatt.readCharacteristic(characteristic)
// 写
val data = byteArrayOf(0x01, 0x02, 0x03)
characteristic.value = data // 老版本
gatt.writeCharacteristic(characteristic)
// Android 13+ 新 API
// gatt.writeCharacteristic(characteristic, data, BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT)
订阅通知
特征值变化时被动通知,不用轮询:
gatt.setCharacteristicNotification(characteristic, true)
val descriptor = characteristic.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"))
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
gatt.writeDescriptor(descriptor)
Compose 集成
把 BLE 操作封装成 Repository,用 Flow 暴露状态:
class BleRepository(context: Context) {
private val _connectionState = MutableStateFlow<ConnectionState>(ConnectionState.Idle)
val connectionState = _connectionState.asStateFlow()
private val _data = MutableSharedFlow<ByteArray>()
val data = _data.asSharedFlow()
suspend fun connect(device: BluetoothDevice) { /* ... */ }
suspend fun write(data: ByteArray) { /* ... */ }
fun disconnect() { /* ... */ }
}
NFC 基础
NFC(近场通信)用于触碰式数据传输。常见场景:门禁卡、公交卡、NFC 标签、Android Beam(已废弃)。
权限和 Feature
<uses-permission android:name="android.permission.NFC" />
<uses-feature android:name="android.hardware.nfc" android:required="false" />
检测 NFC 支持
val nfcAdapter = NfcAdapter.getDefaultAdapter(context)
if (nfcAdapter == null) {
// 设备不支持 NFC
}
if (!nfcAdapter.isEnabled) {
// NFC 没开启,提示用户去设置
}
前台分发
应用在前台时拦截 NFC 标签:
class NfcActivity : AppCompatActivity() {
private lateinit var nfcAdapter: NfcAdapter
private lateinit var pendingIntent: PendingIntent
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
nfcAdapter = NfcAdapter.getDefaultAdapter(this) ?: return
pendingIntent = PendingIntent.getActivity(
this, 0,
Intent(this, javaClass).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP),
PendingIntent.FLAG_MUTABLE
)
}
override fun onResume() {
super.onResume()
nfcAdapter.enableForegroundDispatch(this, pendingIntent, null, null)
}
override fun onPause() {
super.onPause()
nfcAdapter.disableForegroundDispatch(this)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
if (NfcAdapter.ACTION_NDEF_DISCOVERED == intent.action ||
NfcAdapter.ACTION_TECH_DISCOVERED == intent.action ||
NfcAdapter.ACTION_TAG_DISCOVERED == intent.action) {
processNfcIntent(intent)
}
}
private fun processNfcIntent(intent: Intent) {
val tag = intent.getParcelableExtra<Tag>(NfcAdapter.EXTRA_TAG) ?: return
val ndef = Ndef.get(tag)
if (ndef != null) {
readNdef(ndef)
}
}
}
读取 NDEF
fun readNdef(ndef: Ndef) {
ndef.connect()
try {
val ndefMessage = ndef.ndefMessage
ndefMessage.records.forEach { record ->
val payload = record.payload
val text = String(payload, Charsets.UTF_8)
Log.d("NFC", "读到:$text")
}
} finally {
ndef.close()
}
}
写入 NDEF
fun writeNdef(ndef: Ndef, text: String) {
ndef.connect()
try {
val record = NdefRecord.createTextRecord("zh", text)
val message = NdefMessage(arrayOf(record))
if (ndef.isWritable) {
ndef.writeNdefMessage(message)
}
} finally {
ndef.close()
}
}
intent-filter 自动启动
应用没运行时,扫到 NFC 标签自动启动应用:
<activity android:name=".NfcActivity">
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
Compose 集成 NFC
把 NFC 读取封装成可观察状态:
@Composable
fun rememberNfcTag(): State<String?> {
val tagContent = remember { mutableStateOf<String?>(null) }
val context = LocalContext.current
DisposableEffect(Unit) {
val adapter = NfcAdapter.getDefaultAdapter(context) ?: return@DisposableEffect onDispose {}
val pendingIntent = PendingIntent.getActivity(
context, 0,
Intent(context, MainActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP),
PendingIntent.FLAG_MUTABLE
)
// 在 onResume 注册,onPause 注销需要包装到 LifecycleObserver
// 简化版:直接注册
onDispose {
// 释放资源
}
}
return tagContent
}
常见坑
- BLE 扫描不停:耗电严重,5-10 秒必须停。
- GATT 操作要串行:同时读多个特征值会失败,前一个回调完再做下一个。
- GATT 操作在主线程:默认回调在主线程,重操作切子线程。
- Android 12+ 蓝牙权限没申请:扫描直接失败。
- NFC 标签类型不对:要用对应的 Tech(
Ndef/MifareClassic/IsoDep)。 - NFC 在后台读不到:前台分发要
enableForegroundDispatch,应用在前台才行。
小结
蓝牙 BLE 流程:BluetoothAdapter -> BluetoothLeScanner.startScan -> 连接 GATT -> 发现服务 -> 读写特征值。Android 12+ 要申请 BLUETOOTH_SCAN 和 BLUETOOTH_CONNECT 权限。NFC 用 NfcAdapter,前台分发拦截标签,Ndef 读写文本数据,intent-filter 自动启动应用。GATT 操作串行,扫描有超时。
下一章进入进阶主题,先看自定义 View。