Best Tutorial for AndroidDevelopers

Android App Development

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

Snake Game App in Jetpack Compose

Snake Game  App in Jetpack Compose - Responsive Blogger Template
Snake Game  App in Jetpack Compose
Snake is one of those classic games almost every developer has played at least once. Building it yourself is not just fun, it is also a great way to understand game loops, state management, canvas drawing, and gestures in modern Android development.

In this article, I’ll explain how to build a Snake game app using Jetpack Compose, without XML, using only Kotlin and Compose APIs. This tutorial is written for beginners to intermediate Android developers who want hands-on experience with real-world Compose concepts.

This code implements 

A basic Snake game using Jetpack Compose, featuring navigation between screens, real-time game updates, and a simple user interface for gameplay. It handles game logic, rendering, and user input effectively within the Composable functions provided by Jetpack Compose.

Sets up the navigation for the app using NavHost.

What we need? 

Screens:

Three screens: Start, Game, and Result is required.

StartScreenSnake: Displays the welcome screen with a logo and a button to start the game.
SnakeGameScreen: Contains the game logic, the snake, and the controls.
ResultScreen: Displays the game over message and score, with options to play again or exit.

1. Navigation Setup

This sets up a navigation graph where StartScreenSnake is the initial screen, SnakeGameScreen is where the game is played, and ResultScreen displays the score after the game ends.

2. Start Screen (StartScreenSnake)

Displays an image, a welcome message, and a button to navigate to the Snake game screen.

3. Game Screen (SnakeGameScreen)

Manages the game state, including the snake's position, food position, score, and game over status.
Uses a LaunchedEffect coroutine to update the snake's position continuously while the game is running.
Calls helper functions to move the snake and check for collisions.
The snake moves every 200 milliseconds, and it checks for collisions to determine if the game is over.

4. Input Handling (SnakeGameInput)

Provides directional buttons (Up, Down, Left, Right) to control the snake.
Contains a pause/play toggle button.

5. Drawing the Game (SnakeGameGrid)

Renders the snake and the food using a Canvas composable.
The snake is drawn with different styles for the head and body.
Each function draws its respective part using the DrawScope methods, such as drawArc and drawCircle.

6. Collision Detection and Game Logic

Functions handle the movement of the snake, growth when eating food, and checking for collisions with walls or itself.

7. Result Screen (ResultScreen)

Displays the final score and allows the player to either restart the game or exit the app.

Dependency needed for navigation

implementation("androidx.navigation:navigation-compose:2.8.6")

MainActivity

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            JetpackSkillTheme {

                SnakeGameApp()

            }
        }
    }
}

SnakeGameApp

package com.example.jetpackskill

import android.media.MediaPlayer
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Text
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.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import kotlinx.coroutines.delay
import kotlin.random.Random


@Composable
fun SnakeGameApp() {
    val navController = rememberNavController()
    NavHost(navController = navController, startDestination = "start") {
        composable("start") {
            StartScreen(navController)
        }
        composable("start_game") {
            SnakeGameScreen(navController)
        }
        composable(
            "result_screen/{score}",
            arguments = listOf(navArgument("score") { type = NavType.IntType })
        ) { backStackEntry ->
            val score = backStackEntry.arguments?.getInt("score") ?: 0
            ResultScreen(navController, score)
        }
    }
}

@Composable
fun ResultScreen(navController: NavController, score: Int) {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(color = Color.Black)
            .padding(16.dp),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(
            text = "Game Over!",
            fontSize = 32.sp,
            color = Color.Red,
            textAlign = TextAlign.Center
        )

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

        Text(
            text = "Your Score: $score",
            fontSize = 24.sp,
            color = Color.  Green,
            textAlign = TextAlign.Center
        )

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

        Button(onClick = {
            // Navigate back to the start screen to play again
            navController.navigate("start_game") {
                popUpTo("result_screen") { inclusive = true }
            }
        }) {
            Text("Play Again")
        }
    }
}

@Composable
fun StartScreen(navController: NavController) {
    Box(
        Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Button(onClick = { navController.navigate("start_game") }
        ) {
            Text("Start Game")
        }
    }
}

