Creating a Simple Counter App with Increment/Decrement Functionality

Jyoti MauryaJyoti Maurya
8 min read

In this tutorial, we’ll build a simple yet functional counter app using Kotlin and Jetpack Compose. This counter app will allow users to increment and decrement a numeric value by pressing buttons. While it’s a beginner-friendly project, it also introduces essential Compose concepts like @Composable, State, and UI recomposition.

Prerequisites

Before you begin, ensure you have:

  • Android Studio installed (preferably Electric Eel or later)

  • Basic knowledge of Kotlin

  • Familiarity with Jetpack Compose fundamentals

Project Setup

Create a new Android project with the following options:

  • Language: Kotlin

  • Minimum SDK: API 21+

  • Template: Empty Activity

Ensure that Jetpack Compose is enabled in your build.gradle files. Your build.gradle(:app) should have:

buildFeatures {
    compose true
}

composeOptions {
    kotlinCompilerExtensionVersion = "1.4.3" // Update to latest
}

Designing the Counter UI

Here’s a breakdown of what we will do:

  • A number displayed in the center of the screen

  • "Increment" button to increase the value

  • "Decrement" button to decrease the value

Code Implementation

Let’s jump into the core Composable function.

Let’s create mainActivity.kt

package com.igdtuw.basicsjc

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.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.ui.Modifier
import com.igdtuw.basicsjc.ui.theme.BasicsJCTheme
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.Button
import androidx.compose.ui.Alignment
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            BasicsJCTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    Column (modifier = Modifier.fillMaxSize().padding(innerPadding),
                        verticalArrangement = Arrangement.Center,
                        horizontalAlignment = Alignment.CenterHorizontally){
                        var count by remember {mutableStateOf(0)}
                        AnimatedCounter(count = count)
                        Row(modifier = Modifier.fillMaxWidth(),
                            horizontalArrangement = Arrangement.SpaceAround) {
                            Button(onClick = { count = count - 1 }) {
                                Text(text = "Decrement ")
                            }
                            Button(onClick = { count = count + 1 }) {
                                Text(text = "Increment ")
                            }
                        }
                    }

                }
            }
        }
    }
}

Create another file animatedCounter.kt

package com.igdtuw.basicsjc

import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextStyle
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.animation.*
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.unit.sp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight

@Composable
fun AnimatedCounter(
    count: Int,
    modifier: Modifier = Modifier,
    style: TextStyle = MaterialTheme.typography.bodyLarge
){
    var oldCount by remember { mutableStateOf(count) }
    SideEffect {
        oldCount = count
    }

    Row(modifier = modifier) {
        val countString = count.toString()
        val oldCountString = oldCount.toString().padStart(countString.length, ' ')

        for (i in countString.indices){
            val oldChar = oldCountString.getOrNull(i)
            val newChar = countString[i]

            var char = if(oldChar == newChar){
                oldCountString[i]
            } else {
                countString[i]
            }

            AnimatedContent(targetState = char,
                transitionSpec = {
                    slideInVertically {height -> height } togetherWith slideOutVertically{ height -> -height}
                },
                label = "digit"
            ) { char ->
                Text(
                    text = char.toString(),
                    style = style,
                    softWrap = false,
                    color = Color.Cyan,
                    fontSize = 46.sp,
                    fontWeight = FontWeight.Bold,
                    fontFamily = FontFamily.SansSerif,
                    letterSpacing = 5.sp,
                    lineHeight = 50.sp
                )
            }
        }
    }

}

Output

Overall Explanation

This simple Jetpack Compose application demonstrates how to create a counter interface with two buttons—Increment and Decrement—and is structured for clarity, reusability, and best practices. The core UI logic resides in a composable function named CounterScreen, which displays the current counter value in a vertically centered layout. It uses a Column to stack the counter display and a Row to position the two buttons side by side.

State management is handled using Jetpack Compose’s remember and mutableIntStateOf, which store the counter value and allow it to update reactively on button clicks. The Increment button simply increases the counter, while the Decrement button is conditionally disabled when the count is zero, preventing negative values—an example of good UX design.

The entire interface is styled using MaterialTheme for consistency and wrapped inside a Scaffold layout within MainActivity. This structure not only makes the UI responsive and neatly padded but also prepares the app to scale with additional content in the future. Comments and KDoc-style documentation throughout the code make it beginner-friendly and suitable for learning, tutorials, or educational blog posts.

Line-by-Line Explanation

MainActivity.kt

package com.igdtuw.basicsjc

Defines the package name of the app. Helps organize the code and prevent class name conflicts.

import android.os.Bundle

Imports the Bundle class used to pass data between Android components, often used in onCreate.

import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge

ComponentActivity: A base class for activities that want to use Jetpack Compose.

setContent: Sets the content view using Compose instead of XML.

enableEdgeToEdge: Makes the app use the full screen (draw behind the system bars).

import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.ui.Modifier

Imports basic UI elements and modifiers for layout behavior like filling available space.

import com.igdtuw.basicsjc.ui.theme.BasicsJCTheme

Imports your custom Compose theme (likely auto-generated by the template).

import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth

Layout components and modifiers for vertical (Column), horizontal (Row) alignment, padding, etc.

import androidx.compose.material3.Button
import androidx.compose.ui.Alignment

Button: Material Design button component.

