Best Tutorial for Android Jetpack Compose

Android App Development

Stay ahead with the latest tools, trends, and best practices in Android development

How to Build a Tetris Game in Jetpack Compose

How to Build a Tetris Game in Jetpack Compose - Coding Bihar
How to Build a Tetris Game in Jetpack Compose

🧩 What is Tetris?

Tetris is a classic block-stacking puzzle game where players guide falling shapes, known as Tetrominoes, to fit perfectly into horizontal rows. Complete a line, and it disappears—giving you more room to stack. Fail to clear, and the screen fills up, leading to game over.

It’s simple, endlessly playable, and one of the most iconic games in history.

πŸ’₯ Why is Tetris So Popular?

Despite being around for nearly four decades, Tetris is still wildly popular. Here's why:

✅ Easy to pick up: You don’t need a tutorial to start.
✅ Hard to master: High-level strategies like T-spins and combos add serious depth.
πŸ“± Cross-platform: From arcade machines to Android phones, it runs everywhere.
🧠 Good for your brain: Studies suggest it helps with spatial reasoning and focus.
πŸ” Infinite replayability: Every game is unique thanks to randomness and increasing difficulty.

“Tetris is the purest form of game design.” – Every game dev ever

πŸ›️ The past History of Tetris

Believe it or not, Tetris didn’t come from Silicon Valley or Japan.
Inspired by a puzzle game using pentominoes (shapes made of five squares), he simplified it to four-block pieces and called it Tetris—a blend of “tetra” (Greek for four) and tennis, his favorite sport.

The game started as a simple ASCII program on a Russian Electronika 60 computer. No graphics. Just addictive gameplay.

🌍 Global Boom: How Tetris Took Over the World

The game silently spread across Moscow’s computing circles and made its way to Hungary. In 1989,It was tried to licensed by a British developer and soon, Nintendo bundled it with the Game Boy in 1989.

That decision changed gaming forever. Kids played it in cars. Adults played it in waiting rooms. It transcended language, age, and platform.

Build a Screen Recorder Android App using Jetpack Compose 

πŸ“± Tetris in the Modern Age (1990s to 2020s)

Tetris has been reinvented across every era:

  • 1990s–2000s: Versions on NES, Sega, Game Boy Advance, and PCs. Multiplayer and 3D Tetris arrived.
  • Mobile Era: Played on Nokia brick phones. Later, smartphones brought touch controls and vibrant redesigns.
  • Online & AI Era: Games like Tetris 99 introduced battle royale mechanics.
AI bots mastered infinite play through neural networks and reinforcement learning.
How to Build a Tetris Game in Jetpack Compose

πŸ› ️ How to Build a Tetris Game in Jetpack Compose

Ready to build your own Tetris game on Android? Jetpack Compose makes it smoother than ever, offering a modern declarative UI framework with animations and recomposition out-of-the-box.

Let’s break down the process.

Tetra Fall Game in Jetpack Compose
In this tutorial, we’ll build a fully working Tetris Game using Jetpack Compose, Kotlin, and ViewModel. You’ll learn:
  • How to render a grid
  • How to move and rotate Tetris blocks (Tetrominoes)
  • How to manage score and game-over
  • How to build a real-time game loop using coroutines
  • Bonus: Add repeatable touch input and restart button

πŸ“ Step 1: Define the Cell and Tetromino Classes

Cell.kt

 package com.example.tetrafall

data class Cell(
    var isFilled: Boolean = false,
    var color: Int = 0xFF000000.toInt() // Default: black
)

Tetromino.kt

package com.example.tetrafall

enum class TetrominoType { I, O, T, S, Z, J, L }

data class Tetromino(
    val type: TetrominoType,
    val color: Int,
    val shape: List>
)