@Composable
fun SnakeGameScreen(navController: NavController) {
    val context = LocalContext.current
    val mediaPlayer = remember {
        // Check if the sound resource is valid
        MediaPlayer.create(context, R.raw.eat_sound)
        //?: throw IllegalArgumentException("Sound resource not found")
    }

    var screenWidthPx by remember { mutableIntStateOf(0) }
    var screenHeightPx by remember { mutableIntStateOf(0) }
    var isPlaying by remember { mutableStateOf(true) } // Start as false to avoid immediate game start

    val cellSize = with(LocalDensity.current) { (20.dp).toPx().toInt() }
    var snake by remember { mutableStateOf(listOf(Position(10 * cellSize, 10 * cellSize))) }
    var direction by remember { mutableStateOf(Direction.RIGHT) }
    var score by remember { mutableIntStateOf(0) }

    // Food with random position and color
    var food by remember {
        mutableStateOf(
            Food(
                position = Position(0, 0), // Temporary position until screen size is set
                color = Color.Green
            )
        )
    }

    LaunchedEffect(isPlaying, screenWidthPx, screenHeightPx) {
        if (screenWidthPx > 0 && screenHeightPx > 0 && isPlaying) { // Check valid dimensions and if playing
            food = Food(
                position = Position(
                    x = Random.nextInt(1, (screenWidthPx / cellSize) - 1) * cellSize,
                    y = Random.nextInt(1, ((screenHeightPx - 100) / cellSize) - 1) * cellSize
                ),
                color = Color(
                    red = Random.nextInt(256) / 255f,
                    green = Random.nextInt(256) / 255f,
                    blue = Random.nextInt(256) / 255f
                )
            )

            while (isPlaying) {
                snake = moveSnake(
                    snake, direction, screenWidthPx, screenHeightPx - 100, cellSize, mediaPlayer, navController, score
                )

                // Check if snake's head collides with the food
                if (snake.first().x == food.position.x && snake.first().y == food.position.y) {
                    mediaPlayer.start() // Play sound when food is eaten
                    score += 1

                    // Grow the snake and move food
                    snake = snake + snake.last()
                    food = Food(
                        position = Position(
                            Random.nextInt(screenWidthPx / cellSize) * cellSize,
                            Random.nextInt((screenHeightPx - 100) / cellSize) * cellSize
                        ),
                        color = Color(
                            red = Random.nextInt(256) / 255f,
                            green = Random.nextInt(256) / 255f,
                            blue = Random.nextInt(256) / 255f
                        )
                    )
                }
                delay(150L)
            }
        }
    }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .onSizeChanged { size ->
                screenWidthPx = size.width
                screenHeightPx = size.height
            },
        verticalArrangement = Arrangement.SpaceBetween
    ) {

        // Display Score at the top
        Text(
            text = "Score: $score",
            color = Color.Black,
            fontSize = 24.sp,
            modifier = Modifier
                .fillMaxWidth()
                .padding(16.dp),
            textAlign = TextAlign.End
        )
        // Game area
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .weight(1f)
                .background(Color.Black)
        ) {
            Canvas(modifier = Modifier.fillMaxSize()) {
                snake.forEach {
                    drawCircle(
                        color = Color.Green,
                        radius = cellSize / 2f,
                        center = Offset(it.x.toFloat(), it.y.toFloat())
                    )
                }

                drawCircle(
                    color = food.color,
                    radius = cellSize / 2f,
                    center = Offset(food.position.x.toFloat(), food.position.y.toFloat())
                )
            }
        }

        // Control Buttons
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(16.dp),
            horizontalArrangement = Arrangement.SpaceBetween
        ) {
            Button(onClick = { direction = Direction.LEFT }) { Text("Left") }
            Column(horizontalAlignment = Alignment.CenterHorizontally) {
                Button(onClick = { direction = Direction.UP }) { Text("Up") }
                Spacer(modifier = Modifier.height(8.dp))
                Button(onClick = { direction = Direction.DOWN }) { Text("Down") }
            }
            Button(onClick = { direction = Direction.RIGHT }) { Text("Right") }
        }

        // Play/Pause Button and Score Display
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(16.dp),
            horizontalArrangement = Arrangement.SpaceAround
        ) {
            Button(onClick = { isPlaying = !isPlaying }) {
                Text(if (isPlaying) "Pause" else "Play")
            }
            Text("Score: $score", color = Color.White)
        }
    }
}

fun moveSnake(
    snake: List, // Specify List type
    direction: Direction,
    maxWidth: Int,
    maxHeight: Int,
    cellSize: Int,
    mediaPlayer: MediaPlayer,
    navController: NavController,
    score: Int
): List { // Ensure return type matches
    val head = snake.first()
    val newHead = when (direction) {
        Direction.UP -> head.copy(y = head.y - cellSize)
        Direction.DOWN -> head.copy(y = head.y + cellSize)
        Direction.LEFT -> head.copy(x = head.x - cellSize)
        Direction.RIGHT -> head.copy(x = head.x + cellSize)
    }

    // Play sound if snake touches the edges and navigate to result screen
    if (newHead.x < 0 || newHead.x >= maxWidth || newHead.y < 0 || newHead.y >= maxHeight) {
        if (!mediaPlayer.isPlaying) {
            mediaPlayer.start()
            navController.navigate("result_screen/${score}") {
                popUpTo("start_game") { inclusive = true }
            }
        }
    }

    // Ensure the snake stays within the screen bounds
    val boundedHead = Position(
        x = newHead.x.coerceIn(0, maxWidth - cellSize),
        y = newHead.y.coerceIn(0, maxHeight - cellSize)
    )

    return listOf(boundedHead) + snake.dropLast(1)
}

GameState

package com.example.jetpackskill

import androidx.compose.ui.graphics.Color

data class Position(val x: Int, val y: Int)
data class Food(val position: Position, val color: Color)
    // Data Classes
enum class Direction { UP, DOWN, LEFT, RIGHT }

OUTPUT:

Snake Game  App in Jetpack Compose Output 1Snake Game  App in Jetpack Compose Output 2
Snake Game  App in Jetpack Compose Output 3

Snake Game App
01

Snake Game App

Full source code with app icon, atrractive UI. It handles game logic, rendering, and user input effectively within the Composable functions provided by Jetpack Compose.

₹199.00 Source Code
Sandeep Kumar - Android Developer

About the Author

Sandeep Kumar is an Android developer and educator who writes beginner-friendly Jetpack Compose tutorials on CodingBihar.com. His focus is on clean UI, Material Design 3, and real-world Android apps.

SkillDedication

— Python High Level Programming Language- Expert-Written Tutorials, Projects, and Tools—

Coding Bihar

Welcome To Coding Bihar👨‍🏫