You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter开发中如何实现Host Card Emulation (HCE)功能?

Flutter HCE功能落地完整实现方案

你当前使用的nfc_manager包仅支持NFC标签读写、读卡器模式相关能力,不提供HCE(主机卡模拟)的开箱支持。HCE不存在纯Dart层的实现方案,必须通过MethodChannel桥接各平台原生HCE能力落地,以下是可直接跑通的实现流程,覆盖安卓、iOS双端。

前置平台限制说明

  • 安卓端:Android 4.4及以上版本原生支持HCE,无需Root权限,但需要声明对应NFC权限、配置AID(应用标识符)过滤规则,仅当外部读卡器选中匹配的AID时,才会唤起应用的HCE服务。
  • iOS端:限制极强,iOS 13+才开放HCE能力,仅支持模拟ISO7816-4规范的接触式智能卡,不支持Mifare Classic等常见门禁/公交卡协议模拟;必须使用付费苹果开发者账号申请HCE专属权限,配置对应entitlements文件才能调用,普通免费开发者账号无法使用;且模拟的卡片仅能被安卓设备、专用POS等外部读卡器识别,无法被另一台iOS设备读取,App退到后台后HCE会话会自动断开。

分步实现流程

1. Flutter层公共桥接逻辑

HCE的核心APDU(应用协议数据单元)收发逻辑在原生层实现,Flutter层通过MethodChannel实现状态监听、服务启停、数据交互,核心代码如下:

import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';

class HceController {
  static const MethodChannel _hceChannel = MethodChannel('com.yourpackage.hce');
  /// 监听HCE收到的读卡器APDU指令、服务运行状态
  static final ValueNotifier<Map<String, dynamic>> hceEvent = ValueNotifier({});
  static final ValueNotifier<bool> isHceRunning = ValueNotifier(false);

  static void initHceBridge() {
    _hceChannel.setMethodCallHandler((call) async {
      switch (call.method) {
        case "onApduReceived":
          hceEvent.value = Map<String, dynamic>.from(call.arguments);
          break;
        case "onHceStateChanged":
          isHceRunning.value = call.arguments as bool;
          break;
      }
    });
  }

  /// 启动HCE服务,传入自定义AID、要模拟的卡片数据
  static Future<bool> startHce({required String aid, required List<int> mockCardData}) async {
    return await _hceChannel.invokeMethod('startHce', {
      "aid": aid,
      "cardData": mockCardData,
    });
  }

  /// 停止HCE服务
  static Future<void> stopHce() async {
    await _hceChannel.invokeMethod('stopHce');
  }

  /// 向读卡器返回APDU响应数据
  static Future<void> sendApduResponse(List<int> responseBytes) async {
    await _hceChannel.invokeMethod('sendApduResponse', {
      "response": responseBytes,
    });
  }
}

在App启动入口处调用HceController.initHceBridge()完成桥接初始化即可。

2. 安卓端原生适配

2.1 清单文件配置

在android/app/src/main/AndroidManifest.xml中添加权限、注册HCE服务:

<uses-permission android:name="android.permission.NFC" />
<uses-feature android:name="android.hardware.nfc.hce" android:required="true" />

<application>
  <!-- 其他配置省略 -->
  <service
    android:name=".HceServiceImpl"
    android:exported="true"
    android:permission="android.permission.BIND_NFC_SERVICE">
    <intent-filter>
      <action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE" />
    </intent-filter>
    <meta-data
      android:name="android.nfc.cardemulation.host_apdu_service"
      android:resource="@xml/hce_aid_config" />
  </service>
</application>

在android/app/src/main/res/xml目录下新建hce_aid_config.xml,配置AID过滤规则:

<?xml version="1.0" encoding="utf-8"?>
<host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android"
  android:description="HCE服务"
  android:requireDeviceUnlock="false">
  <aid-group android:category="other" android:description="自定义卡模拟">
    <!-- 替换为你自己的AID,自定义AID建议以F开头,属于国际未分配段避免冲突 -->
    <aid-filter android:name="F0010203040506" />
  </aid-group>
</host-apdu-service>

2.2 HCE服务实现

新建HceServiceImpl.kt,继承系统HostApduService实现APDU收发,和Flutter层通信:

package com.yourpackage // 替换为你自己的包名

import android.nfc.cardemulation.HostApduService
import android.os.Bundle
import io.flutter.embedding.engine.FlutterEngineCache
import io.flutter.plugin.common.MethodChannel

class HceServiceImpl : HostApduService() {
    private lateinit var channel: MethodChannel
    private val SELECT_APDU_PREFIX = byteArrayOf(0x00, 0xA4.toByte(), 0x04, 0x00)
    private val RESP_SUCCESS = byteArrayOf(0x90.toByte(), 0x00)
    private val RESP_FAIL = byteArrayOf(0x6A.toByte(), 0x82.toByte())
    private var targetAid: ByteArray? = null
    private var mockCardData: ByteArray? = null

