Android NFC 功能集成-读卡器模式

1 阅读5分钟

为了实现“高内聚、低耦合”,让 NFC 功能脱离具体的 Activity 并且极度易用,现代 Android 开发的最佳实践是:结合 Jetpack Lifecycle(生命周期感知)来封装 NFC

虽然 Android 系统强制要求 NFC 的 ReaderMode 必须依赖一个前台的 Activity 实例,但我们可以通过 DefaultLifecycleObserver 让这个封装类自动监听传入 Activity 的生命周期(onResumeonPause),从而实现:使用者不需要在 Activity 里重写任何生命周期方法,直接调用 start 和 stop 即可

这里为了避免和系统原生的 android.nfc.NfcManager 重名,我将这个封装类命名为 NfcScannerManager


第一步:权限与配置清单 (AndroidManifest.xml)

在你的 AndroidManifest.xml 中,添加以下权限和硬件声明。不需要其他额外的配置。

<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <!-- 1. 声明 NFC 权限 -->
    <uses-permission android:name="android.permission.NFC" />
    
    <!-- 2. 声明需要 NFC 硬件支持(如果你的应用没有 NFC 也能用,把 required 改为 false) -->
    <uses-feature android:name="android.hardware.nfc" android:required="true" />

    <application>
        ...
    </application>
</manifest>

第二步:封装 NfcScannerManager 类

这个类是一个“黑盒”,内部自动处理了 Android 16 推荐的获取 Adapter 方式、后台线程转主线程、以及最容易引发崩溃的生命周期挂载问题。

import android.app.Activity
import android.content.Context
import android.nfc.NfcAdapter
import android.nfc.Tag
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner

/**
 * 独立的 NFC 扫描管理器
 * 自动绑定生命周期,防崩溃、防内存泄漏
 */
class NfcScannerManager(private val activity: ComponentActivity) : 
    DefaultLifecycleObserver, 
    NfcAdapter.ReaderCallback {

    // 使用 Android 最新的系统服务获取方式(兼容 Android 15/16)
    private val nfcAdapter: NfcAdapter? by lazy {
        val manager = activity.getSystemService(Context.NFC_SERVICE) as? android.nfc.NfcManager
        manager?.defaultAdapter
    }

    // 标记业务层是否要求开启扫描
    private var isScanRequested = false

    // 对外暴露的回调接口
    var onTagDiscoveredListener: ((uidHex: String, techList: List<String>) -> Unit)? = null

    init {
        // 核心魔法:自动监听传入 Activity 的生命周期,彻底解耦
        activity.lifecycle.addObserver(this)
    }

    /**
     * 对外暴露:检查 NFC 状态
     * @return 0: 设备不支持, 1: 未开启, 2: 可用
     */
    fun checkNfcStatus(): Int {
        if (nfcAdapter == null) return 0
        if (!nfcAdapter!!.isEnabled) return 1
        return 2
    }

    /**
     * 对外暴露:开始扫描
     */
    fun startScan() {
        isScanRequested = true
        enableReaderMode()
    }

    /**
     * 对外暴露:停止扫描
     */
    fun stopScan() {
        isScanRequested = false
        disableReaderMode()
    }

    // --- 内部生命周期处理 (开发者使用时无需关心) ---

    override fun onResume(owner: LifecycleOwner) {
        // 当 Activity 回到前台时,如果业务层要求扫描,则恢复底层硬件扫描
        if (isScanRequested) {
            enableReaderMode()
        }
    }

    override fun onPause(owner: LifecycleOwner) {
        // 当 Activity 退到后台时,强制关闭底层扫描,防止系统抛出异常或耗电
        disableReaderMode()
    }

    override fun onDestroy(owner: LifecycleOwner) {
        // Activity 销毁时清理引用,防止内存泄漏
        onTagDiscoveredListener = null
        stopScan()
    }

    // --- 内部硬件控制 ---

    private fun enableReaderMode() {
        if (nfcAdapter?.isEnabled == true) {
            val flags = NfcAdapter.FLAG_READER_NFC_A or 
                        NfcAdapter.FLAG_READER_NFC_B or 
                        NfcAdapter.FLAG_READER_NFC_F or 
                        NfcAdapter.FLAG_READER_NFC_V or
                        NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK // 跳过系统 NDEF 检查,提速

            // 开启 ReaderMode,并传入额外的 Bundle 增加稳定性
            val options = Bundle().apply {
                putInt(NfcAdapter.EXTRA_READER_PRESENCE_CHECK_DELAY, 250)
            }
            nfcAdapter?.enableReaderMode(activity, this, flags, options)
        }
    }

    private fun disableReaderMode() {
        nfcAdapter?.disableReaderMode(activity)
    }

    // --- 底层 NFC 硬件读卡回调 ---

    override fun onTagDiscovered(tag: Tag?) {
        tag ?: return
        
        // 解析卡片 UID 和技术类型
        val uidHex = bytesToHex(tag.id)
        val techList = tag.techList.toList()

        // 切换回主线程抛出结果,方便外部(如 WebView)直接调用,防止跨线程崩溃
        activity.runOnUiThread {
            onTagDiscoveredListener?.invoke(uidHex, techList)
        }
    }

    private fun bytesToHex(bytes: ByteArray): String {
        val hexChars = CharArray(bytes.size * 2)
        for (i in bytes.indices) {
            val v = bytes[i].toInt() and 0xFF
            hexChars[i * 2] = "0123456789ABCDEF"[v ushr 4]
            hexChars[i * 2 + 1] = "0123456789ABCDEF"[v and 0x0F]
        }
        return String(hexChars)
    }
}

