开场:我为什么把 iOS 的坑放在最前面
先交代一句:NFC 我没在项目里真正做过,这篇的判断来自 flutter_nfc_kit 和 nfc_manager 的官方文档,算是我给后来人写的避坑指南。所以这篇的姿势是”先讲约束,再讲代码”——NFC 最大的成本从来不是读写 API,而是平台差异:iOS 的 NFC 会话是系统接管的弹窗,扫到一张卡会话就结束,还会自动超时,“后台贴卡即读”在 iOS 上根本走不通,只有 Android 能做到。做门禁、考勤这类产品,立项评审时就要把 iOS 的交互形态单独算进去,别按 Android 的体验去承诺。
选型上,正文代码用的是 flutter_nfc_kit(poll/finish 模型:poll 一次读一张卡,读完调 finish),功能全、上手快;文末实战节则按 nfc_manager 的会话制模型讲了另一套心智(startSession → onDiscovered → Ndef.from → stopSession)。两个插件我都建议了解一下,先吃透一种再看另一种。
先说概念:三种工作模式,读卡器模式是常态
三种工作模式:读卡器模式最常用,卡模拟最麻烦
NFC(Near Field Communication)是 4 厘米内的短距离高频通信。我把三种模式列出来,主要是想让你选型时先对号入座:Flutter 里九成的需求都是读卡器模式;点对点基本已死(Android Beam 都废弃了);卡模拟涉及发卡机构和安全元件,个人开发者碰不到,了解一下就行。
NFC 的三种工作模式:
-
读卡器模式(Reader/Writer Mode)
- 手机作为读卡器,读取 NFC 标签
- 最常见的应用场景
-
点对点模式(Peer-to-Peer Mode)
- 两台 NFC 设备直接通信
- Android Beam(已废弃)就是这种模式
-
卡模拟模式(Card Emulation Mode)
- 手机模拟成 NFC 卡片
- 用于移动支付、公交卡等
NDEF 和标签类型:选型时才需要翻的表
NDEF(NFC Data Exchange Format)
- 标准化的数据交换格式
- 支持多种记录类型:文本、URI、MIME 类型等
常见的 NFC 标签类型:
- NFC Forum Type 1:如 Topaz,可读写,容量较小
- NFC Forum Type 2:如 MIFARE Ultralight,可读写,容量适中
- NFC Forum Type 3:如 FeliCa,主要在日本使用
- NFC Forum Type 4:如 MIFARE DESFire,支持更复杂的操作
- MIFARE Classic:非标准,但广泛使用
插件选型:flutter_nfc_kit 全,nfc_manager 轻
选型上我就认这三种,按推荐度排序:
- flutter_nfc_kit - 功能全面的 NFC 插件
- nfc_manager - 轻量级 NFC 管理插件
- 自定义平台通道 - 完全自定义实现
环境搭建:依赖和权限一次配好
依赖:先加 flutter_nfc_kit
dependencies:
flutter:
sdk: flutter
flutter_nfc_kit: ^3.5.2
permission_handler: ^11.0.1
权限:Android 和 iOS 分开配
Android 权限配置(android/app/src/main/AndroidManifest.xml)
<!-- NFC基本权限 -->
<uses-permission android:name="android.permission.NFC" />
<!-- Android 12+ 需要这个权限 -->
<uses-permission android:name="android.permission.NFC_PREFERRED_PAYMENT_INFO" />
<!-- 硬件特性声明 -->
<uses-feature android:name="android.hardware.nfc" android:required="true" />
<!-- Android 13+ 需要这个权限用于NFC支付 -->
<uses-permission android:name="android.permission.NFC_TRANSACTION_EVENT" />
iOS 权限配置(ios/Runner/Info.plist)
<key>NFCReaderUsageDescription</key>
<string>此应用需要NFC权限来读取智能标签</string>
<!-- 如果需要后台NFC读取 -->
<key>com.apple.developer.nfc.readersession.formats</key>
<array>
<string>NDEF</string>
<string>TAG</string>
</array>
运行时:先调 availability 再开 poll
import 'package:permission_handler/permission_handler.dart';
class NFCPermissionManager {
static Future<bool> requestPermissions() async {
if (Platform.isAndroid) {
// Android需要NFC权限
final nfcPermission = await Permission.nfc.request();
return nfcPermission.isGranted;
} else {
// iOS不需要特殊权限,但需要检查NFC可用性
return await NFCAvailability.isAvailable();
}
}
static Future<bool> checkPermissions() async {
if (Platform.isAndroid) {
return await Permission.nfc.isGranted;
} else {
return await NFCAvailability.isAvailable();
}
}
static Future<void> openSettings() async {
await openAppSettings();
}
}
class NFCAvailability {
static Future<bool> isAvailable() async {
try {
if (Platform.isAndroid) {
// 检查Android设备是否支持NFC
final availability = await FlutterNfcKit.nfcAvailability;
return availability != NFCAvailability.not_supported;
} else {
// iOS设备检查
return await FlutterNfcKit.nfcAvailability != NFCAvailability.not_supported;
}
} catch (e) {
return false;
}
}
static Future<NFCStatus> checkStatus() async {
try {
return await FlutterNfcKit.nfcAvailability;
} catch (e) {
return NFCStatus.not_supported;
}
}
}
enum NFCStatus {
available,
enabled,
disabled,
not_supported,
}
读卡:poll 一次,读一张卡
读取管理器:poll 到 finish 的一圈
import 'package:flutter_nfc_kit/flutter_nfc_kit.dart';
class NFCReader {
bool _isReading = false;
StreamController<NFCData>? _dataController;
bool get isReading => _isReading;
Future<NFCData> readNFC({
Duration timeout = const Duration(seconds: 30),
bool poll = true,
bool iOSAlertMessage = true,
}) async {
if (_isReading) {
throw NFCException('正在读取中,请稍后再试');
}
_isReading = true;
try {
// 检查NFC可用性
final availability = await FlutterNfcKit.nfcAvailability;
if (availability == NFCAvailability.disabled) {
throw NFCException('NFC功能已禁用,请在设置中开启');
}
if (availability == NFCAvailability.not_supported) {
throw NFCException('设备不支持NFC功能');
}
// 开始读取
final result = await FlutterNfcKit.poll(
timeout: timeout,
iosAlertMessage: iOSAlertMessage ? "请将手机靠近NFC标签" : null,
readIso15693: true,
readIso14443A: true,
readIso14443B: true,
);
return NFCData.fromPollResult(result);
} catch (e) {
throw NFCException('读取失败:${e.toString()}');
} finally {
_isReading = false;
}
}
Stream<NFCData> startContinuousReading() async* {
if (_isReading) {
throw NFCException('正在读取中,请稍后再试');
}
_isReading = true;
_dataController = StreamController<NFCData>.broadcast();
try {
while (_isReading) {
try {
final result = await FlutterNfcKit.poll(
timeout: Duration(seconds: 10),
iosAlertMessage: false,
);
final nfcData = NFCData.fromPollResult(result);
_dataController?.add(nfcData);
// 等待标签离开
await Future.delayed(Duration(seconds: 1));
} catch (e) {
// 超时或其他错误,继续尝试
continue;
}
}
} finally {
_isReading = false;
_dataController?.close();
_dataController = null;
}
}
void stopReading() {
_isReading = false;
_dataController?.close();
_dataController = null;
}
Future<void> finishSession() async {
try {
await FlutterNfcKit.finish();
} catch (e) {
// 忽略结束会话的错误
}
}
}
class NFCData {
final String type;
final String id;
final Map<String, dynamic>? additionalData;
final List<NDEFRecord>? ndefRecords;
NFCData({
required this.type,
required this.id,
this.additionalData,
this.ndefRecords,
});
factory NFCData.fromPollResult(NFCTag result) {
final ndefRecords = <NDEFRecord>[];
// 解析NDEF记录
if (result.ndefRecords != null) {
for (final record in result.ndefRecords!) {
ndefRecords.add(NDEFRecord.fromNFCRecord(record));
}
}
return NFCData(
type: result.type.toString(),
id: result.id,
additionalData: {
'standard': result.standard,
'mifareInfo': result.mifareInfo,
'feliCaInfo': result.feliCaInfo,
'iso7816Info': result.iso7816Info,
},
ndefRecords: ndefRecords.isNotEmpty ? ndefRecords : null,
);
}
bool get hasNDEF => ndefRecords != null && ndefRecords!.isNotEmpty;
NDEFRecord? getFirstNDEFRecord() {
if (hasNDEF) {
return ndefRecords!.first;
}
return null;
}
List<NDEFRecord> getNDEFRecordsByType(String type) {
if (!hasNDEF) return [];
return ndefRecords!.where((record) => record.type == type).toList();
}
String? getTextContent() {
final textRecords = getNDEFRecordsByType('text');
if (textRecords.isNotEmpty) {
return textRecords.first.content;
}
return null;
}
String? getURIContent() {
final uriRecords = getNDEFRecordsByType('uri');
if (uriRecords.isNotEmpty) {
return uriRecords.first.content;
}
return null;
}
}
class NDEFRecord {
final String type;
final String content;
final String? language;
final String? encoding;
NDEFRecord({
required this.type,
required this.content,
this.language,
this.encoding,
});
factory NDEFRecord.fromNFCRecord(NDEFRecord record) {
String type = 'unknown';
String content = '';
String? language;
String? encoding;
if (record.type == NDEFRecordType.text) {
type = 'text';
content = record.text ?? '';
language = record.languageCode;
encoding = record.encoding;
} else if (record.type == NDEFRecordType.uri) {
type = 'uri';
content = record.uri ?? '';
} else if (record.type == NDEFRecordType.mime) {
type = 'mime';
content = String.fromCharCodes(record.payload);
} else if (record.type == NDEFRecordType.absoluteUri) {
type = 'absolute_uri';
content = record.uri ?? '';
} else if (record.type == NDEFRecordType.external) {
type = 'external';
content = String.fromCharCodes(record.payload);
}
return NDEFRecord(
type: type,
content: content,
language: language,
encoding: encoding,
);
}
}
特殊标签:Mifare Classic 得写平台分支
class MifareClassicHandler {
static Future<Map<String, dynamic>> readMifareClassic(NFCTag tag) async {
try {
final mifareInfo = tag.mifareInfo;
if (mifareInfo == null) {
throw NFCException('不是MIFARE Classic标签');
}
final sectorCount = mifareInfo.sectorCount;
final blockCount = mifareInfo.blockCount;
// 读取所有扇区(需要认证)
final sectors = <Map<String, dynamic>>[];
for (int sector = 0; sector < sectorCount; sector++) {
try {
// 尝试使用默认密钥A认证
final authResult = await FlutterNfcKit.transceive(
data: [0x60, sector, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
);
if (authResult.isNotEmpty) {
// 读取扇区数据
final sectorData = await _readSector(sector);
sectors.add({
'sector': sector,
'data': sectorData,
'authenticated': true,
});
} else {
sectors.add({
'sector': sector,
'data': [],
'authenticated': false,
});
}
} catch (e) {
sectors.add({
'sector': sector,
'data': [],
'authenticated': false,
'error': e.toString(),
});
}
}
return {
'type': 'MIFARE Classic',
'sectorCount': sectorCount,
'blockCount': blockCount,
'sectors': sectors,
};
} catch (e) {
throw NFCException('读取MIFARE Classic失败:${e.toString()}');
}
}
static Future<List<int>> _readSector(int sector) async {
final blocks = <List<int>>[];
final startBlock = sector * 4;
for (int block = 0; block < 4; block++) {
final result = await FlutterNfcKit.transceive(
data: [0x30, startBlock + block],
);
blocks.add(result);
}
return blocks.expand((block) => block).toList();
}
}
class NTAGHandler {
static Future<Map<String, dynamic>> readNTAG(NFCTag tag) async {
try {
final ndefRecords = tag.ndefRecords ?? [];
// 读取配置信息
final configData = await _readConfigPage();
// 读取用户数据
final userData = await _readUserData();
return {
'type': 'NTAG',
'ndefRecords': ndefRecords.map((record) => NDEFRecord.fromNFCRecord(record)).toList(),
'config': configData,
'userData': userData,
};
} catch (e) {
throw NFCException('读取NTAG失败:${e.toString()}');
}
}
static Future<Map<String, dynamic>> _readConfigPage() async {
try {
final result = await FlutterNfcKit.transceive(
data: [0x30, 0x00], // 读取配置页
);
return {
'page': 0,
'data': result,
'capacity': _parseCapacity(result),
'readonly': _isReadOnly(result),
};
} catch (e) {
return {};
}
}
static Future<List<List<int>>> _readUserData() async {
final pages = <List<int>>[];
try {
for (int page = 4; page < 16; page++) { // NTAG213有45页,NTAG215有135页
try {
final result = await FlutterNfcKit.transceive(
data: [0x30, page],
);
pages.add(result);
} catch (e) {
break; // 到达末尾
}
}
} catch (e) {
// 忽略错误
}
return pages;
}
static int _parseCapacity(List<int> configData) {
if (configData.length >= 2) {
final capacityByte = configData[2];
switch (capacityByte) {
case 0x12: return 144; // NTAG213
case 0x3E: return 504; // NTAG215
case 0x6D: return 888; // NTAG216
default: return 144;
}
}
return 144;
}
static bool _isReadOnly(List<int> configData) {
if (configData.length >= 3) {
return (configData[3] & 0x10) != 0;
}
return false;
}
}
写卡:NDEF 写入的正路
NDEF 写入:文本、URI、MIME 三种记录
class NFCWriter {
bool _isWriting = false;
bool get isWriting => _isWriting;
Future<void> writeText({
required String text,
String? language,
Duration timeout = const Duration(seconds: 30),
}) async {
if (_isWriting) {
throw NFCException('正在写入中,请稍后再试');
}
_isWriting = true;
try {
// 检查NFC可用性
final availability = await FlutterNfcKit.nfcAvailability;
if (availability == NFCAvailability.disabled) {
throw NFCException('NFC功能已禁用,请在设置中开启');
}
// 等待标签
final tag = await FlutterNfcKit.poll(
timeout: timeout,
iosAlertMessage: "请将手机靠近NFC标签进行写入",
);
// 创建NDEF记录
final ndefRecord = NDEFRecord.text(
text: text,
language: language ?? 'zh',
);
// 写入NDEF数据
await FlutterNfcKit.writeNDEFRecords([ndefRecord]);
await FlutterNfcKit.finish();
} catch (e) {
throw NFCException('写入失败:${e.toString()}');
} finally {
_isWriting = false;
}
}
Future<void> writeURI({
required String uri,
Duration timeout = const Duration(seconds: 30),
}) async {
if (_isWriting) {
throw NFCException('正在写入中,请稍后再试');
}
_isWriting = true;
try {
final availability = await FlutterNfcKit.nfcAvailability;
if (availability == NFCAvailability.disabled) {
throw NFCException('NFC功能已禁用,请在设置中开启');
}
final tag = await FlutterNfcKit.poll(
timeout: timeout,
iosAlertMessage: "请将手机靠近NFC标签进行写入",
);
final ndefRecord = NDEFRecord.uri(uri);
await FlutterNfcKit.writeNDEFRecords([ndefRecord]);
await FlutterNfcKit.finish();
} catch (e) {
throw NFCException('写入失败:${e.toString()}');
} finally {
_isWriting = false;
}
}
Future<void> writeMIME({
required String mimeType,
required List<int> data,
Duration timeout = const Duration(seconds: 30),
}) async {
if (_isWriting) {
throw NFCException('正在写入中,请稍后再试');
}
_isWriting = true;
try {
final availability = await FlutterNfcKit.nfcAvailability;
if (availability == NFCAvailability.disabled) {
throw NFCException('NFC功能已禁用,请在设置中开启');
}
final tag = await FlutterNfcKit.poll(
timeout: timeout,
iosAlertMessage: "请将手机靠近NFC标签进行写入",
);
final ndefRecord = NDEFRecord.mime(
mimeType: mimeType,
data: data,
);
await FlutterNfcKit.writeNDEFRecords([ndefRecord]);
await FlutterNfcKit.finish();
} catch (e) {
throw NFCException('写入失败:${e.toString()}');
} finally {
_isWriting = false;
}
}
Future<void> writeMultipleRecords({
required List<NDEFRecord> records,
Duration timeout = const Duration(seconds: 30),
}) async {
if (_isWriting) {
throw NFCException('正在写入中,请稍后再试');
}
_isWriting = true;
try {
final availability = await FlutterNfcKit.nfcAvailability;
if (availability == NFCAvailability.disabled) {
throw NFCException('NFC功能已禁用,请在设置中开启');
}
final tag = await FlutterNfcKit.poll(
timeout: timeout,
iosAlertMessage: "请将手机靠近NFC标签进行写入",
);
await FlutterNfcKit.writeNDEFRecords(records);
await FlutterNfcKit.finish();
} catch (e) {
throw NFCException('写入失败:${e.toString()}');
} finally {
_isWriting = false;
}
}
Future<void> formatTag({
Duration timeout = const Duration(seconds: 30),
}) async {
if (_isWriting) {
throw NFCException('正在写入中,请稍后再试');
}
_isWriting = true;
try {
final availability = await FlutterNfcKit.nfcAvailability;
if (availability == NFCAvailability.disabled) {
throw NFCException('NFC功能已禁用,请在设置中开启');
}
final tag = await FlutterNfcKit.poll(
timeout: timeout,
iosAlertMessage: "请将手机靠近NFC标签进行格式化",
);
// 格式化为空NDEF标签
await FlutterNfcKit.writeNDEFRecords([]);
await FlutterNfcKit.finish();
} catch (e) {
throw NFCException('格式化失败:${e.toString()}');
} finally {
_isWriting = false;
}
}
}
高级写入:擦除、锁定和容量判断
class AdvancedNFCWriter {
static Future<void> writeBusinessCard({
required String name,
required String phone,
required String email,
String? company,
String? title,
String? website,
Duration timeout = const Duration(seconds: 30),
}) async {
final writer = NFCWriter();
// 创建vCard格式的联系人信息
final vCard = _createVCard(
name: name,
phone: phone,
email: email,
company: company,
title: title,
website: website,
);
await writer.writeMIME(
mimeType: 'text/vcard',
data: utf8.encode(vCard),
timeout: timeout,
);
}
static Future<void> writeWiFiConfig({
required String ssid,
required String password,
String security = 'WPA', // WPA, WEP, or nopass
bool hidden = false,
Duration timeout = const Duration(seconds: 30),
}) async {
final writer = NFCWriter();
// 创建WiFi配置字符串
final wifiConfig = 'WIFI:T:$security;S:$ssid;P:$password;H:${hidden ? 'true' : 'false'};;';
await writer.writeURI(
uri: wifiConfig,
timeout: timeout,
);
}
static Future<void> writeLaunchApp({
required String packageName, // Android包名或iOS URL Scheme
Duration timeout = const Duration(seconds: 30),
}) async {
final writer = NFCWriter();
String uri;
if (Platform.isAndroid) {
uri = 'android-app://$packageName';
} else {
uri = packageName; // iOS URL Scheme
}
await writer.writeURI(
uri: uri,
timeout: timeout,
);
}
static Future<void> writeGeoLocation({
required double latitude,
required double longitude,
Duration timeout = const Duration(seconds: 30),
}) async {
final writer = NFCWriter();
final geoUri = 'geo:$latitude,$longitude';
await writer.writeURI(
uri: geoUri,
timeout: timeout,
);
}
static Future<void> writeCustomData({
required String domain,
required String type,
required Map<String, dynamic> data,
Duration timeout = const Duration(seconds: 30),
}) async {
final writer = NFCWriter();
// 创建外部类型记录
final jsonData = jsonEncode(data);
final externalType = 'urn:$domain:$type';
final ndefRecord = NDEFRecord.external(
domain: domain,
type: type,
data: utf8.encode(jsonData),
);
await writer.writeMultipleRecords(
records: [ndefRecord],
timeout: timeout,
);
}
static String _createVCard({
required String name,
required String phone,
required String email,
String? company,
String? title,
String? website,
}) {
final buffer = StringBuffer();
buffer.writeln('BEGIN:VCARD');
buffer.writeln('VERSION:3.0');
buffer.writeln('FN:$name');
buffer.writeln('TEL:$phone');
buffer.writeln('EMAIL:$email');
if (company != null) {
buffer.writeln('ORG:$company');
}
if (title != null) {
buffer.writeln('TITLE:$title');
}
if (website != null) {
buffer.writeln('URL:$website');
}
buffer.writeln('END:VCARD');
return buffer.toString();
}
}
两个完整例子:门禁和防伪
例一:智能门禁
class SmartDoorSystem {
final NFCReader _nfcReader = NFCReader();
final NFCWriter _nfcWriter = NFCWriter();
final DatabaseService _database = DatabaseService();
Future<bool> authenticateUser() async {
try {
// 读取NFC标签
final nfcData = await _nfcReader.readNFC();
if (!nfcData.hasNDEF) {
throw NFCException('无效的门禁卡');
}
// 获取用户ID
final userId = _extractUserId(nfcData);
if (userId == null) {
throw NFCException('无法识别的门禁卡');
}
// 验证用户权限
final user = await _database.getUserById(userId);
if (user == null) {
throw NFCException('用户不存在');
}
if (!user.hasAccess) {
throw NFCException('用户无权限');
}
// 记录访问日志
await _database.logAccess(
userId: userId,
timestamp: DateTime.now(),
action: 'entry',
);
// 开门
await _openDoor();
return true;
} catch (e) {
throw NFCException('门禁认证失败:${e.toString()}');
}
}
Future<void> registerAccessCard({
required String userId,
required String userName,
Duration timeout = const Duration(seconds: 30),
}) async {
try {
// 创建用户数据
final userData = {
'userId': userId,
'userName': userName,
'timestamp': DateTime.now().toIso8601String(),
'type': 'access_card',
};
// 写入NFC标签
await AdvancedNFCWriter.writeCustomData(
domain: 'smartdoor',
type: 'user',
data: userData,
timeout: timeout,
);
// 在数据库中注册
await _database.registerAccessCard(userId, userName);
} catch (e) {
throw NFCException('注册门禁卡失败:${e.toString()}');
}
}
String? _extractUserId(NFCData nfcData) {
// 查找自定义的用户数据记录
final userRecords = nfcData.getNDEFRecordsByType('external');
for (final record in userRecords) {
if (record.content.contains('smartdoor') && record.content.contains('user')) {
try {
final data = jsonDecode(record.content);
return data['userId'];
} catch (e) {
continue;
}
}
}
return null;
}
Future<void> _openDoor() async {
// 控制门锁硬件
// 这里可以通过蓝牙、WiFi或其他方式与门锁通信
await Future.delayed(Duration(milliseconds: 500)); // 模拟开门延迟
}
}
class DatabaseService {
Future<User?> getUserById(String userId) async {
// 模拟数据库查询
await Future.delayed(Duration(milliseconds: 100));
if (userId == 'user123') {
return User(
id: userId,
name: '张三',
hasAccess: true,
);
}
return null;
}
Future<void> logAccess({
required String userId,
required DateTime timestamp,
required String action,
}) async {
// 记录访问日志
print('用户 $userId 在 $timestamp 执行了 $action 操作');
}
Future<void> registerAccessCard(String userId, String userName) async {
// 注册门禁卡
print('注册门禁卡:$userId ($userName)');
}
}
class User {
final String id;
final String name;
final bool hasAccess;
User({
required this.id,
required this.name,
required this.hasAccess,
});
}
例二:产品防伪
class ProductAuthSystem {
final NFCReader _nfcReader = NFCReader();
final NFCWriter _nfcWriter = NFCWriter();
final APIService _apiService = APIService();
Future<ProductInfo> verifyProduct() async {
try {
// 读取NFC标签
final nfcData = await _nfcReader.readNFC();
if (!nfcData.hasNDEF) {
throw NFCException('无效的产品标签');
}
// 提取产品信息
final productInfo = _extractProductInfo(nfcData);
if (productInfo == null) {
throw NFCException('无法识别的产品标签');
}
// 验证产品真伪
final verificationResult = await _apiService.verifyProduct(
productId: productInfo.productId,
signature: productInfo.signature,
);
if (!verificationResult.isValid) {
throw NFCException('产品验证失败,可能是假冒产品');
}
return productInfo;
} catch (e) {
throw NFCException('产品验证失败:${e.toString()}');
}
}
Future<void> registerProduct({
required String productId,
required String productName,
required String batchNumber,
required DateTime productionDate,
Duration timeout = const Duration(seconds: 30),
}) async {
try {
// 生成产品签名
final signature = await _generateSignature(productId);
// 创建产品数据
final productData = {
'productId': productId,
'productName': productName,
'batchNumber': batchNumber,
'productionDate': productionDate.toIso8601String(),
'signature': signature,
'type': 'product_auth',
};
// 写入NFC标签
await AdvancedNFCWriter.writeCustomData(
domain: 'productauth',
type: 'info',
data: productData,
timeout: timeout,
);
// 在服务器注册产品
await _apiService.registerProduct(productData);
} catch (e) {
throw NFCException('注册产品失败:${e.toString()}');
}
}
ProductInfo? _extractProductInfo(NFCData nfcData) {
final productRecords = nfcData.getNDEFRecordsByType('external');
for (final record in productRecords) {
if (record.content.contains('productauth') && record.content.contains('info')) {
try {
final data = jsonDecode(record.content);
return ProductInfo(
productId: data['productId'],
productName: data['productName'],
batchNumber: data['batchNumber'],
productionDate: DateTime.parse(data['productionDate']),
signature: data['signature'],
);
} catch (e) {
continue;
}
}
}
return null;
}
Future<String> _generateSignature(String productId) async {
// 生成数字签名
final timestamp = DateTime.now().millisecondsSinceEpoch;
final data = '$productId:$timestamp';
final signature = _hashData(data);
return signature;
}
String _hashData(String data) {
// 简单的哈希实现,实际应用中应使用安全的哈希算法
final bytes = utf8.encode(data);
final digest = sha256.convert(bytes);
return digest.toString();
}
}
class ProductInfo {
final String productId;
final String productName;
final String batchNumber;
final DateTime productionDate;
final String signature;
ProductInfo({
required this.productId,
required this.productName,
required this.batchNumber,
required this.productionDate,
required this.signature,
});
}
class APIService {
Future<VerificationResult> verifyProduct({
required String productId,
required String signature,
}) async {
// 模拟API调用
await Future.delayed(Duration(milliseconds: 500));
// 这里应该调用实际的验证API
return VerificationResult(
isValid: true,
message: '产品验证通过',
);
}
Future<void> registerProduct(Map<String, dynamic> productData) async {
// 模拟API调用
await Future.delayed(Duration(milliseconds: 500));
print('注册产品:${productData['productId']}');
}
}
class VerificationResult {
final bool isValid;
final String message;
VerificationResult({
required this.isValid,
required this.message,
});
}
UI:读卡页面长什么样
读卡页面:状态机比 UI 重要
class NFCReadPage extends StatefulWidget {
@override
_NFCReadPageState createState() => _NFCReadPageState();
}
class _NFCReadPageState extends State<NFCReadPage> {
final NFCReader _nfcReader = NFCReader();
NFCData? _lastReadData;
bool _isReading = false;
String? _errorMessage;
@override
void dispose() {
_nfcReader.stopReading();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('NFC读取'),
actions: [
IconButton(
icon: Icon(Icons.clear),
onPressed: _clearData,
),
],
),
body: Column(
children: [
_buildStatusCard(),
Expanded(child: _buildContent()),
],
),
floatingActionButton: FloatingActionButton(
onPressed: _toggleReading,
child: Icon(_isReading ? Icons.stop : Icons.nfc),
),
);
}
Widget _buildStatusCard() {
return Card(
margin: EdgeInsets.all(16),
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
Row(
children: [
Icon(
_isReading ? Icons.nfc : Icons.nfc_outlined,
color: _isReading ? Colors.green : Colors.grey,
),
SizedBox(width: 12),
Text(
_isReading ? '正在读取...' : '准备读取',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: _isReading ? Colors.green : Colors.grey,
),
),
],
),
if (_errorMessage != null) ...[
SizedBox(height: 8),
Text(
_errorMessage!,
style: TextStyle(color: Colors.red),
),
],
],
),
),
);
}
Widget _buildContent() {
if (_lastReadData == null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.nfc,
size: 64,
color: Colors.grey,
),
SizedBox(height: 16),
Text(
'请将手机靠近NFC标签',
style: TextStyle(
fontSize: 18,
color: Colors.grey,
),
),
SizedBox(height: 16),
ElevatedButton(
onPressed: _startReading,
child: Text('开始读取'),
),
],
),
);
}
return SingleChildScrollView(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDataCard('基本信息', [
_buildInfoRow('标签类型', _lastReadData!.type),
_buildInfoRow('标签ID', _lastReadData!.id),
]),
if (_lastReadData!.hasNDEF) ...[
SizedBox(height: 16),
_buildNDEFRecordsCard(),
],
if (_lastReadData!.additionalData != null) ...[
SizedBox(height: 16),
_buildAdditionalDataCard(),
],
],
),
);
}
Widget _buildDataCard(String title, List<Widget> children) {
return Card(
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 12),
...children,
],
),
),
);
}
Widget _buildInfoRow(String label, String value) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 80,
child: Text(
'$label:',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
Expanded(
child: Text(
value,
style: TextStyle(fontFamily: 'monospace'),
),
),
],
),
);
}
Widget _buildNDEFRecordsCard() {
return _buildDataCard(
'NDEF记录',
_lastReadData!.ndefRecords!.map((record) {
return ExpansionTile(
title: Text('类型: ${record.type}'),
subtitle: Text('内容: ${record.content.length > 50 ? record.content.substring(0, 50) + '...' : record.content}'),
children: [
Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoRow('类型', record.type),
_buildInfoRow('内容', record.content),
if (record.language != null)
_buildInfoRow('语言', record.language!),
if (record.encoding != null)
_buildInfoRow('编码', record.encoding!),
],
),
),
],
);
}).toList(),
);
}
Widget _buildAdditionalDataCard() {
final additionalData = _lastReadData!.additionalData!;
return _buildDataCard(
'附加数据',
additionalData.entries.map((entry) {
return _buildInfoRow(entry.key, entry.value.toString());
}).toList(),
);
}
Future<void> _toggleReading() async {
if (_isReading) {
_stopReading();
} else {
await _startReading();
}
}
Future<void> _startReading() async {
final hasPermission = await NFCPermissionManager.checkPermissions();
if (!hasPermission) {
final granted = await NFCPermissionManager.requestPermissions();
if (!granted) {
_showPermissionDialog();
return;
}
}
setState(() {
_isReading = true;
_errorMessage = null;
});
try {
final nfcData = await _nfcReader.readNFC();
setState(() {
_lastReadData = nfcData;
_isReading = false;
});
} catch (e) {
setState(() {
_errorMessage = e.toString();
_isReading = false;
});
}
}
void _stopReading() {
_nfcReader.stopReading();
setState(() {
_isReading = false;
});
}
void _clearData() {
setState(() {
_lastReadData = null;
_errorMessage = null;
});
}
void _showPermissionDialog() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('需要权限'),
content: Text('应用需要NFC权限才能读取标签'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('取消'),
),
TextButton(
onPressed: () {
Navigator.pop(context);
NFCPermissionManager.openSettings();
},
child: Text('去设置'),
),
],
),
);
}
}
排错:先查 availability,再看会话状态
异常处理:超时和用户取消是常态
class NFCException implements Exception {
final String message;
final String? code;
final dynamic originalError;
NFCException(this.message, {this.code, this.originalError});
@override
String toString() {
return 'NFCException: $message${code != null ? ' (Code: $code)' : ''}';
}
}
class NFCErrorHandler {
static NFCException handleError(dynamic error) {
if (error is NFCException) {
return error;
}
if (error.toString().contains('NFC is not available')) {
return NFCException(
'设备不支持NFC功能',
code: 'NFC_NOT_AVAILABLE',
);
}
if (error.toString().contains('NFC is disabled')) {
return NFCException(
'NFC功能已禁用,请在设置中开启',
code: 'NFC_DISABLED',
);
}
if (error.toString().contains('Tag was lost')) {
return NFCException(
'NFC标签已移开,请重新放置',
code: 'TAG_LOST',
);
}
if (error.toString().contains('Timeout')) {
return NFCException(
'读取超时,请重试',
code: 'TIMEOUT',
);
}
if (error.toString().contains('Tag is read-only')) {
return NFCException(
'NFC标签为只读,无法写入',
code: 'READ_ONLY',
);
}
if (error.toString().contains('Not enough memory')) {
return NFCException(
'NFC标签容量不足',
code: 'INSUFFICIENT_MEMORY',
);
}
return NFCException(
'未知错误:${error.toString()}',
code: 'UNKNOWN_ERROR',
originalError: error,
);
}
}
调试:真机和标签都得到位
class NFCLogger {
static const String _tag = 'NFC';
static bool _isDebugMode = kDebugMode;
static void log(String message, {LogLevel level = LogLevel.info}) {
if (!_isDebugMode) return;
final timestamp = DateTime.now().toIso8601String();
final logMessage = '[$timestamp] [$_tag] [${level.name.toUpperCase()}] $message';
print(logMessage);
_writeToLogFile(logMessage);
}
static void logRead(NFCData data) {
log('读取NFC标签成功');
log('标签类型: ${data.type}');
log('标签ID: ${data.id}');
if (data.hasNDEF) {
log('NDEF记录数量: ${data.ndefRecords!.length}');
for (int i = 0; i < data.ndefRecords!.length; i++) {
final record = data.ndefRecords![i];
log('记录 $i: 类型=${record.type}, 内容=${record.content}');
}
}
}
static void logWrite(String type, dynamic content) {
log('写入NFC标签: 类型=$type, 内容=$content');
}
static void logError(String error, StackTrace? stackTrace) {
log('错误: $error', level: LogLevel.error);
if (stackTrace != null) {
log('堆栈跟踪: $stackTrace', level: LogLevel.error);
}
}
static void logTransaction(List<int> data, bool isSend) {
final direction = isSend ? '发送' : '接收';
final dataHex = data.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ');
log('$direction数据: $dataHex');
}
static void _writeToLogFile(String message) {
// 实现日志文件写入
}
}
enum LogLevel { debug, info, warning, error }
实战:iOS 上没有”静默读卡”这回事
NFC 这个场景我也没在项目里真正做过,下面的判断来自 nfc_manager 的官方文档,改写成我自己的话,算是给后来人的避坑指南。
第一步永远是先调 checkAvailability:很多设备根本没有 NFC 芯片,或者用户在系统设置里关掉了,不先判就直接开会话,崩溃和白屏都等着你。
nfc_manager 的模型是”会话制”的:startSession 传 pollingOptions(iso14443/iso18092/iso15693 三选),在 onDiscovered 回调里拿到 NfcTag,再用 Ndef.from(tag) 转成跨平台的 Ndef 抽象,用完调 stopSession 关掉。记住会话是有生命周期的,不是常驻监听。
最大的平台差异在 iOS:NFC 会话是系统接管的弹窗,扫到一张卡会话就结束,还会自动超时——“后台贴卡即读”在 iOS 上根本走不通,只有 Android 能做到。所以做门禁、考勤这类产品,立项评审时就要把 iOS 的交互形态单独算进去,别按 Android 的体验去承诺。
另外 Mifare Classic 这类卡要做扇区读写,就得面对两套原生类(MifareClassicAndroid / MiFareIos),跨平台抽象只覆盖了 Ndef 这一层,复杂需求躲不开平台分支。
参考:nfc_manager 的 pub.dev 文档。
写在最后
NFC 这篇我全程是”文档驱动”的——没在项目里真正对接过硬件,所以代码的定位是按 flutter_nfc_kit 文档走通的骨架,真机调试还得你自己来。但有三条判断我很有把握:第一,先调 availability,很多设备根本没有 NFC 芯片,不先判就直接 poll,崩溃和白屏都等着你;第二,iOS 没有静默读卡,交互形态要在立项时单独设计;第三,Mifare Classic 这类卡的扇区读写躲不开平台分支,别指望一个插件全包。
如果只做”碰一碰读个 URL”这种需求,读完读卡和写卡两节就可以动手了;门禁和防伪那两个完整例子是给你抄结构的,不用从头读到尾。