fun getTetromino(type: TetrominoType): Tetromino {
    return when (type) {
        TetrominoType.I -> Tetromino(type, 0xFF00BCD4.toInt(), listOf(0 to 0, 1 to 0, 2 to 0, 3 to 0))
        TetrominoType.O -> Tetromino(type, 0xFFFFEB3B.toInt(), listOf(0 to 0, 1 to 0, 0 to 1, 1 to 1))
        TetrominoType.T -> Tetromino(type, 0xFFE91E63.toInt(), listOf(1 to 0, 0 to 1, 1 to 1, 2 to 1))
        TetrominoType.S -> Tetromino(type, 0xFF4CAF50.toInt(), listOf(1 to 0, 2 to 0, 0 to 1, 1 to 1))
        TetrominoType.Z -> Tetromino(type, 0xFFF44336.toInt(), listOf(0 to 0, 1 to 0, 1 to 1, 2 to 1))
        TetrominoType.J -> Tetromino(type, 0xFF3F51B5.toInt(), listOf(0 to 0, 0 to 1, 1 to 1, 2 to 1))
        TetrominoType.L -> Tetromino(type, 0xFFFF9800.toInt(), listOf(2 to 0, 0 to 1, 1 to 1, 2 to 1))
    }
}

Step 2: Create the GameViewModel

GameViewModel.kt

Handles grid, score, game logic, movement, and game loop.
package com.example.tetrafall

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch

class GameViewModel : ViewModel() {
    val rows = 20
    val cols = 10
    val grid = List(rows) { MutableList(cols) { Cell() } }

    var currentShape by mutableStateOf(getRandomShape())
    var shapePositionX by mutableIntStateOf(3)
    var shapePositionY by mutableIntStateOf(0)
    var score by mutableIntStateOf(0)
    private val _isPaused = MutableStateFlow(false)
    val isPaused: StateFlow = _isPaused
    private var gameOver by mutableStateOf(false)

    init {
        startGameLoop()
    }

    private fun startGameLoop() {
        viewModelScope.launch {
            while (true) {
                delay(600)
                if (!isPaused.value && !gameOver) {
                    moveDown()
                }
            }
        }
    }

    fun togglePause() {
        _isPaused.value = !_isPaused.value
    }

    fun moveDown() {
        if (canMove(0, 1)) {
            shapePositionY++
        } else {
            placeShape()
            clearFilledRows()
            spawnNewShape()

            // ✅ Now check game over by scanning top rows
            if (isGameOver()) {
                gameOver = true
            }
        }
    }

    fun moveLeft() {
        if (canMove(-1, 0)) shapePositionX--
    }

    fun moveRight() {
        if (canMove(1, 0)) shapePositionX++
    }

    fun rotateCurrentShape() {
        val rotated = rotateShape(currentShape.shape)
        if (canMove(0, 0, rotated)) {
            currentShape = currentShape.copy(shape = rotated)
        }
    }

    fun instantDrop() {
        while (canMove(0, 1)) shapePositionY++
        moveDown()
    }

    private fun canMove(
        dx: Int,
        dy: Int,
        shape: List> = currentShape.shape
    ): Boolean {
        return shape.all { (x, y) ->
            val newX = shapePositionX + x + dx
            val newY = shapePositionY + y + dy
            newX in 0 until cols && newY in 0 until rows && !grid[newY][newX].isFilled
        }
    }

    private fun placeShape() {
        currentShape.shape.forEach { (x, y) ->
            val gx = shapePositionX + x
            val gy = shapePositionY + y
            if (gx in 0 until cols && gy in 0 until rows) {
                grid[gy][gx] = Cell(true, currentShape.color)
            }
        }
    }

    private fun clearFilledRows() {
        val newGrid = mutableListOf>()

        // Step 1: Keep only rows that are NOT completely filled
        for (row in grid) {
            if (!row.all { it.isFilled }) {
                newGrid.add(row.toMutableList())
            }
        }

        // Step 2: Calculate how many rows were cleared
        val clearedRows = rows - newGrid.size

        // Step 3: Add new empty rows at the top
        repeat(clearedRows) {
            newGrid.add(0, MutableList(cols) { Cell() })
        }

        // Step 4: Copy newGrid back to the actual grid
        for (y in 0 until rows) {
            for (x in 0 until cols) {
                grid[y][x] = newGrid[y][x]
            }
        }

        // Step 5: Update score
        score += clearedRows * 100
    }

