How I Came Up with the Idea for the Memory Match Game and What You Can Learn from It
Have you ever wanted to create a game that’s both fun and a great way to test your memory? Well, that’s exactly what led me to build the Memory Match Game! Here's the story behind it and what you can learn while creating your own version of this game.
Why I Made This Game:
A few months ago, I was experimenting with Jetpack Compose, which is a powerful toolkit for building Android UIs. I’d already built a couple of basic apps, but I wanted to take it up a notch and create something more interactive. I was looking for an interesting challenge that would allow me to apply different aspects of Jetpack Compose—such as state management, UI layout, animations, and more—in a fun way.
While brainstorming ideas, I remembered a simple but classic card game: the Memory Match Game. The objective of the game is straightforward: flip two cards at a time and try to find matching pairs. It’s the perfect combination of fun and challenge, as it tests your memory and concentration. Plus, it’s easy to play and instantly rewarding when you match all the cards.
I thought, “Why not turn this classic game into a project that will also help me learn and improve my skills in Jetpack Compose?” And so, the journey of creating the Memory Match Game began!
What I Learned from Making the Game:
Working with State in Jetpack Compose:
One of the first things I learned was how to manage state effectively in Jetpack Compose. For this game, I needed to keep track of the state of each card (whether it’s flipped or matched). I also needed to track the player’s score and the elapsed time. By using remember and mutableStateOf, I was able to ensure the UI updates dynamically as the game progresses.
Building Interactive UIs:
Building the Memory Match Game was a great way to get hands-on experience with building interactive UIs. With Jetpack Compose, it's easy to manage clicks, animations, and other user interactions. I used clickable to let players flip the cards and update the UI based on the user’s actions. The game immediately responds to the user’s inputs—making the experience smooth and fun!
Applying Layouts and Design Principles:
Designing the grid layout was a key challenge. I needed to display multiple cards in a way that was visually appealing and easy to interact with. Jetpack Compose's LazyVerticalGrid made it simple to lay out the cards in rows and columns, while allowing for a smooth scrolling experience if the game were to include more cards.
Implementing Animations:
Every time a card is flipped, I wanted it to feel satisfying—so I added smooth animations. Jetpack Compose makes it easy to handle animations, so I could add a flip animation when the card is revealed. To enhance the game's experience, I added smooth animations to reveal cards, making the gameplay feel more polished and visually appealing.
Time and Score Tracking:
One of the coolest features I implemented was the timer and score tracking. As soon as the player flips the first card, the timer starts, and the game shows how much time has passed. Additionally, as players successfully match pairs, the score is updated instantly, giving them real-time feedback on their progress. This taught me a lot about managing multiple states and synchronizing UI changes.
Handling Edge Cases:
Of course, no game is complete without handling edge cases. I needed to make sure the game correctly handles situations like flipping the same card twice, ensuring that the game doesn't freeze or crash. Using LaunchedEffect and coroutines helped me manage delays and timeouts, making the game experience smooth and bug-free.
What You Can Learn from This Project:
State Management in Jetpack Compose:
This game will teach you how to use Jetpack Compose’s state management tools like remember, mutableStateOf, and LaunchedEffect to handle dynamic updates in your app.
UI and Layout Techniques:
You’ll get a hands-on experience with creating flexible, responsive layouts using LazyVerticalGrid and other layout modifiers. This will help you learn how to organize elements on the screen, making your app more user-friendly.
Interactive and Dynamic Features:
You’ll see how to make your app respond to user interactions—like clicks and taps—by updating the UI in real time. It’s the kind of skill that will make your apps feel more alive and responsive.
Animation Basics:
Learning how to implement animations in Jetpack Compose is an essential skill, and this project gives you a great foundation. You’ll get to experiment with animations for smooth transitions, like card flips and timed events.
Working with Time and Delays:
If you’re building any kind of time-based app, knowing how to manage time effectively is important. This project will teach you how to use coroutines and LaunchedEffect for things like timers and delays.
Game Logic and Flow:
Building this game helped me understand how to structure game logic—whether it's checking if two cards match, updating the score, or resetting the game. You’ll learn how to break down complex tasks and build clear, concise logic for your own games or interactive apps.
Let's Build
In this tutorial, you'll build a fully functional memory match game in Jetpack Compose using emojis as cards. The game includes:
- Card flipping
- Match checking
- Win condition
- Time tracking
- Reset button with shuffle
🛠️ Step 1: Add Required Dependencies
In your build.gradle (app):
implementation ("androidx.datastore:datastore-preferences:1.1.7")
🎵 Step 2: Add Sound Files
Place your sound files inside the res/raw/ folder:
📁 Step 3: Create GameDataStore.kt
package com.cdingbihar.memorygame
import android.content.Context
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.first
// DataStore setup
private val Context.dataStore by preferencesDataStore(name = "game_prefs")
object GameDataStore {
private val SCORE_KEY = intPreferencesKey("score")
private val TIME_KEY = intPreferencesKey("time")
suspend fun saveScore(context: Context, score: Int, time: Int) {
context.dataStore.edit { prefs ->
prefs[SCORE_KEY] = score
prefs[TIME_KEY] = time
}
}
suspend fun readScore(context: Context): Pair<Int, Int> {
val prefs = context.dataStore.data.first()
val score = prefs[SCORE_KEY] ?: 0
val time = prefs[TIME_KEY] ?: 0
return Pair(score, time)
}
}
🎚️ Step 4: Create SoundPlayer.kt
When the player matches all pairs, display a celebration dialog:
package com.cdingbihar.memorygame
import android.content.Context
import android.media.MediaPlayer
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
@Composable
fun rememberSoundPlayer(context: Context): (Int) -> Unit {
val ctx = rememberUpdatedState(context)
return remember {
{ soundResId: Int ->
val mediaPlayer = MediaPlayer.create(ctx.value, soundResId)
mediaPlayer?.start()
mediaPlayer?.setOnCompletionListener { it.release() }
}
}
}
🧠 Step 5: Main Game Code (MemoryGameScreen.kt)
Use a grid layout and card styling to display all the cards:
package com.cdingbihar.memorygame
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
data class MemoryCard(
val id: Int,
val emoji: String,
var isFlipped: Boolean = false,
var isMatched: Boolean = false
)
@Composable
fun MemoryGameScreen() {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val emojis = listOf("🐶", "🐱", "🦁", "🐸", "🐵", "🐷")
val soundPlayer = rememberSoundPlayer(context)
var cards = remember { shuffleCards(emojis).toMutableStateList() }
var flippedCard by remember { mutableStateOf<MemoryCard?>(null) }
var matchedPairs by remember { mutableIntStateOf(0) }
var isBusy by remember { mutableStateOf(false) }
var timeElapsed by remember { mutableIntStateOf(0) }
var gameStarted by remember { mutableStateOf(false) }
var showDialog by remember { mutableStateOf(false) }
var lastScore by remember { mutableIntStateOf(0) }
var lastTime by remember { mutableIntStateOf(0) }
// Load last saved score/time when screen starts
LaunchedEffect(Unit) {
val (s, t) = GameDataStore.readScore(context)
lastScore = s
lastTime = t
}
// Timer for elapsed time
LaunchedEffect(gameStarted) {
while (gameStarted) {
delay(1000)
timeElapsed++
}
}
val allFlipped = cards.all { it.isFlipped }
val gameWon = matchedPairs == emojis.size
// Save result and reload last score/time
if (allFlipped) {
LaunchedEffect("saveResult") {
gameStarted = false
scope.launch {
GameDataStore.saveScore(context, matchedPairs, timeElapsed)
val (s, t) = GameDataStore.readScore(context)
lastScore = s
lastTime = t
showDialog = true
}
}
}
// Show Win or Game Over dialog
if (showDialog) {
AlertDialog(
onDismissRequest = {},
title = { Text(if (gameWon) "🎉 You Win!" else "Game Over") },
text = {
Column {
Text("Score: $matchedPairs / ${emojis.size}")
Text("Time: ${timeElapsed}s")
}
},
confirmButton = {
TextButton(onClick = {
cards.clear()
cards.addAll(shuffleCards(emojis))
flippedCard = null
matchedPairs = 0
isBusy = false
timeElapsed = 0
showDialog = false
gameStarted = false
}) {
Text("Play Again")
}
}
)
}
Column(
modifier = Modifier
.fillMaxSize()
.systemBarsPadding()
.padding(16.dp)
) {
Text(
"🐾 Memory Match Game",
fontSize = 24.sp,
fontWeight = FontWeight.Bold
)
Spacer(Modifier.height(8.dp))
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text("Score: $matchedPairs / ${emojis.size}")
Text("Time: ${timeElapsed}s")
}
Spacer(Modifier.height(4.dp))
Text("Last Score: $lastScore, Last Time: ${lastTime}s")
Spacer(Modifier.height(12.dp))
LazyVerticalGrid(
columns = GridCells.Fixed(3),
modifier = Modifier.fillMaxSize()
) {
items(cards, key = { it.id }) { card ->
val rotation by animateFloatAsState(
targetValue = if (card.isFlipped || card.isMatched) 180f else 0f,
label = "rotation"
)
val alpha by animateFloatAsState(
targetValue = if (!card.isMatched && !card.isFlipped) 0f else 1f,
label = "alpha"
)
Box(
modifier = Modifier
.padding(8.dp)
.aspectRatio(1f)
.rotate(rotation)
.background(
color = if (card.isMatched) Color(0xFF81C784) else Color(0xFF90CAF9),
shape = MaterialTheme.shapes.medium
)
.clickable(enabled = !isBusy) {
soundPlayer(R.raw.flip)
if (card.isFlipped && !card.isMatched) {
card.isFlipped = false
if (flippedCard == card) flippedCard = null
return@clickable
}
if (!gameStarted) gameStarted = true
card.isFlipped = true
if (flippedCard == null) {
flippedCard = card
} else {
isBusy = true
if (flippedCard!!.emoji == card.emoji) {
card.isMatched = true
flippedCard!!.isMatched = true
matchedPairs++
flippedCard = null
isBusy = false
soundPlayer(R.raw.match)
} else {
soundPlayer(R.raw.nomatch)
flippedCard = null
isBusy = false
}
}
},
contentAlignment = Alignment.Center
) {
if (card.isFlipped || card.isMatched) {
Text(card.emoji, fontSize = 32.sp, modifier = Modifier.alpha(alpha))
}
}
}
}
}
}
fun shuffleCards(emojis: List<String>): MutableList<MemoryCard> {
val all = (emojis + emojis).shuffled()
return all.mapIndexed { index, emoji -> MemoryCard(id = index, emoji = emoji) }.toMutableStateList()
}
🚀 Step 6: Setup MainActivity
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
MemoryGameTheme {
/* Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
)
}
*/
MemoryGameScreen()
}
}
}
📦 Final Tip
Make sure you use the latest Jetpack Compose dependencies and call MemoryGameScreen()
from your main activity's setContent.
OUTPUT:
Happy coding! 🎯