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

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

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

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


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

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

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 方式、后台线程转主线程、以及最容易引发崩溃的生命周期挂载问题。

kotlin 复制代码
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 桥接的场景):

kotlin 复制代码
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() 和一个监听器,调用方完全不需要理解底层的 NfcAdapter 和 Flags 配置。完全符合面向对象设计的"迪米特法则(最少知道原则)"。
  4. ForegroundDispatch 被废弃/限制的风险 :老代码经常用 enableForegroundDispatch,它会抛出乱七八糟的系统提示音,甚至可能被系统的支付 APP(如钱包)抢焦点。使用 ReaderMode 是现代 Android 的标准解法,它直接、静音且完全霸占焦点。
相关推荐
三少爷的鞋3 小时前
AI 时代,我们都将成为通才型开发者:只懂 Android,已经不够了
android
Dovis(誓平步青云)7 小时前
家里设备越来越多,如何用一张空间地图控制灯光和温度![
android·java·前端·javascript·人工智能·电脑
晚风叙码9 小时前
MySQL 数据类型详解:从数值到字符串,一篇讲透
android·mysql·adb
传奇开心果编程9 小时前
【Compose Multiplatform 跨端开发学与练】第3课 布局与组件
android·windows·学习·ui·ios·kotlin·composer
辉夜技术12 小时前
iPad 插着数据线,Mac 却说"附近没有任何显示器"
客户端
传奇开心果编程12 小时前
【Compose Multiplatform 跨端开发学与练】第8课 资源管理与主题
android·windows·学习·ios·kotlin·web·composer
传奇开心果编程14 小时前
【Compose Multiplatform 跨端开发学与练】第9课 测试与调试
android·学习·macos·ios·kotlin·web·composer
传奇开心果编程14 小时前
【Compose Multiplatform 跨端开发学与练】第4课 导航与路由
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程14 小时前
【Compose Multiplatform 跨端开发学与练】第6课 状态管理与架构
android·学习·ui·ios·架构·kotlin·composer
事圆则缓14 小时前
Android AOSP 定制常见概念:源码目录、系统镜像与刷机流程
android