首页 / Android 入门教程 / 蓝牙与 NFC

Android 入门教程

蓝牙与 NFC

本教程共 100 篇 · 第 93 篇 · 更新于 2026-07-28 · 约 8 分钟阅读

AndroidAndroid 入门教程蓝牙BLENFCGATT

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" />
Warning

Android 12+ 扫描和连接要分别申请 BLUETOOTH_SCANBLUETOOTH_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
}

常见坑

  1. BLE 扫描不停:耗电严重,5-10 秒必须停。
  2. GATT 操作要串行:同时读多个特征值会失败,前一个回调完再做下一个。
  3. GATT 操作在主线程:默认回调在主线程,重操作切子线程。
  4. Android 12+ 蓝牙权限没申请:扫描直接失败。
  5. NFC 标签类型不对:要用对应的 Tech(Ndef/MifareClassic/IsoDep)。
  6. NFC 在后台读不到:前台分发要 enableForegroundDispatch,应用在前台才行。

小结

蓝牙 BLE 流程:BluetoothAdapter -> BluetoothLeScanner.startScan -> 连接 GATT -> 发现服务 -> 读写特征值。Android 12+ 要申请 BLUETOOTH_SCANBLUETOOTH_CONNECT 权限。NFC 用 NfcAdapter,前台分发拦截标签,Ndef 读写文本数据,intent-filter 自动启动应用。GATT 操作串行,扫描有超时。

下一章进入进阶主题,先看自定义 View。