How to Create a NFC Reader and Writer Android Application
Last Updated :
17 Feb, 2025
NFC that stands for Near Field Communication, is a very short range wireless technology that allows us share small amount of data between devices. Through this technology we can share data between an NFC tag and an android device or between two android devices. A maximum of 4 cm distance is required to establish this connection. In this articles, we will be developing a basic application that involved communicating between an Android Device and a NFC Tag.
Prerequisites
- NFC-Enabled Android Device - Your device must support NFC.
- Android Studio - Install Android Studio, the official IDE for Android development.
- Basic Knowledge of Kotlin - We will be developing this app using the Kotlin programming language.
- NFC Tag - You must have a NFC tag, sticker or a card for testing purposes.
Steps to Create a NFC Reader and Writer Android Application
Creating a Application for NFC Reader and Writer in Android is a complex task, So we will follow steps to create this application.
Step 1: Create a New Project in Android Studio
To create a new project in Android Studio please refer to How to Create/Start a New Project in Android Studio.
Note that you must select Kotlin as the programming language.
Step 2: Adding Permissions in Manifest File
Navigate to app > manifests > AndroidManifest.xml and add the following permissions under the manifest tag.
<uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.VIBRATE" />
Step 3: Working with Main Activity
This activity or screen will include two basic buttons from where we can navigate to two separate activities for specific Read and Write functionalities.
MainActivity.kt
package org.geeksforgeeks.nfcdemogfg
import android.content.Intent
import android.os.Bundle
import android.widget.Button
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val write: Button = findViewById(R.id.write)
val read: Button = findViewById(R.id.read)
// Navigating to write activity
write.setOnClickListener {
val intent = Intent(this, WriteActivity::class.java)
startActivity(intent)
}
// Navigating to read activity
read.setOnClickListener {
val intent = Intent(this, ReadActivity::class.java)
startActivity(intent)
}
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:background="@color/white"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<!-- to navigate to write activity -->
<com.google.android.material.button.MaterialButton
android:id="@+id/write"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:text="Write Data"
android:backgroundTint="@color/green"
app:layout_constraintBottom_toTopOf="@+id/read"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<!-- to navigate to read activity -->
<com.google.android.material.button.MaterialButton
android:id="@+id/read"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="Read Data"
android:backgroundTint="@color/green"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/write"
app:layout_constraintStart_toStartOf="@+id/write"
app:layout_constraintTop_toBottomOf="@+id/write" />
</androidx.constraintlayout.widget.ConstraintLayout>
Design UI (activity_main):
Step 4: Create an activity of NFC write functionality
Let's create an activity to code the logic for NFC Write functionality.
WriteActivity.kt
package org.geeksforgeeks.nfcdemogfg
import android.os.Build
import android.os.Bundle
import android.widget.EditText
import android.widget.Toast
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.app.PendingIntent
import android.content.IntentFilter
import android.nfc.NdefMessage
import android.nfc.NdefRecord
import android.nfc.NfcAdapter
import android.nfc.Tag
import android.nfc.tech.Ndef
import android.nfc.tech.NfcF
class WriteActivity : AppCompatActivity() {
private lateinit var editText: EditText
private var intentFiltersArray: Array<IntentFilter>? = null
private val techListsArray = arrayOf(arrayOf(NfcF::class.java.name))
private val nfcAdapter: NfcAdapter? by lazy {
NfcAdapter.getDefaultAdapter(this)
}
private var pendingIntent: PendingIntent? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_write)
editText = findViewById(R.id.edit_text)
// prepare pending Intent
val intent = Intent(this, javaClass).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
pendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_MUTABLE)
} else {
PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE)
}
val ndef = IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED)
try {
ndef.addDataType("text/plain")
} catch (e: IntentFilter.MalformedMimeTypeException) {
throw RuntimeException("fail", e)
}
intentFiltersArray = arrayOf(ndef)
// Check NFC availability
if (nfcAdapter == null) {
Toast.makeText(this, "NFC not supported", Toast.LENGTH_SHORT).show()
} else if (!nfcAdapter!!.isEnabled) {
Toast.makeText(this, "Please turn on NFC", Toast.LENGTH_SHORT).show()
}
}
override fun onResume() {
super.onResume()
nfcAdapter?.enableForegroundDispatch(this, pendingIntent, intentFiltersArray, techListsArray)
}
// handles new intent delivered to the activity. For eg. NFC Intent
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
try {
val message=editText.text.toString()
if(message != "") {
if (NfcAdapter.ACTION_TECH_DISCOVERED == intent.action
|| NfcAdapter.ACTION_NDEF_DISCOVERED == intent.action
) {
val tag = intent.getParcelableExtra<Tag>(NfcAdapter.EXTRA_TAG) ?: return
val ndef = Ndef.get(tag) ?: return
if (ndef.isWritable) {
val nfcMessage = NdefMessage(
arrayOf(
NdefRecord.createTextRecord("en", message)
)
)
ndef.connect()
ndef.writeNdefMessage(nfcMessage)
ndef.close()
Toast.makeText(applicationContext, "Successfully Written!", Toast.LENGTH_SHORT).show()
}
}
} else {
Toast.makeText(applicationContext, "Write on text box!", Toast.LENGTH_SHORT).show()
}
}
catch (e:Exception) {
Toast.makeText(applicationContext, e.message, Toast.LENGTH_SHORT).show()
}
}
override fun onPause() {
if (this.isFinishing) {
nfcAdapter?.disableForegroundDispatch(this)
}
super.onPause()
}
}
activity_write.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="64dp"
android:background="@color/white"
tools:context=".WriteActivity">
<EditText
android:id="@+id/edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter message..."
android:textColor="@color/black"
android:inputType="text"
android:textSize="24sp"
style="@style/Widget.Material3.Button.OutlinedButton"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
</androidx.constraintlayout.widget.ConstraintLayout>
Design UI (activity_write):
Explanation of the Writer Kotlin Programs
Now, let's breakdown the code to understand how it works.
i). Setup NFC Adapter and Check availability
- The
getDefaultAdapter(this)
method retrieves the NFC adapter for the device. If the device does not support NFC, this method will return null
. - The
by
lazy
keyword ensures that the NFC adapter is only initialized when it is accessed for the first time, reducing unnecessary overhead during app launch.
Kotlin
// Setup NFC Adapter
private val nfcAdapter: NfcAdapter? by lazy {
NfcAdapter.getDefaultAdapter(this)
}
// Check NFC availability
if (nfcAdapter == null) {
Toast.makeText(this, "NFC not supported", Toast.LENGTH_SHORT).show()
} else if (!nfcAdapter!!.isEnabled) {
Toast.makeText(this, "Please turn on NFC", Toast.LENGTH_SHORT).show()
}
ii). Configure Pending Intent
What is a Pending Intent?
- A Pending Intent is a token that grants another application (in this case, the NFC system) permission to execute a predefined intent on your app's behalf.
Why Do We Need It for NFC?
- When an NFC tag is detected, the system needs to launch your app or deliver an intent to it without restarting the activity. The Pending Intent makes sure that this handover is seamless.
Mutable vs Immutable Flags
- On Android 12+ (API 31),
PendingIntent.FLAG_MUTABLE
is required for NFC events, allowing the system to modify the intent.
Kotlin
// initialize pending intent
private var pendingIntent: PendingIntent? = null
// prepare pending intent
val intent = Intent(this, javaClass).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
pendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_MUTABLE)
} else {
PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE)
}
iii). Setting up Intent Filter for NFC discovery
- Intent Filter is a class that specifies the types of intent an activity can handle. In case of NFC, it notifies the activity when an NFC is detected or DISCOVERED in android terms.
- The .addDataType() adds a MIME(Multipurpose Internet Mail Extensions) type data into the Intent Filter to ensure it only handles such type of data (in this case, "text/plain")
Kotlin
val ndef = IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED)
try {
ndef.addDataType("text/plain")
} catch (e: IntentFilter.MalformedMimeTypeException) {
throw RuntimeException("fail", e)
}
intentFiltersArray = arrayOf(ndef)
iv). Setting up Foreground Dispatch
- The enableForegroundDispatch() method ensures that NFC intents are delivered to the app when it is in the foreground.
- The disableForegroundDispatch() method is called during onPause() to release system resources and prevent unintended intent handling when the app is not active.
Kotlin
override fun onResume() {
super.onResume()
nfcAdapter?.enableForegroundDispatch(this, pendingIntent, intentFiltersArray, techListsArray)
}
override fun onPause() {
if (this.isFinishing) {
nfcAdapter?.disableForegroundDispatch(this)
}
super.onPause()
}
v). Setting up New Intent
- The onNewIntent() function is used to handle new intents delivered to the activity (in this case, NFC Intent).
- NDEF (NFC Data Exchange Format) is a standard format for storing data on NFC tags. An
NdefMessage
is a array of one or more NdefRecord
s. - The
NdefRecord.createTextRecord("en", message)
method creates a text record with a language code ("en" for English) and the message which is an input from the user. - The methods ndef.connect(), ndef.writeNdefMessage(), and ndef.close() is used to connect to a tag, write a message into the tag and disconnect after successful write to free system resources respectively.
Kotlin
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
try {
val message=editText.text.toString()
if(message != "") {
if (NfcAdapter.ACTION_TECH_DISCOVERED == intent.action
|| NfcAdapter.ACTION_NDEF_DISCOVERED == intent.action
) {
// Retrieves the Tag object from the intent.
val tag = intent.getParcelableExtra<Tag>(NfcAdapter.EXTRA_TAG) ?: return
val ndef = Ndef.get(tag) ?: return
if (ndef.isWritable) {
val nfcMessage = NdefMessage(
arrayOf(
NdefRecord.createTextRecord("en", message)
)
)
ndef.connect()
ndef.writeNdefMessage(nfcMessage)
ndef.close()
Toast.makeText(applicationContext, "Successfully Written!", Toast.LENGTH_SHORT).show()
}
}
} else {
Toast.makeText(applicationContext, "Write on text box!", Toast.LENGTH_SHORT).show()
}
}
catch (e:Exception) {
Toast.makeText(applicationContext, e.message, Toast.LENGTH_SHORT).show()
}
}
vi). ACTION_NDEF_DISCOVERED
or ACTION_TECH_DISCOVERED
These are triggered when the system detects a tag with NDEF data.
if (NfcAdapter.ACTION_NDEF_DISCOVERED == intent.action || NfcAdapter.ACTION_TECH_DISCOVERED == intent.action)
vii). Extracting the NFC Tag
- The first line helps in retrieving the NFC Tag
from the intent using EXTRA_TAG
.
- The second line converts the tag to an Ndef
object, which provides methods to read/write NDEF data.
val tag = intent.getParcelableExtra<Tag>(NfcAdapter.EXTRA_TAG) ?: returnval ndef = Ndef.get(tag) ?: return
Step 5: Create an activity of NFC read functionality
Let's create an activity to code the logic for NFC Read functionality.
ReadActivity.kt
package org.geeksforgeeks.nfcdemogfg
import android.app.PendingIntent
import android.content.Intent
import android.content.IntentFilter
import android.nfc.NfcAdapter
import android.nfc.Tag
import android.nfc.tech.Ndef
import android.nfc.tech.NfcF
import android.os.Build
import android.os.Bundle
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
class ReadActivity : AppCompatActivity() {
private lateinit var textView: TextView
private val nfcAdapter: NfcAdapter? by lazy {
NfcAdapter.getDefaultAdapter(this)
}
private var pendingIntent: PendingIntent? = null
private var intentFiltersArray: Array<IntentFilter>? = null
private val techListsArray = arrayOf(arrayOf(NfcF::class.java.name))
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_read)
textView = findViewById(R.id.text_view)
// Check NFC availability
if (nfcAdapter == null) {
Toast.makeText(this, "NFC not supported", Toast.LENGTH_SHORT).show()
} else if (!nfcAdapter!!.isEnabled) {
Toast.makeText(this, "Please turn on NFC", Toast.LENGTH_SHORT).show()
}
// Prepare pending intent for NFC detection
val intent = Intent(this, javaClass).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
pendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_MUTABLE)
} else {
PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
)
}
val ndef = IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED)
try {
ndef.addDataType("text/plain")
} catch (e: IntentFilter.MalformedMimeTypeException) {
throw RuntimeException("fail", e)
}
intentFiltersArray = arrayOf(ndef)
}
override fun onResume() {
super.onResume()
// Enable NFC foreground dispatch to listen for tags
nfcAdapter?.enableForegroundDispatch(
this,
pendingIntent,
intentFiltersArray,
techListsArray
)
}
override fun onPause() {
// Disable NFC foreground dispatch
if (this.isFinishing) {
nfcAdapter?.disableForegroundDispatch(this)
}
super.onPause()
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
if (NfcAdapter.ACTION_NDEF_DISCOVERED == intent.action || NfcAdapter.ACTION_TECH_DISCOVERED == intent.action) {
val tag = intent.getParcelableExtra<Tag>(NfcAdapter.EXTRA_TAG) ?: return
val ndef = Ndef.get(tag) ?: return
try {
ndef.connect()
val ndefMessage = ndef.cachedNdefMessage
val records = ndefMessage.records
if (records.isNotEmpty()) {
// Assuming the message is stored in the first record
val messageRecord = records[0]
val message = String(messageRecord.payload).drop(3)
textView.text = message // Set the message to the TextView
}
ndef.close()
} catch (e: Exception) {
Toast.makeText(applicationContext, e.message, Toast.LENGTH_SHORT).show()
}
}
}
}
activity_read.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="64dp"
android:background="@color/white"
tools:context=".ReadActivity">
<TextView
android:id="@+id/text_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Your message..."
android:textColor="@color/black"
android:textSize="24sp"
style="@style/Widget.Material3.Button.OutlinedButton"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
</androidx.constraintlayout.widget.ConstraintLayout>
Design UI(activity_read):
Explanation of Reader Kotlin Program
Now, let's breakdown the code to understand how it works. Since, most of the code is similar to NFC Write code, we will only discuss the code specific for the Read functionality, which is
Extracting the Message
ndef.cachedNdefMessage
retrieves the cached NDEF message from the tag.String(messageRecord.payload)
converts the data stored as a byte array into a string.The method
drop(3)
removes the characters in the first 3 indices, which represent metadata for the text encoding.
Kotlin
ndef.connect()
val ndefMessage = ndef.cachedNdefMessage
val records = ndefMessage.records
if (records.isNotEmpty()) {
// Assuming the message is stored in the first record
val messageRecord = records[0]
val message = String(messageRecord.payload).drop(3)
textView.text = message // Set the message to the TextView
}
Note: Remember to tap on the NFC tag after typing the message to load the data on the tag and tap on the NFC tag again in the Read screen to retrieve the data and display it on the screen.
Refer to the github repository to get the entire code.
Output: