Originally published on Medium:
https://medium.com/@supsabhi/unlocking-nfc-reading-mifare-cards-with-jetpack-compose-bf250171d1f1
NFC (Near Field Communication) is one of those technologies we use almost daily — without even realising it. From tapping your phone at the supermarket checkout to scanning an access badge at work, NFC has quietly become a part of our everyday lives.
If you’re building an Android app that involves card reading — for public transportation, access control, loyalty programs, or event tickets — understanding how to implement NFC tag reading is essential. In this post, we’ll explore how to read MIFARE Classic tags using Android and Jetpack Compose.
Before diving into the implementation, let’s briefly talk about the types of NFC tags you may encounter. NFC Forum classifies tags into Type 1 to Type 5, and among the most popular families are:
MIFARE Classic — Found in transport cards, hotel keys, and loyalty cards. Widely used, but not the most secure.
MIFARE Ultralight — Low-cost and used for disposable tickets or event passes.
MIFARE DESFire — Designed for higher-security applications, but more expensive.
NTAG Series — Known for better security and compatibility. A solid choice for general-purpose usage.
Each has its pros and cons.Selection of card depends on your app’s needs — security, cost, and data volume.
Let’s build a basic app that can read MIFARE Classic tags. We’ll use Jetpack Compose for UI and handle NFC in a standard way.
Step 1: Add Required Permissions and Features
In your AndroidManifest.xml, declare NFC permissions and features:
<uses-permission android:name="android.permission.NFC" />
<uses-feature android:name="android.hardware.nfc" android:required="true" />
Also, add an intent-filter and meta-data block to capture the tag:
<intent-filter>
<action android:name="android.nfc.action.TECH_DISCOVERED" />
</intent-filter>
<meta-data
android:name="android.nfc.action.TECH_DISCOVERED"
android:resource="@xml/nfc_tech_filter" />
And create a res/xml/nfc_tech_filter.xml file with:
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<tech-list>
<tech>android.nfc.tech.MifareClassic</tech>
</tech-list>
</resources>
Step 2: Capture the Tag in MainActivity
Set up the NFC adapter and foreground dispatch in your MainActivity:
class MainActivity : ComponentActivity() {
private var nfcAdapter: NfcAdapter? = null
private val viewModel: NfcViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
nfcAdapter = NfcAdapter.getDefaultAdapter(this)
setContent {
NfcScreen(viewModel)
}
}
override fun onResume() {
super.onResume()
val intent = Intent(this, javaClass).apply {
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
}
val pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_MUTABLE)
nfcAdapter?.enableForegroundDispatch(this, pendingIntent, null, null)
}
override fun onPause() {
super.onPause()
nfcAdapter?.disableForegroundDispatch(this)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
val tag = intent.getParcelableExtra<Tag>(NfcAdapter.EXTRA_TAG)
tag?.let {
val result = readMifareClassic(it)
viewModel.updateNfcData(result)
}
}
}
Step 3: Read MIFARE Classic Data
This function reads data from each block of a MIFARE Classic tag:
fun readMifareClassic(tag: Tag): String {
val mfc = MifareClassic.get(tag) ?: return "Not a MIFARE Classic tag"
val sb = StringBuilder()
try {
mfc.connect()
val sectorCount = mfc.sectorCount
for (sector in 0 until sectorCount) {
val auth = mfc.authenticateSectorWithKeyA(sector, MifareClassic.KEY_DEFAULT)
if (auth) {
val blockCount = mfc.getBlockCountInSector(sector)
val blockIndex = mfc.sectorToBlock(sector)
for (block in 0 until blockCount) {
val data = mfc.readBlock(blockIndex + block)
sb.append("Sector $sector Block $block: ${data.joinToString(" ") { "%02X".format(it) }}\n")
}
} else {
sb.append("Sector $sector: Authentication failed\n")
}
}
} catch (e: IOException) {
sb.append("Error: ${e.localizedMessage}")
} finally {
try { mfc.close() } catch (_: IOException) {}
}
return sb.toString()
}
Step 4: Store and Display the Data Using ViewModel + Compose
We’ll use a ViewModel to hold the NFC data and update the UI reactively.
class NfcViewModel : ViewModel() {
var nfcData by mutableStateOf("Scan a MIFARE Classic tag")
private set
fun updateNfcData(data: String) {
nfcData = data
}
}
The Jetpack Compose UI:
@Composable
fun NfcScreen(viewModel: NfcViewModel) {
val nfcData = viewModel.nfcData
Column(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Scan MIFARE Tag", fontSize = 24.sp)
Spacer(modifier = Modifier.height(16.dp))
Text(text = nfcData, fontSize = 18.sp, color = Color.DarkGray)
}
}
That’s it — you now have a working Android app that can scan and read MIFARE Classic NFC tags using Jetpack Compose!
This is just the foundation. You can expand this to:Write to NFC tags,Authenticate with custom keys,Handle different tag types (Ultralight, DESFire, NTAG),Encrypt/decrypt data blocks
Final Thoughts
While MIFARE Classic is commonly used, it’s worth noting that it has some known security weaknesses. For sensitive data, consider more secure alternatives like MIFARE DESFire or NTAG cards. Always choose your NFC tech based on the use-case.
Whether you’re building an access control system or creating an innovative event app, integrating NFC can take your user experience to the next level.
Let me know in the comments if you’d like a follow-up post on writing to NFC tags ?
Top comments (0)