This can be useful if you're building things like:
- Digital ID cards
- Access control systems
- Smart ticketing solutions
- Contactless authentication apps
In this guide, I'll walk through how to implement Android NFC Host Card Emulation in a Flutter project. The idea is simple: when an NFC reader scans the phone, the phone responds just like a smart card would.
Prerequisites
Before starting, make sure you have:
- Flutter installed
- Android Studio installed
- A physical Android device with NFC support
- Android SDK 19+ (required for HCE)
- Basic understanding of Flutter and Android native code
How NFC HCE Works in Android
When an NFC reader scans your device, Android follows a specific flow:
In this implementation we use:
- HostApduService to handle NFC communication
- SharedPreferences to receive data from Flutter
- APDU command processing to respond to the NFC reader
Project Structure
Inside your Flutter project, the Android implementation will look something like this:
android/
└── app/
└── src/
└── main/
├── kotlin/com/example/nfcdemo/
│ └── MyHostApduService.kt
└── res/
├── xml/
│ └── apduservice.xml
└── values/
└── strings.xml
Step 1: Create the HCE Service
The main part of this implementation is the HostApduService. This service is responsible for receiving commands from the NFC reader and sending responses.
Create the following file:
android/app/src/main/kotlin/com/example/nfcdemo/MyHostApduService.kt
// Path: android/app/src/main/kotlin/com/example/nfcdemo/MyHostApduService.kt
package com.example.nfcdemo
import android.content.Context
import android.nfc.cardemulation.HostApduService
import android.os.Bundle
import android.util.Log
import android.content.SharedPreferences
class MyHostApduService : HostApduService() {
companion object {
const val TAG = "MyHceService"
const val STATUS_SUCCESS = "9000"
const val STATUS_FAILED = "6F00"
}
override fun onDeactivated(reason: Int) {
Log.d(TAG, "Service Deactivated: $reason")
}
override fun processCommandApdu(commandApdu: ByteArray, extras: Bundle?): ByteArray {
val prefs = getSharedPreferences("FlutterSharedPreferences",
Context.MODE_PRIVATE)
val savedAID = prefs.getString("flutter.aid", null)
if (savedAID != null && commandApdu.contentEquals(hexToByteArray(savedAID))) {
val userId = prefs.getString("flutter.userid", "DemoUser") ?: "DemoUser"
val cardId = prefs.getString("flutter.cardid", "CARD001") ?: "CARD001"
val response = "$userId|$cardId".toByteArray()
return response + hexToByteArray(STATUS_SUCCESS)
}
return hexToByteArray(STATUS_FAILED)
}
private fun hexToByteArray(hex: String): ByteArray {
val len = hex.length
val data = ByteArray(len / 2)
var i = 0
while (i < len) {
data[i / 2] = (
(Character.digit(hex[i], 16) shl 4) +
Character.digit(hex[i + 1], 16)
).toByte()
i += 2
}
return data
}
}
What this service does
The service listens for APDU commands from the NFC reader.
If the command contains the correct AID, it:
- Reads values from SharedPreferences
- Builds a response string
- Sends it back to the NFC reader
For example, the response might look like this:
user123|card001
This allows Flutter to dynamically control what data the NFC reader receives.
Step 2: Configure the AID
Next, you need to tell Android which AID your service should handle. Create this file:
android/app/src/main/res/xml/apduservice.xml
<host-apdu-service
xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/service_desc"
android:requireDeviceUnlock="false">
<aid-group
android:description="@string/aid_desc"
android:category="other">
<aid-filter android:name="F0010203040506" />
</aid-group>
</host-apdu-service>
MyHostApduService.kt and your NFC reader configuration.
Step 3: Add String Resources
Add the following strings:
android/app/src/main/res/values/strings.xml
<resources>
<string name="service_desc">NFC HCE Service</string>
<string name="aid_desc">Sample AID Group</string>
</resources>
Step 4: Register the Service
Finally, you need to register the service in AndroidManifest.xml:
// Path: android/app/src/main/AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.NFC" />
<uses-feature
android:name="android.hardware.nfc.hce"
android:required="false" />
<application
android:label="NFC Demo"
android:allowBackup="false">
<service
android:name=".MyHostApduService"
android:permission="android.permission.BIND_NFC_SERVICE"
android:exported="true">
<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/apduservice" />
</service>
</application>
</manifest>
Common Issues
NFC Not Triggering
Make sure NFC is enabled on the device and the device supports Host Card Emulation. You can check this in device Settings → Connected Devices → NFC.
Service Not Responding
Usually happens when the AID doesn't match between
MyHostApduService.kt, apduservice.xml,
and your NFC reader configuration. Verify all three match exactly.
Flutter Data Not Appearing
Ensure Flutter saves values in SharedPreferences using the correct keys:
flutter.userid and flutter.cardid.
Conclusion
Android Host Card Emulation is a powerful feature that allows your phone to behave like an NFC smart card. By combining it with Flutter and a small amount of native Android code, you can build applications for things like:
- Contactless authentication
- Digital ID systems
- Access control
- Smart ticketing
The key pieces are HostApduService, AID configuration, and APDU command handling. Once those are set up, your phone can respond to NFC readers just like a physical card.