Open In App

How to Create a NFC Reader and Writer Android Application

Last Updated : 17 Feb, 2025
Comments
Improve
Suggest changes
Like Article
Like
Report

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

  1. NFC-Enabled Android Device - Your device must support NFC.
  2. Android Studio - Install Android Studio, the official IDE for Android development.
  3. Basic Knowledge of Kotlin - We will be developing this app using the Kotlin programming language.
  4. 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 NdefRecords.
  • 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:



Next Article

Similar Reads