In this step-by-step tutorial, you will build a functional Android calculator using Kotlin and Jetpack Compose—Google’s modern, recommended framework for building native UI.
How to create a simple calculator Android app?
What You Will Build
You will create a responsive, clean calculator capable of performing core arithmetic operations:
- Numbers: 0 through 9 and a decimal point.
- Operators: Addition (
+), Subtraction (-), Multiplication (×), and Division (÷). - Actions: Clear (
C), Delete (DEL), and Calculate (=).
Step 1: Create a New Project in Android Studio
1.Initialize the Android Project:Set up your IDE and project configuration.
- Open Android Studio.
- Click New Project and select Empty Activity (Jetpack Compose).
- Name your project SimpleCalculator.
- Set the Language to Kotlin and select Minimum SDK as API 24 (Android 7.0) or higher.
- Click Finish and wait for Gradle to sync.
Step 2: Define Calculator Actions and State
To clean up business logic and UI states, define an enum for the mathematical operators and an Action sealed interface to handle user button taps cleanly.
Create a new file named CalculatorAction.kt:
Kotlin
sealed interface CalculatorAction {
data class Number(val number: Int) : CalculatorAction
object Clear : CalculatorAction
object Delete : CalculatorAction
object Decimal : CalculatorAction
object Calculate : CalculatorAction
data class Operation(val operation: CalculatorOperation) : CalculatorAction
}
enum class CalculatorOperation(val symbol: String) {
ADD("+"),
SUBTRACT("-"),
MULTIPLY("×"),
DIVIDE("÷")
}
Next, define the CalculatorState.kt file to keep track of user input and calculation state:
Kotlin
data class CalculatorState(
val number1: String = "",
val number2: String = "",
val operation: CalculatorOperation? = null
)
Step 3: Implement ViewModel Business Logic
The ViewModel handles the arithmetic logic and updates the UI state when a user clicks any button.
Create CalculatorViewModel.kt:
Kotlin
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
class CalculatorViewModel : ViewModel() {
var state by mutableStateOf(CalculatorState())
private set
fun onAction(action: CalculatorAction) {
when (action) {
is CalculatorAction.Number -> enterNumber(action.number)
is CalculatorAction.Decimal -> enterDecimal()
is CalculatorAction.Clear -> state = CalculatorState()
is CalculatorAction.Operation -> enterOperation(action.operation)
is CalculatorAction.Delete -> performDeletion()
is CalculatorAction.Calculate -> performCalculation()
}
}
private fun enterNumber(number: Int) {
if (state.operation == null) {
if (state.number1.length >= 8) return
state = state.copy(number1 = state.number1 + number)
return
}
if (state.number2.length >= 8) return
state = state.copy(number2 = state.number2 + number)
}
private fun enterDecimal() {
if (state.operation == null && !state.number1.contains(".") && state.number1.isNotBlank()) {
state = state.copy(number1 = state.number1 + ".")
return
}
if (!state.number2.contains(".") && state.number2.isNotBlank()) {
state = state.copy(number2 = state.number2 + ".")
}
}
private fun enterOperation(operation: CalculatorOperation) {
if (state.number1.isNotBlank()) {
state = state.copy(operation = operation)
}
}
private fun performDeletion() {
when {
state.number2.isNotBlank() -> state = state.copy(number2 = state.number2.dropLast(1))
state.operation != null -> state = state.copy(operation = null)
state.number1.isNotBlank() -> state = state.copy(number1 = state.number1.dropLast(1))
}
}
private fun performCalculation() {
val num1 = state.number1.toDoubleOrNull()
val num2 = state.number2.toDoubleOrNull()
if (num1 != null && num2 != null) {
val result = when (state.operation) {
CalculatorOperation.ADD -> num1 + num2
CalculatorOperation.SUBTRACT -> num1 - num2
CalculatorOperation.MULTIPLY -> num1 * num2
CalculatorOperation.DIVIDE -> if (num2 != 0.0) num1 / num2 else Double.NaN
null -> return
}
state = state.copy(
number1 = result.toString().take(12).removeSuffix(".0"),
number2 = "",
operation = null
)
}
}
}
Step 4: Build the UI Screen with Jetpack Compose
Now, create the visual components and button grid layout.
Custom Button Component
Create CalculatorButton.kt:
Kotlin
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.sp
@Composable
fun CalculatorButton(
symbol: String,
modifier: Modifier = Modifier,
color: Color = Color.DarkGray,
textColor: Color = Color.White,
onClick: () -> Unit
) {
Box(
contentAlignment = Alignment.Center,
modifier = modifier
.clip(CircleShape)
.clickable { onClick() }
.then(modifier)
) {
Text(
text = symbol,
fontSize = 32.sp,
color = textColor
)
}
}
Main Screen Layout Grid
Create CalculatorScreen.kt to assemble the readout display and a 5-row button keypad:
Kotlin
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
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.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun CalculatorScreen(
state: CalculatorState,
onAction: (CalculatorAction) -> Unit,
modifier: Modifier = Modifier
) {
Box(
modifier = modifier
.fillMaxSize()
.background(Color.Black)
.padding(16.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
// Display Area
Text(
text = state.number1 + (state.operation?.symbol ?: "") + state.number2,
textAlign = TextAlign.End,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 32.dp),
fontWeight = FontWeight.Light,
fontSize = 64.sp,
color = Color.White,
maxLines = 2
)
// Button Keypad Rows
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
CalculatorButton(
symbol = "AC",
color = Color.LightGray,
textColor = Color.Black,
modifier = Modifier.weight(2f),
onClick = { onAction(CalculatorAction.Clear) }
)
CalculatorButton(
symbol = "Del",
color = Color.LightGray,
textColor = Color.Black,
modifier = Modifier.weight(1f),
onClick = { onAction(CalculatorAction.Delete) }
)
CalculatorButton(
symbol = "÷",
color = Color(0xFFFF9800),
modifier = Modifier.weight(1f),
onClick = { onAction(CalculatorAction.Operation(CalculatorOperation.DIVIDE)) }
)
}
// Keypad Numbers 7-9 & Multiply
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
CalculatorButton(symbol = "7", modifier = Modifier.weight(1f)) { onAction(CalculatorAction.Number(7)) }
CalculatorButton(symbol = "8", modifier = Modifier.weight(1f)) { onAction(CalculatorAction.Number(8)) }
CalculatorButton(symbol = "9", modifier = Modifier.weight(1f)) { onAction(CalculatorAction.Number(9)) }
CalculatorButton(symbol = "×", color = Color(0xFFFF9800), modifier = Modifier.weight(1f)) {
onAction(CalculatorAction.Operation(CalculatorOperation.MULTIPLY))
}
}
// Keypad Numbers 4-6 & Subtract
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
CalculatorButton(symbol = "4", modifier = Modifier.weight(1f)) { onAction(CalculatorAction.Number(4)) }
CalculatorButton(symbol = "5", modifier = Modifier.weight(1f)) { onAction(CalculatorAction.Number(5)) }
CalculatorButton(symbol = "6", modifier = Modifier.weight(1f)) { onAction(CalculatorAction.Number(6)) }
CalculatorButton(symbol = "-", color = Color(0xFFFF9800), modifier = Modifier.weight(1f)) {
onAction(CalculatorAction.Operation(CalculatorOperation.SUBTRACT))
}
}
// Keypad Numbers 1-3 & Add
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
CalculatorButton(symbol = "1", modifier = Modifier.weight(1f)) { onAction(CalculatorAction.Number(1)) }
CalculatorButton(symbol = "2", modifier = Modifier.weight(1f)) { onAction(CalculatorAction.Number(2)) }
CalculatorButton(symbol = "3", modifier = Modifier.weight(1f)) { onAction(CalculatorAction.Number(3)) }
CalculatorButton(symbol = "+", color = Color(0xFFFF9800), modifier = Modifier.weight(1f)) {
onAction(CalculatorAction.Operation(CalculatorOperation.ADD))
}
}
// Bottom Row: 0, Decimal, Equals
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
CalculatorButton(symbol = "0", modifier = Modifier.weight(2f)) { onAction(CalculatorAction.Number(0)) }
CalculatorButton(symbol = ".", modifier = Modifier.weight(1f)) { onAction(CalculatorAction.Decimal) }
CalculatorButton(symbol = "=", color = Color(0xFFFF9800), modifier = Modifier.weight(1f)) {
onAction(CalculatorAction.Calculate)
}
}
}
}
}
Step 5: Connect Everything in MainActivity
Connect the ViewModel with CalculatorScreen inside your app’s MainActivity.kt:
Kotlin
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.lifecycle.viewmodel.compose.viewModel
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val viewModel = viewModel<CalculatorViewModel>()
val state = viewModel.state
CalculatorScreen(
state = state,
onAction = viewModel::onAction
)
}
}
}
Step 6: Run Your Application
- Connect your Android device via USB (with Developer Options & USB Debugging enabled) or launch an Android Virtual Device (AVD) emulator.
- Click the green Run button ($\triangleright$) in Android Studio.
- Test calculations like
12 + 8 = 20or5 × 9 = 45.
Key Architecture Concepts Learned
| Concept | Implementation in this App |
| Declarative UI | Built responsive button layouts with Column, Row, and Modifier.weight() in Compose. |
| Unidirectional Data Flow | UI emits user actions to ViewModel, and ViewModel updates the immutable CalculatorState. |
| Configuration Safety | Using Jetpack Compose state keeps UI states intact across screen transitions. |