A powerful, easy-to-use Kotlin Multiplatform (KMP) library for reading NFC tags on Android and iOS using Compose Multiplatform.
This library is currently under active development and has not yet been published to Maven Central.
The core functionality is implemented and working on both Android and iOS, but we are still completing testing before the first stable release. The API may change before the official release.
Watch or star this repository to be notified when it is released.
- 🚀 Unified API: A single, clean API to handle NFC scanning on both platforms.
- 🎨 Compose Native: Lifecycle-aware state management that fits perfectly into your Compose UI.
- 🛠️ Fully Customizable:
- Android: Custom Bottom Sheet with support for Lottie animations (via Compottie).
- iOS: Seamless integration with the native system NFC scanning dialog.
- ⚙️ Flexible Configuration: Control timeouts, dismissal behaviors, and UI strings with a type-safe DSL.
- 📊 Detailed Tag Info: Extract Serial Numbers, NDEF payloads, and supported technology lists.
Add the dependency to your commonMain source set in build.gradle.kts:
sourceSets {
commonMain.dependencies {
implementation("com.devtamuno.kmp:nfcreader:<version>")
}
}- Add NFC permissions to your
AndroidManifest.xml:
<uses-permission android:name="android.permission.NFC" />
<uses-feature android:name="android.hardware.nfc" android:required="false" />- Add
NFCReaderUsageDescriptionto yourInfo.plist. - Enable the Near Field Communication Tag Reading capability in your Xcode project.
- Add
NDEFsupport to thecom.apple.developer.nfc.readersession.formatsentitlement.
Create an NfcConfig and pass it to rememberNfcReadManagerState:
val nfcManager = rememberNfcReadManagerState(
config = NfcConfig(
titleMessage = "Ready to Scan",
subtitleMessage = "Hold your tag near the device.",
buttonText = "Cancel",
nfcReadTimeout = 30.seconds
)
)Collect the nfcReadResult and react to different scanning states:
val result by nfcManager.nfcReadResult.collectAsState()
when (val state = result) {
is NfcReadResult.Success -> {
Text("Tag ID: ${state.data.serialNumber}")
Text("Type: ${state.data.type}")
Text("Payload: ${state.data.payload}")
Text("Technologies: ${state.data.techList.joinToString()}")
}
is NfcReadResult.Error -> {
Text("Error: ${state.message}", color = Color.Red)
}
NfcReadResult.Scanning -> {
Text("Scanning for NFC tag...")
}
NfcReadResult.OperationCancelled -> {
Text("Scanning cancelled")
}
NfcReadResult.Initial -> {
Button(onClick = { nfcManager.startScanning() }) {
Text("Start Scanning")
}
}
}| Property | Type | Default | Platform |
|---|---|---|---|
titleMessage |
String |
Required | Android |
subtitleMessage |
String |
Required | Android & iOS |
buttonText |
String |
Required | Android |
nfcReadTimeout |
Duration |
60.seconds (min 5s) |
Android |
nfcUnsupportedMessage |
String |
"NFC is not supported on this device" |
Android |
nfcDisabledMessage |
String |
"NFC is disabled on this device" |
Android |
nfcScanTimeoutMessage |
String |
"NFC scan timed out" |
Android |
nfcSuccessMessage |
String |
"Tag scanned successfully" |
iOS |
sheetGesturesEnabled |
Boolean |
true |
Android |
shouldDismissBottomSheetOnBackPress |
Boolean |
false |
Android |
shouldDismissBottomSheetOnClickOutside |
Boolean |
false |
Android |
nfcScanningAnimationSlot |
@Composable ColumnScope.() -> Unit |
Built-in Lottie animation | Android |
Note: On iOS, only
subtitleMessageis used — it maps directly to the native system NFC scanning dialog message. All other properties are Android-specific.
serialNumber: The tag's unique ID as a hex-encoded string (available on both Android and iOS).type: The tag type as anNfcTagTypeenum — see values below.payload: The decoded string content of the tag.nullif the tag is empty or non-NDEF.techList: A list of hardware technologies detected (e.g.,"Mifare Classic","ISO 14443-3A").
| Value | Description |
|---|---|
NDEF |
NFC Data Exchange Format tag |
NON_NDEF |
Tag that does not contain NDEF data |
MIFARE |
MIFARE-based tag (Classic, Ultralight, DESFire) |
ISO15693 |
ISO 15693 vicinity tag |
ISO7816 |
ISO 7816-4 based smart card or tag |
FELICA |
Sony FeliCa tag (transit/payments) |
| State | Description |
|---|---|
Initial |
No scan has been initiated yet |
Scanning |
Actively scanning for a tag |
Success(data) |
Tag was read successfully |
Error(message) |
An error occurred during scanning |
OperationCancelled |
Scanning was cancelled by the user or system |
| Android Implementation | iOS Implementation |
|---|---|
![]() |
![]() |
Contributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the MIT License - see the LICENSE file for details.