    fun isGameOver(): Boolean {
        return currentShape.shape.any { (x, y) ->
            val gx = 3 + x // the spawn position X
            val gy = 0 + y // the spawn position Y
            gx !in 0 until cols || gy !in 0 until rows || grid[gy][gx].isFilled
        }
    }

    private fun spawnNewShape() {
        currentShape = getRandomShape()
        shapePositionX = 3
        shapePositionY = 0
    }

    fun resetGame() {
        for (y in 0 until rows) {
            for (x in 0 until cols) {
                grid[y][x] = Cell()
            }
        }
        score = 0
        gameOver = false
        spawnNewShape()
    }

    private fun getRandomShape(): Tetromino {
        val types = TetrominoType.entries.toTypedArray()
        return getTetromino(types.random())
    }
}

Shape Rotation Logic

package com.example.tetrafall

fun rotateShape(shape: List>): List> {
    val rotated = shape.map { (x, y) -> -y to x }
    val minX = rotated.minOf { it.first }
    val minY = rotated.minOf { it.second }
    return rotated.map { (x, y) -> (x - minX) to (y - minY) }
}

Step 3: Build the Tetris UITetrisScreen.kt

package com.example.tetrafall

import androidx.compose.foundation.background
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.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp


@Composable
fun TetrisScreen(viewModel: GameViewModel = remember { GameViewModel() }) {
    val grid = viewModel.grid
    val shape = viewModel.currentShape
    val offsetX = viewModel.shapePositionX
    val offsetY = viewModel.shapePositionY
    val score = viewModel.score
    val isGameOver = viewModel.isGameOver()

    Column(
        modifier = Modifier.fillMaxSize().padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Tetris", style = MaterialTheme.typography.headlineLarge)
        Text("Score: $score", style = MaterialTheme.typography.headlineMedium)

        Spacer(modifier = Modifier.height(12.dp))

        // Game Grid
        Box(modifier = Modifier.background(Color.Black)) {
            Column {
                for (y in 0 until viewModel.rows) {
                    Row {
                        for (x in 0 until viewModel.cols) {
                            val isShapeCell = shape.shape.any { (dx, dy) ->
                                dx + offsetX == x && dy + offsetY == y
                            }
                            val cell = grid[y][x]
                            val color = when {
                                isShapeCell -> Color(shape.color)
                                cell.isFilled -> Color(cell.color)
                                else -> Color.DarkGray
                            }
                            Box(
                                modifier = Modifier.size(24.dp).padding(1.dp).background(color)
                            )
                        }
                    }
                }
            }
        }

        Spacer(modifier = Modifier.height(24.dp))
        MovementControls(viewModel)

        Row {
            Button(onClick = { viewModel.rotateCurrentShape() }) { Text("⟳ Rotate") }
            Spacer(modifier = Modifier.width(8.dp))
            Button(onClick = { viewModel.instantDrop() }) { Text("Drop ⬇") }
        }
    }
        Spacer(modifier = Modifier.height(16.dp))

        if (isGameOver) {        

        RewardScreen(
            score = viewModel.score,
            //   highScore = viewModel.highScore,
            onPlayAgain = {
                //         viewModel.scoreSaved = false
                viewModel.resetGame()
            },
            modifier = Modifier
                .fillMaxSize()
                .background(Color.White.copy(alpha = 0.95f)) // optional dimming
        )

    }
}

Step 4: Add Controls with Repeat Touch

package com.example.tetrafall

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay

