在Flutter使用NFC
近场通信技术有各种各样的用例,让我们看看我们如何在跨平台的Flutter应用程序中使用它。
NFC究竟是什么?
NFC(近场通信)实际上描述了一系列的通信技术,可以用来在两个设备之间进行短距离的通信(取决于硬件,在我的测试中最多3厘米)。
通常情况下,其中一个设备没有任何形式的电源(例如上面的贴纸,被动部分),由另一个设备无线供电(例如你的手机,主动部分)。
NFC硬件有多种类型,从塑料卡上的贴纸、小钥匙扣令牌到缝有芯片的衣服。实际的芯片也有不同的功能,比如:
- 支持的不同协议/存储格式(后面会有更多介绍)
- 存储大小(从几个字节到32KB不等)
- 读/写速度
- 内置的硬件加密(如CRYPTO 1、3DES、AES128)。
- 读取距离(需要一个特殊的读卡器,例如恩智浦ICODE SLIX,最远可达1.5米)。
- 支持写锁,支持读计数器,支持读锁
ISO/IEC 14443将NFC标签分为5个不同的组别,这取决于上述的特征集。
通常情况下,你会处理第二组兼容的标签,这些标签允许多次读写,大小在48字节到2KB之间(理论上规范允许达到1MB)。
根据你的使用情况,你也可能最终使用遵循ISO/IEC 15693的标签,该标签描述了范围为多米的非接触式芯片卡。
NDEF
NFC数据交换格式,正如其名称所示,描述了一种如何将内容保存在芯片上的方法。
所以,是的,我们终于在谈论软件了 :)
NDEF允许所有兼容设备了解存储在芯片上的数据结构和类型。所有的内容都被保存为信息,一条信息可以包含多个记录。
每条记录以一个头开始,其中包含关于该记录的元数据,例如包括有效载荷的类型和大小信息。头部之后是记录的有效内容。
NDEF Record = [Record HEADER][Record playload]
整洁的部分是头的类型,默认情况下,你有7个类型的值:
- 0: Empty — 一个没有类型和有效内容的记录
- 1: Well-Known (WKT) — NFC论坛RTD中预先定义的类型之一,编码为统一资源名称(URN),如 “urn:wkt:T”,其中T代表文本。
- 2: MIME media-type — RFC 2046中定义的类型,如text/csv
- 3: Absolute URL — 在RFC 3986中定义
- 4: External — 用户定义的值
- 5: Unknown — 未知类型,要求长度为0
- 6: Unchanged — 用于分块有效内容之间或末端(终止)的记录,长度必须为0
- 7: Reserved — 保留给未来使用
你用它做的大多数事情要么属于1-Well-Known、2-MIME或4-External类别。
一个非常常见的4-External记录的例子是Android的 “android.com:pkg”,它被用来打开应用程序。
关于实际的结构和配置还有很多,但现在,我们将结束这个话题,最后看一下Flutter的代码。
Flutter和NFC
你会发现pub.flutter-io.cn上有多个与NFC有点关系的包。
我选择的这个叫nfc_manager,支持Android和iOS,这个包在我们的应用程序中的日常使用被证明是可靠的。
我们将在此展示的项目是基于Flutter的计数器例子,并加入了NFC的元素。
这个想法很简单:你可以在一个标签上存储你当前的计数器值,或者从标签上读取一个值。
标签就像计数器应用的一个持久性存储层。
最酷的一点是,这种存储不在设备上,也不需要任何电源。
必要的设置
对于这两个平台,在我们的例子中是安卓和iOS,你需要配置你的应用程序以便能够使用NFC功能。
上述链接软件包的pub.flutter-io.cn页面已经指出了所需的步骤。
安卓
只要在你的flutter项目中的AndroidManifest.xml文件中添加权限请求,你就可以开始了。
如果你认为NFC是你的应用程序需要使用的一个要求,你也可以为NFC添加一个 “use-feature “标签。
iOS
首先,似乎只有当你拥有一个付费的苹果开发者账户时,你才能玩转NFC。
登录苹果开发者门户,选择
Certificates, Identifiers & Profiles -> Identifiers → Select your app -> Enable NFC Tag Reading -> Save.
一旦你完成了这些,你可能需要同步/下载更新的配置文件(对我来说,它开箱即用)。
下一步是打开XCode,导航到你的项目和下面显示的屏幕。
这应该已经生成了你读写NDEF标签所需的所有文件。
你可能想更新你的.plist文件中的文字,说明你为什么要使用NFC。如果你需要更多的标签类型,请查看pub.flutter-io.cn页面或苹果的这个链接。
值得注意的是
安卓和iOS对NFC的处理方式有很大区别!
安卓用户可以在系统设置中关闭设备的NFC,而iOS不允许用户关闭NFC。安卓用户可以在系统设置中关闭设备的NFC,如果他们愿意,iOS不允许用户关闭NFC。如果有应用程序要求,它将始终处于开启状态。
更重要的是,安卓允许在后台扫描多个标签,而不向用户提供任何视觉指示。
iOS总是一个接一个地扫描标签,并总是将你的应用程序覆盖在下面所示的视图上。这意味着你必须添加某种UI元素来触发阅读,并且一旦扫描了一个标签,就总是启动/停止监听器。
这可能是一个问题,也可能不是,这在很大程度上取决于你的应用程序和所需的工作流程。
示例代码
这个例子的所有代码都在main.dart文件中,下面的片段逐一展示了更相关的部分。为了以防万一,下面是该应用程序在Android上运行的截图。
如何检查NFC是否可用捏
import 'package:nfc_manager/nfc_manager.dart';
//...
void main() async {
WidgetsFlutterBinding.ensureInitialized(); // Required for the line below
isNfcAvalible = await NfcManager.instance.isAvailable();
runApp(const MyApp());
}
//...
该包提供了一个方法来检查NFC是否可用,你可以在Flutter环境准备好后(在第4行完成)或在runApp被调用后随时调用。
如果你是在安卓系统上,并且这个方法返回错误,这可能意味着:
- 该手机没有NFC硬件
- NFC设置是关闭的
- 你没有使用NFC的权限
如果你在iOS设备上得到错误信息,这意味着该设备根本没有NFC硬件。
以NDEF的形式写和读一个标签
NDEF是所有平台上最常用的格式,也是以下代码中唯一使用的格式。如果你需要其他格式,请确保你所有的目标平台都支持该格式。
//...
Future<void> _listenForNFCEvents() async {
//Always run this for ios but only once for android
if (Platform.isAndroid && listenerRunning == false || Platform.isIOS) {
//Android supports reading nfc in the background, starting it one time is all we need
if (Platform.isAndroid) {
_alert(
'NFC listener running in background now, approach tag(s)',
);
//Update button states
setState(() {
listenerRunning = true;
});
}
NfcManager.instance.startSession(
onDiscovered: (NfcTag tag) async {
bool succses = false;
//Try to convert the raw tag data to NDEF
final ndefTag = Ndef.from(tag);
//If the data could be converted we will get an object
if (ndefTag != null) {
// If we want to write the current counter vlaue we will replace the current content on the tag
if (writeCounterOnNextContact) {
//Ensure the write flag is off again
setState(() {
writeCounterOnNextContact = false;
});
//Create a 1Well known tag with en as language code and 0x02 encoding for UTF8
final ndefRecord = NdefRecord.createText(_counter.toString());
//Create a new ndef message with a single record
final ndefMessage = NdefMessage([ndefRecord]);
//Write it to the tag, tag must still be "connected" to the device
try {
//Any existing content will be overrwirten
await ndefTag.write(ndefMessage);
_alert('Counter written to tag');
succses = true;
} catch (e) {
_alert("Writting failed, press 'Write to tag' again");
}
}
//The NDEF Message was already parsed, if any
else if (ndefTag.cachedMessage != null) {
var ndefMessage = ndefTag.cachedMessage!;
//Each NDEF message can have multiple records, we will use the first one in our example
if (ndefMessage.records.isNotEmpty &&
ndefMessage.records.first.typeNameFormat ==
NdefTypeNameFormat.nfcWellknown) {
//If the first record exists as 1:Well-Known we consider this tag as having a value for us
final wellKnownRecord = ndefMessage.records.first;
///Payload for a 1:Well Known text has the following format:
///[Encoding flag 0x02 is UTF8][ISO language code like en][content]
if (wellKnownRecord.payload.first == 0x02) {
//Now we know the encoding is UTF8 and we can skip the first byte
final languageCodeAndContentBytes =
wellKnownRecord.payload.skip(1).toList();
//Note that the language code can be encoded in ASCI, if you need it be carfully with the endoding
final languageCodeAndContentText =
utf8.decode(languageCodeAndContentBytes);
//Cutting of the language code
final payload = languageCodeAndContentText.substring(2);
//Parsing the content to int
final storedCounters = int.tryParse(payload);
if (storedCounters != null) {
succses = true;
_alert('Counter restored from tag');
setState(() {
_counter = storedCounters;
});
}
}
}
}
}
//Due to the way ios handles nfc we need to stop after each tag
if (Platform.isIOS) {
NfcManager.instance.stopSession();
}
if (succses == false) {
_alert(
'Tag was not valid',
);
}
},
// Required for iOS to define what type of tags should be noticed
pollingOptions: {
NfcPollingOption.iso14443,
NfcPollingOption.iso15693,
},
);
}
}
//...example code
让我们从最初的平台检查(第4行至第14行)开始逐节回顾代码。
使用平台类,我们可以检查我们是否在Android上运行,并使用一个本地标志防止启动读取器两次。
如果读取器没有运行,或者我们在iOS上运行,我们使用
所有的读和写都发生在```onDiscovered```回调中,首先检查提供的NfcTag对象。
我们只想处理NDEF标签,请看第20行,我们可以直接调用```Ndef.from(tag)```来检查提供的标签是否可以被解析为NDEF格式。
> 当你调用此方法时,nfc_manager将自动解析标签上的NDEF信息及其记录。
#### 写入
向标签写入数据是通过创建NdefMessage对象并将NdefRecord对象的列表作为内容来完成的(见第30至32行)。
我们将使用Well-Known类型的文本,默认语言编码设置为 "en"。nfc_manager包提供了一些帮助对象来创建所需的数据。
写入数据的最后一步是在我们的NDEF标签对象上调用写入方法,并传递NdefMessage对象(第36行)。
> 请注意:要写一个标签,你首先需要读取它,或者更准确地说,它仍然必须在手机的范围内。
#### 读取
读取NDEF格式的数据发生在第44至63行。
该代码首先检查标签上是否有任何NDEF信息。如果找到了,我们要确保它有记录,并且第一条记录是基于Well-Known格式的。
现在,代码确保有效数据(一串字节)以0x2开始,这是UTF-8编码文本的标志。有效数据应该始终是这样的格式。
[ENCODING_FLAG][ISO_LANGUAGE_CODE][TEXT]
剩下要做的就是将这些字节(我们可以跳过第一个字节)解码为文本,并使用```substring(2)```切断语言代码。
最后一部分与示例应用程序有关(第65至70行),确保文本是一个有效的整数,并将计数器设置为它。
#### 摘要&链接&小知识
NFC可以被整合到Flutter应用程序中,而不需要做很大的改变,而且现在的大部分手机都支持NFC。
NFC标签的存储空间不大,但仍可用于触发功能、分享信息或将用户指向特定的资源。
最重要的是,通过NDEF,所有的兼容设备都可以读取信息,而不局限于一个设备/品牌或应用程序。
例如,在日常工作中使用NFC来触发我们应用中的各种功能,以替代扫描二维码或手动搜索。
对用户来说,最大的好处是NFC不需要摄像头,而且如果需要改变上下文,标签可以被重写。
举个简单的例子:
你想看看一个存储区的内容?把你的手机靠近NFC标签,手机就会向你显示备件的清单,不管你之前做了什么,都不需要你按下按钮(至少在安卓系统上是这样的......)。
Have FUN!
<!-- https://medium.com/flutter-community/nfc-with-flutter-f8c3515cb0e0 -->