    override fun onCreate() {
        super.onCreate()
        // 从缓存获取Flutter引擎实例,建立通信通道
        val engine = FlutterEngineCache.getInstance().get("hce_flutter_engine")
        channel = MethodChannel(engine!!.dartExecutor.binaryMessenger, "com.yourpackage.hce")
        channel.setMethodCallHandler { call, result ->
            when(call.method) {
                "startHce" -> {
                    targetAid = (call.argument<String>("aid")!!).hexToBytes()
                    mockCardData = (call.argument<List<Int>>("cardData")!!).map { it.toByte() }.toByteArray()
                    result.success(true)
                }
                "stopHce" -> {
                    targetAid = null
                    mockCardData = null
                    result.success(null)
                }
                "sendApduResponse" -> {
                    val resp = (call.argument<List<Int>>("response")!!).map { it.toByte() }.toByteArray()
                    sendResponseApdu(resp)
                    result.success(null)
                }
            }
        }
        channel.invokeMethod("onHceStateChanged", true)
    }

    override fun processCommandApdu(command: ByteArray?, p1: Bundle?): ByteArray {
        command ?: return RESP_FAIL
        // 将收到的APDU指令透传给Flutter层
        channel.invokeMethod("onApduReceived", mapOf("apdu" to command.map { it.toInt() and 0xFF }))
        // 处理AID选择指令
        if (command.size >= SELECT_APDU_PREFIX.size) {
            val prefix = command.copyOfRange(0, SELECT_APDU_PREFIX.size)
            if (SELECT_APDU_PREFIX.contentEquals(prefix)) {
                val aidLen = command[4].toInt()
                val reqAid = command.copyOfRange(5, 5 + aidLen)
                return if (reqAid.contentEquals(targetAid)) RESP_SUCCESS else RESP_FAIL
            }
        }
        // 其他指令默认返回模拟卡数据+成功状态,可根据业务逻辑自定义返回
        return mockCardData?.plus(RESP_SUCCESS) ?: RESP_FAIL
    }

    override fun onDeactivated(reason: Int) {
        channel.invokeMethod("onHceStateChanged", false)
    }

    override fun onDestroy() {
        super.onDestroy()
        channel.invokeMethod("onHceStateChanged", false)
    }

    // 十六进制字符串转字节数组工具方法
    private fun String.hexToBytes(): ByteArray {
        require(length % 2 == 0)
        return chunked(2).map { it.toInt(16).toByte() }.toByteArray()
    }
}

2.3 引擎缓存配置

在MainActivity.kt中提前缓存Flutter引擎,保证HCE服务能正常和Flutter层通信:

import android.os.Bundle
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.FlutterEngineCache

class MainActivity : FlutterActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val engine = FlutterEngine(this)
        engine.dartExecutor.executeDartEntrypoint(
            io.flutter.embedding.engine.dart.DartExecutor.DartEntrypoint.createDefault()
        )
        FlutterEngineCache.getInstance().put("hce_flutter_engine", engine)
    }
}

3. iOS端原生适配

  1. 权限配置:在苹果开发者后台给App ID开启Host Card Emulation权限,生成匹配的描述文件,在Xcode中打开项目的entitlements文件添加如下配置:
    <key>com.apple.developer.nfc.hce</key>
    <true/>
    <key>com.apple.developer.nfc.hce.iso7816.select-identifier-prefixes</key>
    <array>
      <string>F00102030405</string> <!-- 替换为你自己的AID前缀 -->
    </array>
    
    在Info.plist中添加NFC权限说明:
    <key>NFCReaderUsageDescription</key>
    <string>需要使用NFC实现卡模拟功能</string>
    
  2. 核心逻辑实现:导入CoreNFC框架,创建NFCHostCardEmulationSession实例实现APDU收发,逻辑和安卓端对齐:接收Flutter层的启动参数、将收到的APDU指令透传给Flutter、调用系统API返回APDU响应、同步HCE会话状态即可。注意iOS的HCE会话必须在App前台启动,退后台自动断开。

常见踩坑点

  • 不要尝试找纯Dart的HCE插件,目前所有公开Flutter NFC插件都只覆盖读卡器、写卡模式,HCE必须写原生桥接
  • iOS HCE能力有强权限限制,拿不到官方entitlement的情况下不用做兼容尝试,完全无法调用
  • 安卓部分定制ROM会默认拦截第三方HCE服务,需要引导用户开启自启动、后台无限制权限
  • 调试时必须用外部NFC设备/另一台NFC手机当读卡器,不能用同设备的NFC读取自己模拟的卡片
  • 自定义AID不要使用官方分配给金融、交通等场景的段,建议以F开头避免冲突

内容的提问来源于stack exchange,提问作者Kumaresh Chandra Baruri

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 03:31:01