第三步:如何在任意 Activity 中使用它

使用起来极其简单。你不需要在 Activity 里面写任何 onResume / onPause 的重写逻辑,所有的脏活累活 NfcScannerManager 已经自己干了。

下面是一个标准的调用示例(结合之前给 H5 桥接的场景):

import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity

class WebActivity : AppCompatActivity() {

    // 1. 声明你的管理器
    private lateinit var nfcScannerManager: NfcScannerManager

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_web) // 假设这里面有个 WebView

        // 2. 初始化 (只需要把当前的 ComponentActivity 传进去即可)
        nfcScannerManager = NfcScannerManager(this)

        // 3. 设置读卡成功的回调 (已经在主线程,直接操作 UI 或 WebView 绝对安全)
        nfcScannerManager.onTagDiscoveredListener = { uidHex, techList ->
            // 收到 NFC 数据!可以在这里通过 Bridge 推送给 H5
            Toast.makeText(this, "读卡成功 UID: $uidHex", Toast.LENGTH_SHORT).show()
            
            // 伪代码:nativeBridge.emitEventToH5("onNfcTagDiscovered", ...)
        }
        
        // ============== 以下模拟 H5 调用的交互逻辑 ==============
        
        // 假设 H5 调用了 checkNfcStatus
        val status = nfcScannerManager.checkNfcStatus()
        if (status == 0) {
            println("这台手机没有NFC模块")
        }
        
        // 假设 H5 调用了 startNfcScan
        // 调用这句后,只要你的 Activity 在前台,手机就会开始找卡。
        // 如果用户把 APP 切到后台,扫描自动停止;切回前台,扫描自动恢复!
        nfcScannerManager.startScan()
        
        // 假设 H5 调用了 stopNfcScan
        // nfcScannerManager.stopScan()
    }
    
    // 你看,这里没有任何 onResume, onPause, onDestroy 的恶心 NFC 代码!
    // 整个 Activity 极其干净。
}

为什么这个设计“高级”且“稳定”?

  1. 自动生命周期挂载 (LifecycleObserver):这是现代 Android 架构组件最优雅的应用之一。过去写 NFC,必须死记硬背在 onPause 里面 disableReaderMode,一旦忘记,APP 在后台就会崩溃或导致系统异常。现在把 Activity 丢进去,它自己管自己生死了。
  2. 免去线程切换烦恼 (runOnUiThread):系统的 onTagDiscovered 跑在 Binder 线程组,在这里直接调 WebView.evaluateJavascript 必崩。封装类内部拦截了这个风险,直接丢到了主线程。
  3. 极简 API:对外仅仅暴露了 checkNfcStatus()startScan()stopScan() 和一个监听器,调用方完全不需要理解底层的 NfcAdapterFlags 配置。完全符合面向对象设计的“迪米特法则(最少知道原则)”。
  4. ForegroundDispatch 被废弃/限制的风险:老代码经常用 enableForegroundDispatch,它会抛出乱七八糟的系统提示音,甚至可能被系统的支付 APP(如钱包)抢焦点。使用 ReaderMode 是现代 Android 的标准解法,它直接、静音且完全霸占焦点。