Alignment: Used for aligning elements in layout (e.g., center alignment).

import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue

State management imports to create and observe changes in state.

class MainActivity : ComponentActivity() {

Declares MainActivity inheriting from ComponentActivity to use Jetpack Compose UI.

    override fun onCreate(savedInstanceState: Bundle?) {

onCreate: Lifecycle method called when the activity starts.

        super.onCreate(savedInstanceState)

Calls the parent class's onCreate to ensure proper activity setup.

        enableEdgeToEdge()

Makes the UI draw behind system bars (status bar, navigation bar) for immersive UI

        setContent {

Sets up the content of the activity using Compose.

            BasicsJCTheme {

Applies your app's theme to the UI.

                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->

Scaffold: A layout structure that provides slots for Material Design components like top bars, floating action buttons, etc.

innerPadding: Padding needed to avoid overlapping system UI.

                    Column (
                        modifier = Modifier
                            .fillMaxSize()
                            .padding(innerPadding),
                        verticalArrangement = Arrangement.Center,
                        horizontalAlignment = Alignment.CenterHorizontally
                    ) {

Column: A vertical layout container.

fillMaxSize(): Fills available space.

padding(innerPadding): Adds system UI safe padding.

Vertically centers children and aligns them horizontally to the center.

                        var count by remember { mutableStateOf(0) }

count: A mutable state variable to hold the counter value.

remember: Keeps state across recompositions.

by: Kotlin delegate syntax for getValue / setValue.

                        AnimatedCounter(count = count)

A custom composable (likely defined elsewhere) that shows the animated counter UI.

                        Row(
                            modifier = Modifier.fillMaxWidth(),
                            horizontalArrangement = Arrangement.SpaceAround
                        ) {

Row: A horizontal layout container.

Spreads its children evenly across the width of the screen.

                            Button(onClick = { count = count - 1 }) {
                                Text(text = "Decrement ")
                            }

Button that decreases the counter value by 1.

                            Button(onClick = { count = count + 1 }) {
                                Text(text = "Increment ")
                            }

Button that increases the counter value by 1.

                        }

Ends the Row.

                    }

Ends the Column.

                }

Ends the Scaffold.

            }

Ends the Theme.

        }

Ends setContent.

    }
}

Ends onCreate and MainActivity.

AnimatedCounter.kt

package com.igdtuw.basicsjc

Defines the package for the file.

// Importing necessary Compose and animation utilities
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.*
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.sp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight

These are the necessary imports for animations, layout, styling, text, and state management in Jetpack Compose.

@Composable
fun AnimatedCounter(
    count: Int,
    modifier: Modifier = Modifier,
    style: TextStyle = MaterialTheme.typography.bodyLarge
)

Defines a reusable composable function AnimatedCounter.

Takes:

  • count: the current number to display.

  • modifier: for customizing layout/styling.

  • style: text style with a default taken from the Material theme.

    var oldCount by remember { mutableStateOf(count) }

Stores the previous value of count using Compose's state system.

    SideEffect {
        oldCount = count
    }

Updates oldCount after the recomposition has been committed, so you can compare the previous and new values during transition.

    Row(modifier = modifier) {

Lays out each digit horizontally.

        val countString = count.toString()

Converts current count to a string (e.g., 15 → "15")

        val oldCountString = oldCount.toString().padStart(countString.length, ' ')

Converts old count to a string and pads it to match current length with spaces (to prevent index issues).

        for (i in countString.indices) {

Loops through each character (digit) index.

            val oldChar = oldCountString.getOrNull(i)
            val newChar = countString[i]

Gets old and new characters at that position, if available.

            var char = if (oldChar == newChar) {
                oldCountString[i]
            } else {
                countString[i]
            }

If the character hasn't changed, keep it as-is. If changed, use the new one.

            AnimatedContent(
                targetState = char,
                transitionSpec = {
                    slideInVertically { height -> height } togetherWith
                    slideOutVertically { height -> -height }
                },
                label = "digit"
            ) { char ->

AnimatedContent: Animates the transition between characters.

targetState = char: Triggers animation when char changes.

slideInVertically and slideOutVertically: Slides new digit in from bottom, old digit out from top.

togetherWith: Combines the in and out animations.

                Text(
                    text = char.toString(),
                    style = style,
                    softWrap = false,
                    color = Color.Cyan,
                    fontSize = 46.sp,
                    fontWeight = FontWeight.Bold,
                    fontFamily = FontFamily.SansSerif,
                    letterSpacing = 5.sp,
                    lineHeight = 50.sp
                )

Displays each digit with styling:

  • Big bold text

  • Cyan color

  • Extra spacing for emphasis

  • Prevents wrapping of text

            } // end of AnimatedContent
        } // end of for-loop
    } // end of Row
}

End of AnimatedContent, for-loop, Row and AnimatedCounter function.

After Words

I hope this blog was helpful in understanding basic application using jetpack compose that used onClick listeners, mutableState, and smooth transitions with AnimatedContent for a more dynamic UI experience

0
Subscribe to my newsletter

Read articles from Jyoti Maurya directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Jyoti Maurya
Jyoti Maurya

I create cross platform mobile apps with AI functionalities. Currently a PhD Scholar at Indira Gandhi Delhi Technical University for Women, Delhi. M.Tech in Artificial Intelligence (AI).