A powerful, easy-to-use Kotlin Multiplatform (KMP) library for reading NFC tags on Android and iOS using Compose Multiplatform.
This library is under active development. While we are approaching a stable v1.0.0, the core engine is already functional and powering NFC interactions on both Android and iOS.
- Unified KMP API: A single
NfcReadManagerStatethat handles platform complexities under the hood. - Android Implementation: Robust
NfcAdapterintegration with a customizableModalBottomSheetand Lottie support (via Compottie). - iOS Implementation: Seamless
CoreNFCintegration utilizing the native system scanning dialog. - Advanced Tag Parsing: Standards-compliant NDEF message and record parsing (Text, URI, vCard, Wi-Fi, MIME, External, Smart Poster).
- Polished Sample App: A comprehensive
:composeAppdemonstrating real-world use cases:- Smart URIs: Automatic detection and launching of web links.
- vCard Support: Parsing contact information for easy saving.
- Wi-Fi Config: Extracting SSID and credentials from tags.
- Multi-Record Support: Handles tags containing multiple NDEF records.
- Unit Tested: Comprehensive test suite for all NDEF payload parsers.
- Dokka Documentation: Fully documented API available here.
- CI/CD: Automated unit testing, binary compatibility validation, and publishing infrastructure.
- Official
v1.0.0release to Maven Central. - Instrumentation tests for Android hardware integration.
Note: We are currently at version
0.0.1. The API is stabilizing but may still undergo minor changes before the first official release. Star the repo to stay updated!
- 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>")
implementation("org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose:<lifecycle-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.
Keep NFC manager creation and lifecycle-aware state collection at the route level. Remember the configuration so it is not recreated during recomposition:
@Composable
fun NfcReaderRoute() {
val config = remember {
NfcConfig(
titleMessage = "Ready to Scan",
subtitleMessage = "Hold your tag near the device.",
buttonText = "Cancel",
android = NfcConfig.AndroidOptions(
nfcReadTimeout = 30.seconds,
shouldDismissBottomSheetOnBackPress = true,
),
)
}
val nfcManager = rememberNfcReadManagerState(config)
val result by nfcManager.nfcReadResult.collectAsStateWithLifecycle()
NfcReaderScreen(
result = result,
onStartScanning = nfcManager::startScanning,
)
}This example uses collectAsStateWithLifecycle from lifecycle-runtime-compose. You can also pass nfcScanningAnimationSlot to rememberNfcReadManagerState to replace the default Android scanning animation.
Pass the result and user actions into a stateless composable. This keeps previews and UI tests independent from NFC hardware:
@Composable
fun NfcReaderScreen(
result: NfcReadResult,
onStartScanning: () -> Unit,
) {
Column {
Button(
onClick = onStartScanning,
enabled = result != NfcReadResult.Scanning,
) {
Text("Start scanning")
}
when (result) {
is NfcReadResult.Success -> {
Text("Tag ID: ${result.data.serialNumber}")
Text("NDEF records: ${result.data.parsedPayloads.size}")
}
is NfcReadResult.Error -> {
Text("Error: ${result.message}", color = Color.Red)
// Use result.error for typed recovery logic when needed.
}
NfcReadResult.Initial -> Text("Ready to scan")
NfcReadResult.Scanning -> Text("Scanning for an NFC tag…")
NfcReadResult.OperationCancelled -> Text("Scanning cancelled")
}
}
}See the composeApp sample for complete tag metadata, structured payload cards, URI handling, custom payload fallbacks, and masked Wi-Fi credentials.
NfcConfig groups configuration values by platform to make it clear where they are used, while still allowing them to be configured from common code using nested AndroidOptions and IosOptions.
These properties are root-level and apply to both platforms (where supported).
| Property | Type | Default |
|---|---|---|
titleMessage |
String |
Required |
subtitleMessage |
String |
Required |
buttonText |
String |
Required |
nfcUnsupportedMessage |
String |
"NFC is not supported on this device" |
nfcDisabledMessage |
String |
"NFC is disabled on this device" |
nfcReadErrorMessage |
String |
"Unable to read the NFC tag" |
ndefParser |
NdefParser |
NdefParser.Default |
android |
AndroidOptions |
AndroidOptions() |
ios |
IosOptions |
IosOptions() |
Accessed via config.android.
| Property | Type | Default | Validation |
|---|---|---|---|
nfcReadTimeout |
Duration |
60.seconds |
Min 5s |
nfcScanTimeoutMessage |
String |
"NFC scan timed out" |
Non-blank |
sheetGesturesEnabled |
Boolean |
true |
- |
shouldDismissBottomSheetOnBackPress |
Boolean |
false |
- |
shouldDismissBottomSheetOnClickOutside |
Boolean |
false |
- |
Accessed via config.ios.
| Property | Type | Default | Validation |
|---|---|---|---|
nfcSuccessMessage |
String |
"Tag scanned successfully" |
Non-blank |
Note: All string properties (
titleMessage,subtitleMessage,buttonText, etc.) are validated to be non-blank. On iOS, onlysubtitleMessageandios.nfcSuccessMessageare used by the native system dialog.titleMessageandbuttonTextare required for the common UI but are ignored by CoreNFC.
serialNumber: The tag's unique ID as a hex-encoded string.type: The tag type as anNfcTagTypeenum.payload: A combined string representation of all payloads (fallback).techList: A list of hardware technologies detected (e.g.,"Mifare Classic").parsedPayloads: AList<ParsedNfcPayload>containing structured data (Text, URI, Wifi, etc.).
| 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, error) |
Scanning failed with a configured message and typed NfcError |
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.

