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