Guide to Building Your First Android Calculator App with Jetpack Compose and Kotlin: A Beginner's Android Development Tutorial
This blog is tailored for beginners, detailing my personal coding journey. It covers the fundamental prerequisites for embarking on your app development adventure using Jetpack Compose and Kotlin.
Prerequisite
Android State, Viewmodel, Layouts, Box, Modifiers, and other Android-related terms will come as you read the blog.
Endeavour to encompass all of the topics listed above, as delving into them comprehensively and also following the Code (Github link attached) will greatly enhance your understanding as you progress through this blog.
Creating the layout for the Calculator
The development of the calculator app unfolds through the following steps:
Layout Crafting and Button Creation: The initial phase revolves around shaping the app's layout, which commences with the design and creation of buttons.
Kotlin File Generation: Our approach begins with the generation of a dedicated Kotlin file solely focused on defining these buttons.
Composable Function Creation: Construct a composable function capable of receiving parameters from external sources.
Parameterized Differentiation: These external parameters should encompass distinctive attributes of the buttons: symbols, sizes, colors, and importantly, an onClick lambda function.
Lambda Function Impact: The onClick lambda function plays a pivotal role in determining the button's behaviour upon user interaction.
Box Function Incorporation: Within the composable function, establish box functions enriched with modifiers.
Modifier Customization: Apply modifiers to shape the box, set its background, and facilitate clipping, all of which can be adjusted externally.
Text Inbuilt Function: Within the box, employ the inbuilt text function to display the desired content on the button.
Below is the Sample Code:-
@Composable fun CalculatorButton( symbol: String, modifier: Modifier = Modifier, color: Color = Color.White, textStyle: TextStyle = TextStyle(), onClick: () -> Unit ) { Box( contentAlignment = Alignment.Center, modifier = Modifier .clip(RoundedCornerShape(100.dp)) .background(color) .clickable { onClick() } .then(modifier) ) { Text( text = symbol, style = textStyle, fontSize = 36.sp, color = Color.White ) } }
Developing Key Components for Ensuring the Functionality of the Calculator App
Following the creation of the Button function, which we will later implement in MainActivity, let's first generate three additional files. These encompass two sealed classes and one data class designed for managing actions, operations, and State. To know the difference between sealed class and data class refer to.
We'll begin by creating the Action class, designed to encapsulate the diverse operations our calculator will carry out. This involves crafting data classes for numbers and operations, as well as implementing object functions for Delete, Clear, Decimal, and Calculate.
Next, we'll craft an Operation class that builds upon the foundation laid by the Action class. This class will encompass fundamental operations such as addition, subtraction, multiplication, and division. Additionally, we'll introduce a State class which consists of attributes like number1, operation, and number2. In programming, a state represents a value crucial for updating your user interface.
Below is the sample code for Action Class:-
sealed class CalculatorAction { data class Number(val number: Int): CalculatorAction() object Clear: CalculatorAction() object Delete: CalculatorAction() data class Operation(val operation: CalculatorOperation): CalculatorAction() object Calculate: CalculatorAction() object Decimal: CalculatorAction() }
Below is the Sample code for Operation Class:-
sealed class CalculatorOperation(val symbol: String) {
object Add: CalculatorOperation("+")
object Subtract: CalculatorOperation("-")
object Multiply: CalculatorOperation("x")
object Divide: CalculatorOperation("/")
}
Below is the Sample code for State data Class:-
data class CalculatorState(
val number1: String = "",
val number2: String = "",
val operation: CalculatorOperation? = null
)
Designing Calculator UI Within MainActivity
Beyond crafting the UI for the calculator app, an equally vital MainActivity task is ViewModel declaration, code residing in a separate file. The ViewModel embeds the calculator's logic.
Shifting to UI design, our meticulously designed Button function takes centre stage, ready for deployment. Our visual strategy involves a pivotal "Box" element, housing two segments: a numeric display and an input zone.
For the numeric display, we employ a Column and Text combo, ensuring clarity. Below is the sample code for Declaring the ViewModel and the State and creating the display of our Calculator
val viewModel = viewModel<CalculatorViewModel>()
val state = viewModel.state
val buttonSpacing = 8.dp
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.DarkGray)
.padding(16.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter),
verticalArrangement = Arrangement.spacedBy(buttonSpacing),
) {
Text(
text = state.number1 + (state.operation?.symbol ?: "") + state.number2,
textAlign = TextAlign.End,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 32.dp),
fontWeight = FontWeight.Light,
fontSize = 80.sp,
color = Color.White,
maxLines = 2
)
In the numbers and operations area, Rows come into play, offering an organized setup for a user-friendly layout of number input and operations. Inside rows, you just need to call the button function and apply appropriate modifications according to the UI. For example, this is a sample code for Clear Button inside Rows and for full code visit the GitHub link attached above.
CalculatorButton(
symbol = "AC",
color = LightGray,
modifier = Modifier
.aspectRatio(2f)
.weight(2f)
) {
viewModel.onAction(CalculatorAction.Clear)
}
Building the ViewModel: Cracking Open the Calculator App's Logic
The significance of the code line provided below is as follows:
var state: This declares a mutable variable named state, which will be used to store and manage the current state of the calculator.
mutableStateOf helps manage the state by automatically updating any components that rely on this state when it changes.
CalculatorState(): This code initializes the initial value of the state variable using the CalculatorState() function that we created earlier.
var state by mutableStateOf(CalculatorState())
Subsequently, we establish an onAction function that outlines the specific operations the calculator will execute in response to user actions. This encompasses the actions previously defined within the CalculatorAction class. After that, we define each function specified in the onAction function.
fun onAction(action: CalculatorAction) {
when(action) {
is CalculatorAction.Number -> enterNumber(action.number)
is CalculatorAction.Delete -> delete()
is CalculatorAction.Clear -> state = CalculatorState()
is CalculatorAction.Operation -> enterOperation(action.operation)
is CalculatorAction.Decimal -> enterDecimal()
is CalculatorAction.Calculate -> calculate()
}
}
Consult the code to observe the operations performed by each function. Within this context, I will explain important elements within the code. Additionally, for aspects that might be less clear, I encourage you to conduct further research independently.
state.copy:- is a common occurrence in the code, involving the creation of a shallow object copy with potential property modifications. Its prime advantage lies in preserving immutability while effectively generating modified object copies. This is especially valuable for handling state changes in applications where retaining the previous state during specific updates is essential.toDoubleOrNull()function is used to attempt to convert a String to a Double (floating-point) value. As we have earlier declared the number1, operation and number2 variables as a string.
private fun enterOperation(operation: CalculatorOperation) {
if(state.number1.isNotBlank()) {
state = state.copy(operation = operation)
}
}
private fun calculate() {
val number1 = state.number1.toDoubleOrNull()
val number2 = state.number2.toDoubleOrNull()
if(number1 != null && number2 != null) {
val result = when(state.operation) {
is CalculatorOperation.Add -> number1 + number2
is CalculatorOperation.Subtract -> number1 - number2
is CalculatorOperation.Multiply -> number1 * number2
is CalculatorOperation.Divide -> number1 / number2
null -> return
}
state = state.copy(
number1 = result.toString().take(15),
number2 = "",
operation = null
)
}
}
private fun delete() {
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 enterDecimal() {
if(state.operation == null && !state.number1.contains(".") && state.number1.isNotBlank()) {
state = state.copy(
number1 = state.number1 + "."
)
return
} else if(!state.number2.contains(".") && state.number2.isNotBlank()) {
state = state.copy(
number2 = state.number2 + "."
)
}
}
private fun enterNumber(number: Int) {
if(state.operation == null) {
if(state.number1.length >= MAX_NUM_LENGTH) {
return
}
state = state.copy(
number1 = state.number1 + number
)
return
}
if(state.number2.length >= MAX_NUM_LENGTH) {
return
}
state = state.copy(
number2 = state.number2 + number
)
}
companion object {
private const val MAX_NUM_LENGTH = 8
Congratulations! You've successfully crafted your inaugural Android app using Kotlin and Jetpack Compose. This marks the very first stride you've taken into the realm of Android development, and there's an extensive journey awaiting you. Stay tuned for Part 2 of this calculator app, where we'll introduce exciting new features to enhance its utility and user experience.