@Composable
fun MovementControls(viewModel: GameViewModel) {
    val isPaused by viewModel.isPaused.collectAsState() // ✅ Collect the StateFlow
    Row {
        RepeatPressButton("←") { viewModel.moveLeft() }
        Spacer(modifier = Modifier.width(8.dp))
        Text((if (isPaused) "▶️" else "⏸️") ,fontSize = 34.sp,
            modifier = Modifier.clickable { viewModel.togglePause() })
        Spacer(modifier = Modifier.width(8.dp))
        RepeatPressButton("→") { viewModel.moveRight() }
    }
}

@Composable
fun RepeatPressButton(text: String, onRepeat: () -> Unit) {
    val isPressed = remember { mutableStateOf(false) }

    LaunchedEffect(isPressed.value) {
        if (isPressed.value) {
            while (isPressed.value) {
                onRepeat()
                delay(100)
            }
        }
    }

    Box(
        modifier = Modifier
            .background(Color.Gray)
            .padding(horizontal = 16.dp, vertical = 8.dp)
            .pointerInput(Unit) {
                detectTapGestures(
                    onPress = {
                        isPressed.value = true
                        tryAwaitRelease()
                        isPressed.value = false
                    }
                )
            }
    ) {
        Text(text = text, color = Color.White)
    }
}

Step 5: MaiinActiivityMainActivity.kt

package com.example.tetrafall

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.example.tetrafall.ui.theme.TetraFallTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            TetraFallTheme {
                /*Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    Greeting(
                        name = "Android",
                        modifier = Modifier.padding(innerPadding)
                    )
                }*/

                TetrisScreen()
            }
        }
    }
}

πŸ§ͺStep 5: RewardScreen

package com.example.tetrafall

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

@Composable
fun RewardScreen(score: Int, onPlayAgain: () -> Unit,modifier: Modifier) {
    Column(
        modifier = modifier
            .fillMaxSize()
            .background(Color.White),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("πŸŽ‰ Game Over!", fontSize = 30.sp, fontWeight = FontWeight.Bold)
        Text("Your Score: $score", fontSize = 24.sp)

        Spacer(modifier = Modifier.height(20.dp))

        Button(onClick = onPlayAgain) {
            Text("Play Again")
        }
    }
}

Output:

Tetra Fall Game Screenshot
To show the next Tetris block (Tetromino) in your Jetpack Compose Tetris game, you can follow these steps:

✅ Step-by-Step: Add a "Next Block" Preview

🧠 Step 1: Update GameViewModel
Add a new variable to store the next shape:
var nextShape by mutableStateOf(getRandomShape())
Update spawnNewShape() to use it:
fun spawnNewShape() {
    currentShape = nextShape
    nextShape = getRandomShape()
    shapePositionX = 3
    shapePositionY = 0
}

🧩 Step 2: Add a Composable for Preview

Create a new composable:
@Composable
fun NextBlockPreview(nextShape: Tetromino) {
    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        Text("Next", style = MaterialTheme.typography.bodyLarge)
        Box(
            modifier = Modifier
                .size(100.dp)
                .background(Color.LightGray)
                .padding(4.dp)
        ) {
            val blockSize = 20.dp
            val shape = nextShape.shape

            for ((x, y) in shape) {
                Box(
                    modifier = Modifier
                        .offset(x.dp * 20, y.dp * 20)
                        .size(blockSize)
                        .background(Color(nextShape.color))
                        .border(1.dp, Color.Black)
                )
            }
        }
    }
}

πŸ–Ό️ Step 3: Place It in TetrisScreen

Above the main game grid, show the preview:
Row(
    modifier = Modifier.fillMaxWidth(),
    horizontalArrangement = Arrangement.SpaceBetween
) {
    Column {
        Text("Tetris", style = MaterialTheme.typography.headlineLarge)
        Text("Score: $score", style = MaterialTheme.typography.headlineMedium)
    }

    NextBlockPreview(viewModel.nextShape)
}
✅ Final Output
Your screen will now show:
  • Current score
  • Next block in a small box (top right)
  • Main Tetris grid
  • Controls
Tetra Fall Game Screenshot 2


Tetra Fall Game Screenshot 3Tetra Fall Game Screenshot 4


Special Message

Welcome to Coding