DEV Community

myougaTheAxo
myougaTheAxo

Posted on

NFC Reading Complete Guide — NDEF/Tag Detection/Compose Integration

What You'll Learn

NFC reading (NDEF read/write, tag detection, foreground dispatch, Compose UI integration) explained.


Manifest Configuration

<manifest>
    <uses-permission android:name="android.permission.NFC" />
    <uses-feature android:name="android.hardware.nfc" android:required="true" />

    <activity android:name=".MainActivity">
        <intent-filter>
            <action android:name="android.nfc.action.NDEF_DISCOVERED" />
            <category android:name="android.intent.category.DEFAULT" />
            <data android:mimeType="text/plain" />
        </intent-filter>
    </activity>
</manifest>
Enter fullscreen mode Exit fullscreen mode

NfcManager

class NfcManager(private val activity: ComponentActivity) {
    private val nfcAdapter: NfcAdapter? = NfcAdapter.getDefaultAdapter(activity)

    val isNfcAvailable: Boolean get() = nfcAdapter != null
    val isNfcEnabled: Boolean get() = nfcAdapter?.isEnabled == true

    private val _tagData = MutableSharedFlow<NfcTagData>(replay = 1)
    val tagData: SharedFlow<NfcTagData> = _tagData

    fun enableForegroundDispatch() {
        val intent = Intent(activity, activity::class.java).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
        val pendingIntent = PendingIntent.getActivity(activity, 0, intent, PendingIntent.FLAG_MUTABLE)
        val filters = arrayOf(IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED).apply {
            addDataType("*/*")
        })
        nfcAdapter?.enableForegroundDispatch(activity, pendingIntent, filters, null)
    }

    fun disableForegroundDispatch() {
        nfcAdapter?.disableForegroundDispatch(activity)
    }

    fun handleIntent(intent: Intent) {
        if (intent.action == NfcAdapter.ACTION_NDEF_DISCOVERED ||
            intent.action == NfcAdapter.ACTION_TAG_DISCOVERED) {
            val tag = if (Build.VERSION.SDK_INT >= 33) {
                intent.getParcelableExtra(NfcAdapter.EXTRA_TAG, Tag::class.java)
            } else {
                @Suppress("DEPRECATION") intent.getParcelableExtra(NfcAdapter.EXTRA_TAG)
            }
            val messages = if (Build.VERSION.SDK_INT >= 33) {
                intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES, NdefMessage::class.java)
            } else {
                @Suppress("DEPRECATION") intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)
            }

            val records = messages?.flatMap { (it as NdefMessage).records.toList() } ?: emptyList()
            val textContent = records.mapNotNull { record ->
                if (record.tnf == NdefRecord.TNF_WELL_KNOWN) String(record.payload, Charsets.UTF_8)
                else null
            }

            _tagData.tryEmit(NfcTagData(
                id = tag?.id?.toHexString() ?: "",
                techList = tag?.techList?.toList() ?: emptyList(),
                content = textContent
            ))
        }
    }
}

data class NfcTagData(
    val id: String,
    val techList: List<String>,
    val content: List<String>
)

fun ByteArray.toHexString() = joinToString("") { "%02X".format(it) }
Enter fullscreen mode Exit fullscreen mode

Compose Screen

@Composable
fun NfcReaderScreen(nfcManager: NfcManager) {
    val tagData by nfcManager.tagData.collectAsStateWithLifecycle(initialValue = null)
    val lifecycleOwner = LocalLifecycleOwner.current

    DisposableEffect(lifecycleOwner) {
        val observer = LifecycleEventObserver { _, event ->
            when (event) {
                Lifecycle.Event.ON_RESUME -> nfcManager.enableForegroundDispatch()
                Lifecycle.Event.ON_PAUSE -> nfcManager.disableForegroundDispatch()
                else -> {}
            }
        }
        lifecycleOwner.lifecycle.addObserver(observer)
        onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
    }

    Column(
        Modifier.fillMaxSize().padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        if (!nfcManager.isNfcAvailable) {
            Text("This device doesn't support NFC", color = MaterialTheme.colorScheme.error)
        } else if (!nfcManager.isNfcEnabled) {
            Text("NFC is disabled. Enable it in settings")
        } else {
            Icon(Icons.Default.Nfc, null, Modifier.size(64.dp), tint = MaterialTheme.colorScheme.primary)
            Text("Tap an NFC tag", style = MaterialTheme.typography.headlineSmall)
        }

        tagData?.let { data ->
            Spacer(Modifier.height(24.dp))
            Card(Modifier.fillMaxWidth()) {
                Column(Modifier.padding(16.dp)) {
                    Text("Tag ID: ${data.id}", style = MaterialTheme.typography.titleMedium)
                    Text("Tech: ${data.techList.joinToString()}", style = MaterialTheme.typography.bodySmall)
                    data.content.forEach { Text("Content: $it") }
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Summary

Feature Implementation
Tag detection enableForegroundDispatch
NDEF reading NdefMessage.records
NFC state NfcAdapter.isEnabled
Compose integration SharedFlow + collectAsState
  • enableForegroundDispatch detects tags when app is foreground
  • Extract text/URI/MIME data from NdefRecord
  • Auto-switch on Lifecycle resume/pause
  • Display NFC availability properly in UI

Ready-Made Android App Templates

8 production-ready Android app templates with Jetpack Compose, MVVM, Hilt, and Material 3.

Browse templatesGumroad

Top comments (0)