A powerful, easy-to-use Kotlin Multiplatform (KMP) library for reading NFC tags on Android and iOS using Compose Multiplatform.
- 🚀 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.
You can use the declarative DSL to configure the reader:
val nfcManager = rememberNfcReadManagerState {
titleMessage = "Ready to Scan"
subtitleMessage = "Hold your tag near the device."
buttonText = "Cancel"
nfcReadTimeout = 30.seconds
// Custom Lottie animation for Android (optional)
nfcScanningAnimationSlot = {
ScanningAnimationDefault.NfcScanningAnimation()
}
}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("Payload: ${state.data.payload}")
}
is NfcReadResult.Error -> {
Text("Error: ${state.message}", color = Color.Red)
}
NfcReadResult.OperationCancelled -> {
Text("Scanning cancelled by user")
}
NfcReadResult.Initial -> {
Button(onClick = { nfcManager.startScanning() }) {
Text("Start Scanning")
}
}
}| Property | Type | Default | Platform |
|---|---|---|---|
titleMessage |
String |
"Ready to Scan" |
Android |
subtitleMessage |
String |
"Hold your tag near the device." |
Android & iOS |
buttonText |
String |
"Cancel" |
Android |
nfcReadTimeout |
Duration |
60.seconds |
Android |
sheetGesturesEnabled |
Boolean |
true |
Android |
shouldDismissBottomSheetOnBackPress |
Boolean |
false |
Android |
shouldDismissBottomSheetOnClickOutside |
Boolean |
false |
Android |
nfcScanningAnimationSlot |
Composable |
Default Animation | Android |
serialNumber: The tag's unique ID (Hex string). Note: Android only.type: EitherNDEForNON_NDEF.payload: The decoded string content of the tag.techList: A list of hardware technologies detected (e.g.,Mifare Classic,ISO 14443-3A).
| 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.

