Flutter × NFC — タグの読み書きを実装してみた
はじめに
Flutter で NFC タグを読み書きするデモアプリを作ってみました。本記事では以下を整理します。
- NFC のユースケース・使いどころ(採用判断の材料)
- Flutter で NFC タグを読み書きする方法(nfc_manager の使い方 + iOS/Android 設定)
最終的に作ったデモです。NFC タグに書き込まれたテキストを読み取って、アプリに表示しています。

NFC タグは こちら を使用しました。
ソースコードは以下のリポジトリで公開しています。
NFC とは
NFC(Near Field Communication)は、4cm 程度の近距離で動作する無線通信の規格です。スマホやカードをかざすだけでデータをやりとりできるのが特徴で、日常で触れる場面も多い技術です。
- Suica / iD / VISA のタッチ決済
- マイナンバーカードの読み取り(コンビニ交付、e-Tax、マイナポータル等)
- 商品やポスターに貼った NFC タグから Web ページを開く
もう一つの特徴は、電池が要らない「タグ」 を扱えることです。タグは読み取り側(スマホなど)が出す電波を電源にして動くため、シール型のタグを数十円〜で配布できます。印刷物や商品パッケージに貼る活用ができるのはこの仕組みがあるためです。
NFC のユースケース
スマホアプリでの NFC と QR コードの使い分け
スマホアプリで「物理的なモノから情報を読み取る」用途では、NFC と QR コードが競合します。どちらを使うかの判断材料として、両者の性質を整理しておきます。
| 観点 | NFC | QR コード |
|---|---|---|
| コスト | タグ1枚数十円〜 | 紙に印刷すれば実質無料 |
| 操作 | かざすだけ | カメラ起動 → 読み取り |
QR コードを選んだ方が良い場面
- 配布規模が大きいマーケティング用途: チラシ、ポスター、店頭 POP など、タグ1枚数十円のコストが重くなる用途
- 読み取り頻度が低い: カメラ起動の手間が運用上許容でき、カメラが使える環境
NFC を選んだ方が良い場面
- 読み取りスピードが重要: タッチするだけで決済・認証が完結
- 環境がカメラに向かない: 暗所、手袋着用、屋外の強光下など
- 既存の IC カードを読み取りたい: マイナンバーカードでの本人確認、社員証や交通系 IC カードの読み取りなど、QR コード化されていない既存カード資産を活用する場面
アプリを作るべきか、Web で完結するか
NFC タグに URL を書き込んでおくと、スマホでかざした時に OS が自動でブラウザを開きます。つまり「タグをかざしたら Web ページが開く」程度の導線であれば、専用アプリを作らなくても URL を書き込んだタグだけで済みます(商品情報への誘導、キャンペーン LP など)。
アプリを作る価値が出てくるのは、以下のような業務要件が絡む場合です。
- オフライン動作: 通信が不安定な現場で、タグ読み取り結果を端末内で処理する必要がある
- 複雑なビジネスロジック: スキャン内容に応じた分岐・バリデーション・他データとの照合
- IC カードの読み取り: マイナンバーカードや社員証など、URI レコードではない IC カードの情報を扱う
スマホアプリで NFC を検討する際は、まず「URL を書き込むだけで済まないか」を確認するのが出発点です。
Flutter での NFC 実装
パッケージ選定
Flutter で NFC を扱う主要パッケージは以下の2つです。どちらも pub.dev で同程度の評価を得ています。
| 項目 | nfc_manager | flutter_nfc_kit |
|---|---|---|
| pub points | 160/160 | 160/160 |
| likes / DL | 522 / 50.5k | 270 / 29.9k |
| 対応 | iOS / Android | iOS / Android / Web |
| API スタイル | コールバック型(onDiscovered) |
Future 型(poll → readNDEF) |
| 最終リリース | 2026-04-04 | 2025 年後半 |
今回は nfc_manager を採用しました。
- メンテナンスが活発: 2026 年も継続的にリリースされており、バックグラウンド復帰時のクラッシュ修正など実運用レベルの課題に追従している
- 実績: likes / DL 数ともに約2倍で、トラブル時に見つかる事例が多い
flutter_nfc_kit の Web 対応は魅力ですが、実機タグの読み書きが目的の今回は決め手になりませんでした。
プラットフォーム設定
Android
AndroidManifest.xml に以下を追加します。
<uses-permission android:name="android.permission.NFC" />
iOS
Info.plist にセッション中に表示されるダイアログの説明文を追加します。
<key>NFCReaderUsageDescription</key>
<string>NFC タグの読み取り・書き込みに使用します</string>
さらに、Xcode の Signing & Capabilities で Near Field Communication Tag Reading を追加し、自動生成される Entitlements ファイルに対応フォーマットを記述します。
<!-- Runner.entitlements -->
<key>com.apple.developer.nfc.readersession.formats</key>
<array>
<string>NDEF</string>
<string>TAG</string>
</array>
NDEF データ構造
実装に入る前に、NFC タグに書き込むデータフォーマットである NDEF(NFC Data Exchange Format)の構造を押さえておきます。NDEF は「メッセージ」が複数の「レコード」を含む入れ子構造です。
NdefMessage
├── NdefRecord #1 (例: テキスト "こんにちは")
├── NdefRecord #2 (例: URI "https://example.com/")
└── ...
レコードは以下の3つの要素で構成されます。
| フィールド | 役割 | 例 |
|---|---|---|
| TNF(Type Name Format) | ペイロードの種別を示す大分類 | wellKnown / absoluteUri など |
| Type | レコード種別の識別子 | T(テキスト)、U(URI) |
| Payload | 実データ(バイト列) | "en\0こんにちは" など |
今回扱うのは NFC Forum が定義した Well-Known レコードのうち、Text レコード(TNF=wellKnown / Type="T") と URI レコード(TNF=wellKnown / Type="U") の2種類です。
可用性チェック
端末が NFC に対応しているか、NFC が ON になっているかを起動時に確認します。
import 'package:nfc_manager/nfc_manager.dart';
final availability = await NfcManager.instance.checkAvailability();
// NfcAvailability.enabled / disabled / unsupported
unsupported は iPad の多くや古い Android 端末、disabled は Android で NFC を OFF にした状態です。
読み取り
nfc_manager では、NFC タグの読み取り・書き込みは「セッション」という単位で行います。大まかな流れは3ステップです。
startSessionでセッションを開始- タグが検出されると
onDiscoveredコールバックが呼ばれるので、その中で読み取り or 書き込みを行う stopSessionでセッションを閉じる
await NfcManager.instance.startSession(
pollingOptions: {
NfcPollingOption.iso14443, // Type 2/4 タグ(NTAG, MIFARE 等)
NfcPollingOption.iso15693, // Type 5 タグ
NfcPollingOption.iso18092, // Type 3 タグ(FeliCa 等)
},
alertMessageIos: 'NFC タグをかざしてください',
onDiscovered: (tag) async {
// プラットフォーム別に NDEF テクノロジに変換する(後述)
final message = await _readNdefMessage(tag);
print('読み取り成功: ${message?.records.length} レコード');
await NfcManager.instance.stopSession(alertMessageIos: '読み取り完了');
},
);
pollingOptions は、検出対象のタグ規格を指定するオプションです。NDEF 対応タグは複数の規格にまたがるため、今回は3種類すべてを指定しています。
プラットフォームの差異
nfc_manager では NDEF アクセスのクラスが iOS / Android で分離されています。それぞれの専用クラス(NdefAndroid / NdefIos)で tag を扱い直す必要があります。
import 'dart:io';
import 'package:nfc_manager/nfc_manager.dart';
import 'package:nfc_manager/nfc_manager_android.dart';
import 'package:nfc_manager/nfc_manager_ios.dart';
Future<NdefMessage?> _readNdefMessage(NfcTag tag) async {
if (Platform.isAndroid) {
final ndef = NdefAndroid.from(tag);
if (ndef == null) throw Exception('NDEF 非対応のタグ');
// 検出時にキャッシュされたメッセージがあれば即返す
return ndef.cachedNdefMessage ?? await ndef.getNdefMessage();
}
if (Platform.isIOS) {
final ndef = NdefIos.from(tag);
if (ndef == null) throw Exception('NDEF 非対応のタグ');
return ndef.cachedNdefMessage ?? await ndef.readNdef();
}
throw UnsupportedError('未対応プラットフォーム');
}
iOS は readNdef()、Android は getNdefMessage() とメソッド名も微妙に違うので、インターフェースを揃えるには自前でラッパーを書く必要があります。
書き込み
書き込みは大きく2ステップです。
- 書き込みたい内容を NDEF レコードに組み立てる
- 組み立てた
NdefMessageをタグに書き込む
1. NDEF レコードを組み立てる
NDEF レコードの payload はバイト列(Uint8List)です。テキスト / URI どちらの場合も、NFC Forum が定めたフォーマット(RTD 仕様)に沿ってバイト列を組み立てる必要があります。
- Text レコード:
[ステータスバイト] + [言語コード] + [本文テキスト]の順でバイト列を並べる - URI レコード: 先頭1バイトで既知のプレフィックス(
https://,tel:など全36種)を圧縮し、残りの URL 文字列を続ける
nfc_manager には、このバイト列組み立て用のヘルパーは付属していません。RTD 仕様に沿って自前で実装する必要があります(具体的なコードはリポジトリを参照)。
2. タグに書き込む
組み立てた NdefMessage をプラットフォームごとの NDEF クラス経由でタグに書き込みます。
Future<void> _writeNdefMessage(NfcTag tag, NdefMessage message) async {
if (Platform.isAndroid) {
final ndef = NdefAndroid.from(tag)!;
if (!ndef.isWritable) throw Exception('読み取り専用タグ');
await ndef.writeNdefMessage(message);
} else if (Platform.isIOS) {
final ndef = NdefIos.from(tag)!;
if (ndef.status != NdefStatusIos.readWrite) {
throw Exception('読み取り専用タグ');
}
await ndef.writeNdef(message);
}
}
まとめ
NFC は便利だが採用場面を選ぶ技術で、Flutter 側の実装自体は nfc_manager があるためそれほど難しくない — というのが本記事の結論です。
ユースケース判断の出発点
- QR コード / Web ページで代替できないかを先に検討する
- 既存の IC カード読み取りなど「NFC でしかできない」場面で採用する
実装時のポイント
- NDEF クラスは iOS / Android で分離されておりプラットフォーム分岐が必要
- Text / URI レコードのバイト列組み立ては自前で実装する







