Best Website for Jetpack Compose App Development

Android Jetpack Compose

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

Scientific Calculator in Jetpack Compose

Scientific Calculator in Jetpack Compose  - Responsive Blogger Template
Scientific Calculator in Jetpack Compose

Scientific Calculator in Jetpack Compose

1. Project overview (what & why)

I built a fully-featured scientific calculator using Jetpack Compose with zero third-party libraries. The goal was not just to reproduce a handheld calculator, but to explore modern UI paradigms, a small expression-evaluation engine, and practical UX choices (DEG/RAD modes, live preview, typed vs. committed results). This project is ideal for Android developers learning Compose, parsing/evaluation basics, and edge-case handling in numeric UIs.

Core features

  • Arithmetic (+ - * / ^ %) and parentheses
  • Functions: sin, cos, tan, asin, acos, atan, ln, log, sqrt
  • Constants: π
  • Factorial, percent, square (x²), cube (x³) and general power (^)

DEG/RAD toggle for trig functions

  • Live preview while typing + final result only when = pressed
  • Nicely formatted results (no trailing .0, 6-decimal rounding)

2. Requirements & constraints

Functional

  • Accurate math for common scientific operations.
  • Immediate visual feedback but no confusing automatic commitment of results.
  • Clear DEG/RAD handling so users don’t get unexpected trig answers.

Non-functional

  • No third-party libraries — keep dependencies minimal.
  • Compose-first UI; single-file reference implementation for learning and reuse.
  • Reasonable performance on low-end devices.

Constraints

  • Numeric precision limited to Double (Kotlin Double) and BigDecimal used only for presentation rounding.
  • Factorial implemented only for non-negative integers.
  • No symbolic algebra — evaluator computes numeric results only.

3. Design decisions & rationale

Jetpack Compose

Compose simplifies UI state management: expression, preview, final result, degree mode are all MutableState. LaunchedEffect drives live-preview evaluation which keeps UI responsive and declarative.

A 3-column layout felt balanced for phone screens and easier to press.

Expression engine

  • Implemented tokenizer → shunting-yard (infix → RPN) → RPN evaluator. Rationale:
  • Shunting-yard is compact, deterministic, and handles operator precedence & functions well.
  • RPN evaluation is straightforward and easy to extend (add functions/operators).
  • Chose to keep everything numeric (no AST transformations), avoiding the complexity of a full parser generator.

DEG/RAD handling

  • Trig functions (sin, cos, tan) convert arguments when DEG is enabled (Math.toRadians), inverse trig converts results back to degrees.
  • This prevents user confusion (e.g., sin(30) → 0.5 in DEG mode).

UX: live preview + commit

  • Live preview provides immediate feedback while typing (helpful to catch obvious mistakes).
  • Committing only on = avoids accidental overwrites and mirrors real calculators’ behavior.
  • Preview displayed in lighter color and final result shown prominently when committed.

Button labeling vs expression

Buttons display human-friendly symbols (×, ÷, x², √, π) but inject machine-friendly tokens into the expression (*, /, ^2, sqrt(, pi). This keeps the evaluator simple while the UI remains readable.

4. Implementation highlights (code patterns)

  • State: expr, preview, result, error, degreeMode — all remembered.
  • Live preview: LaunchedEffect(expr, degreeMode) { ... } attempts to evaluate and sets preview.
  • Commit: onButton("=") { result = preview }
  • Formatting: If r % 1.0 == 0.0 show integer; else use BigDecimal(r).setScale(6, HALF_UP).stripTrailingZeros().toPlainString().
  • Safety: Parser throws descriptive exceptions for mismatched parentheses, unknown tokens, and invalid factorial inputs.

Final Code:


data class CalcButton(val display: String, val value: String)

@Composable
fun ScientificCalculator() {
    var expr by remember { mutableStateOf("") }
    var preview by remember { mutableStateOf("") } // live preview
    var result by remember { mutableStateOf("") }  // final result after '='
    var error by remember { mutableStateOf(null) }
    var degreeMode by remember { mutableStateOf(true) }

    // Live preview
    LaunchedEffect(expr, degreeMode) {
        if (expr.isNotEmpty()) {
            try {
                val r = evaluateExpression(expr, degreeMode)
                preview = if (r % 1.0 == 0.0) {
                    r.toLong().toString()
                } else {
                    BigDecimal(r).setScale(6, RoundingMode.HALF_UP)
                        .stripTrailingZeros().toPlainString()
                }
                error = null
            } catch (_: Exception) {
                preview = ""
            }
        } else {
            preview = ""
        }
    }

    val buttons = listOf(
        listOf(CalcButton("sin", "sin("), CalcButton("cos", "cos("), CalcButton("tan", "tan(")),
        listOf( CalcButton("sin⁻¹", "asin("), CalcButton("cos⁻¹", "acos("), CalcButton("tan⁻¹", "atan(")),
        listOf( CalcButton("7", "7"),CalcButton("8", "8"), CalcButton("9", "9"), CalcButton("÷", "/") ),
        listOf( CalcButton("4", "4"),  CalcButton("5", "5"), CalcButton("6", "6"), CalcButton("×", "*") ),
        listOf(CalcButton("0", "0"), CalcButton("1", "1"), CalcButton("2", "2"), CalcButton("3", "3")),
        listOf(CalcButton("x²", "^"), CalcButton("√", "sqrt("), CalcButton(".", "."),  CalcButton("−", "-")),
        listOf(CalcButton("(", "("), CalcButton(")", ")"), CalcButton("π", "pi"), CalcButton("+", "+")),
        listOf(CalcButton("C", "C"), CalcButton("⌫", "⌫"), CalcButton("%", "%"), CalcButton("=", "="))
    )

    Column(modifier = Modifier.systemBarsPadding().fillMaxSize()) {

        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.SpaceBetween,
            verticalAlignment = Alignment.CenterVertically
        ) {
            Text("CB Scientific Calculator", style = MaterialTheme.typography.titleLarge, color = MaterialTheme.colorScheme.primary)
            Text(if (degreeMode) "DEG" else "RAD", fontWeight = FontWeight.Bold)
            Switch(checked = degreeMode, onCheckedChange = { degreeMode = it })
        }

        // Display
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .weight(0.15f)
                .padding(8.dp)
        ) {

            Text(text = expr.ifEmpty { "0" }, fontSize = 28.sp, modifier = Modifier.align(Alignment.End))
            if (preview.isNotEmpty()) {
                Text(text = preview, fontSize = 22.sp, color = MaterialTheme.colorScheme.primary, modifier = Modifier.align(Alignment.End))
            }
            if (result.isNotEmpty()) {
                Text(text = "= $result", fontSize = 22.sp, fontWeight = FontWeight.Bold, modifier = Modifier.align(Alignment.End))
            }

        }

        // Buttons Grid
        Column(modifier = Modifier.fillMaxWidth().weight(0.85f).padding(6.dp),

            verticalArrangement = Arrangement.Bottom) {
            for (row in buttons) {
                Row(modifier = Modifier.fillMaxWidth().padding(8.dp), horizontalArrangement = Arrangement.spacedBy(4.dp)) {
                    for (btn in row) {
                        CalculatorButton(text = btn.display, modifier = Modifier.weight(1f)) {
                            when (btn.value) {
                                "C" -> { expr = ""; result = ""; preview = ""; error = null }
                                "⌫" -> if (expr.isNotEmpty()) expr = expr.dropLast(1)
                                "=" -> result = preview
                                else -> expr += btn.value
                            }
                        }
                    }
                }
            }
        }
    }
}

@Composable
fun CalculatorButton(text: String, modifier: Modifier = Modifier, onClick: () -> Unit) {
    Button(onClick = onClick, modifier = modifier.height(56.dp)) {
        Text(text = text, fontSize = 18.sp)
    }
}

// ---------- Expression evaluation ----------

private val operators = mapOf(
    "+" to Operator(1, Associativity.LEFT),
    "-" to Operator(1, Associativity.LEFT),
    "*" to Operator(2, Associativity.LEFT),
    "/" to Operator(2, Associativity.LEFT),
    "^" to Operator(3, Associativity.RIGHT),
    "%" to Operator(2, Associativity.LEFT),
    "!" to Operator(4, Associativity.LEFT, unary = true)
)

private data class Operator(val prec: Int, val assoc: Associativity, val unary: Boolean = false)
private enum class Associativity { LEFT, RIGHT }

private fun tokenize(s: String): List {
    val tokens = mutableListOf()
    var i = 0
    while (i < s.length) {
        val c = s[i]
        when {
            c.isWhitespace() -> i++
            c.isDigit() || c == '.' -> {
                val start = i
                while (i < s.length && (s[i].isDigit() || s[i] == '.')) i++
                tokens += s.substring(start, i)
            }
            c.isLetter() -> {
                val start = i
                while (i < s.length && s[i].isLetter()) i++
                tokens += s.substring(start, i)
            }
            else -> { tokens += c.toString(); i++ }
        }
    }
    return tokens
}

private fun isFunction(token: String) = token in listOf("sin","cos","tan","asin","acos","atan","log","ln","sqrt")

@Throws(Exception::class)
private fun infixToRPN(tokens: List): List {
    val out = mutableListOf()
    val stack = ArrayDeque()
    for (token in tokens) {
        when {
            token.toDoubleOrNull() != null -> out += token
            token == "pi" -> out += Math.PI.toString()
            isFunction(token) -> stack.addFirst(token)
            operators.containsKey(token) -> {
                val o1 = token
                while (stack.isNotEmpty() && (operators.containsKey(stack.first()) || isFunction(stack.first()))) {
                    val o2 = stack.first()
                    if (operators.containsKey(o2)) {
                        val op1 = operators[o1]!!
                        val op2 = operators[o2]!!
                        val cond = (op1.assoc == Associativity.LEFT && op1.prec <= op2.prec) ||
                                (op1.assoc == Associativity.RIGHT && op1.prec < op2.prec)
                        if (cond) out += stack.removeFirst() else break
                    } else out += stack.removeFirst()
                }
                stack.addFirst(o1)
            }
            token == "(" -> stack.addFirst(token)
            token == ")" -> {
                while (stack.isNotEmpty() && stack.first() != "(") out += stack.removeFirst()
                if (stack.isEmpty()) throw Exception("Mismatched parentheses")
                stack.removeFirst()
                if (stack.isNotEmpty() && isFunction(stack.first())) out += stack.removeFirst()
            }
            else -> throw Exception("Unknown token: $token")
        }
    }
    while (stack.isNotEmpty()) {
        val t = stack.removeFirst()
        if (t == "(" || t == ")") throw Exception("Mismatched parentheses")
        out += t
    }
    return out
}

@Throws(Exception::class)
private fun evaluateRPN(rpn: List, degreeMode: Boolean): Double {
    val stack = ArrayDeque()
    for (token in rpn) {
        when {
            token.toDoubleOrNull() != null -> stack.addFirst(token.toDouble())
            token == "+" -> binaryOp(stack) { a, b -> a + b }
            token == "-" -> binaryOp(stack) { a, b -> a - b }
            token == "*" -> binaryOp(stack) { a, b -> a * b }
            token == "/" -> binaryOp(stack) { a, b -> a / b }
            token == "^" -> binaryOp(stack) { a, b -> a.pow(b) }
            token == "%" -> binaryOp(stack) { a, b -> a % b }
            token == "!" -> {
                if (stack.isEmpty()) throw Exception("Insufficient operands for !")
                val v = stack.removeFirst()
                stack.addFirst(factorial(v))
            }
            isFunction(token) -> {
                if (stack.isEmpty()) throw Exception("Insufficient operands for $token")
                val v = stack.removeFirst()
                val arg = if (degreeMode) Math.toRadians(v) else v
                val res = when (token) {
                    "sin" -> sin(arg)
                    "cos" -> cos(arg)
                    "tan" -> tan(arg)
                    "asin" -> if (degreeMode) Math.toDegrees(asin(v)) else asin(v)
                    "acos" -> if (degreeMode) Math.toDegrees(acos(v)) else acos(v)
                    "atan" -> if (degreeMode) Math.toDegrees(atan(v)) else atan(v)
                    "ln" -> ln(v)
                    "log" -> log10(v)
                    "sqrt" -> sqrt(v)
                    else -> throw Exception("Unknown function $token")
                }
                stack.addFirst(res)
            }
            else -> throw Exception("Unknown token: $token")
        }
    }
    if (stack.size != 1) throw Exception("Invalid expression")
    return stack.first()
}

private fun binaryOp(stack: ArrayDeque, op: (Double, Double) -> Double) {
    if (stack.size < 2) throw Exception("Insufficient operands")
    val b = stack.removeFirst()
    val a = stack.removeFirst()
    stack.addFirst(op(a, b))
}

private fun factorial(x: Double): Double {
    val n = x.toInt()
    if (n < 0 || n.toDouble() != x) throw Exception("Factorial only for non-negative integers")
    var res = 1.0
    for (i in 1..n) res *= i
    return res
}

@Throws(Exception::class)
fun evaluateExpression(input: String, degreeMode: Boolean): Double {
    val cleaned = input.replace("π","pi")
    val tokens = tokenize(cleaned)
    val rpn = infixToRPN(tokens)
    return evaluateRPN(rpn, degreeMode)
}

MainActivity

class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            CBCalculatorTheme {
                
                ScientificCalculator()
                
            }
        }
    }
}

SkillDedication

Build a Tic-Tac-Toe GUI in Python — Explore Expert-Written Tutorials, Projects, and Tools to Master in Python —

output:

Scientific Calculator in Jetpack Compose Screen shot dark modeScientific Calculator in Jetpack Compose  Screenshot Screenshot light mode


5. Testing & validation

Unit tests (recommended)

  • Tokenizer tests: ensure "sin(30)+2.5" tokenizes correctly.
  • Shunting-yard tests: ensure operator precedence 2+3*4 → 2 3 4 * +.
  • RPN evaluation tests: numeric checks including sin(30) in both DEG and RAD modes.
  • Edge cases: factorial of 5.0, 5.1 (should error), mismatched parentheses errors.


Manual tests

  • Typical operations: 2+2, 10/3, sqrt(16).
  • Trig checks: sin(30) in DEG gives 0.5, in RAD gives -0.988....
  • Inverses: sin(0.5) then asin(0.5) roundtrips (with degree conversion if DEG).

6. Known limitations & trade-offs

  • Uses Double for computation — precision and rounding behaviors are those of floating point. For ultra-high precision (scientific contexts) a bigger numeric library would be needed.
  • Factorial limited to integer inputs. Extending to Gamma function requires additional math libraries or own implementation.
  • No expression editing cursor — appends/backspaces at the end only. Inserting or editing in middle would need a richer editor component.

7. Future improvements

  • Add expression cursor and selection editing (allow insert at caret).
  • History panel with previous expressions & results.
  • Memory functions (M+, M-, MR).
  • Extend factorial to Gamma for non-integer factorials.
  • Internationalization and accessibility improvements (larger buttons, TalkBack labels).

8. Conclusion

This Jetpack Compose scientific calculator balances practical UX with clean implementation: a compact expression evaluator, a modern Compose UI, clear DEG/RAD behavior, and polished result formatting. It is an excellent learning project that scales: add history, better editing, or replace the evaluator with a more powerful symbolic engine as your needs grow.

📌 FAQ – Material Design 3 Expressive Components in Jetpack Compose

1. ❓ What are Material Design "Expressive" components?

Answer:

They are enhanced versions of standard Material 3 components that include:
Richer motion/animation feedback
Support for dynamic color (Material You)
Icon integration inside controls (like switches & buttons)
New patterns like Split Button, Button Groups, and FAB Menu

2. ❓ Do I need a special library for Expressive components in Jetpack Compose?

Answer:

No 🚫 — Expressive components are part of Jetpack Compose Material3.
Some are stable (Buttons, FABs, Navigation, Sliders), while others are still experimental (Segmented Buttons, Split Button, Switch with icons).

3. ❓ Will my app automatically support dark mode & dynamic theming?

Answer:

Yes ✅ — expressive components follow Material You dynamic color automatically if you use MaterialTheme.colorScheme.
No extra work is required unless you override colors.

4. ❓ How do I keep UI state (like calculator result) when theme changes?

Answer:

Use rememberSaveable or a ViewModel.
Example:
var result by rememberSaveable { mutableStateOf("") }

This prevents reset when theme or orientation changes.

5. ❓ What is the difference between Extended FAB and FAB Menu?

Answer:

Extended FAB: A single FAB + Label for one action.
FAB Menu: A main FAB that expands into multiple actions (new in expressive patterns).

6. ❓ What is a Split Button?

Answer:

A button with two sections:
Main action (e.g., Save)
Secondary dropdown (e.g., Save As, Export)
In Compose, you can build it using Row + DropdownMenu.

7. ❓ Are Segmented Buttons the same as TabRow?

Answer:

Not exactly.
Segmented Button Group = small group of related actions (Day/Week/Month).
TabRow = navigation across different screens.

8. ❓ What is the benefit of Expressive Switch vs Regular Switch?

Answer:

Expressive Switch can show icons inside the thumb (✔️, ✖️, 🔒, etc.), giving clearer visual feedback.

9. ❓ Which expressive components are best for calculators or utility apps?

Answer:

Button Groups → mode selection (Deg/Rad, Basic/Scientific).
Switch → theme toggle.
FAB / FAB Menu → quick actions like “History” or “Clear”.
Sliders → scientific constants or precision adjustments.

10. ❓ Are all 15 components production-ready?

Answer:

Most are stable (App bars, Buttons, FABs, Nav, Sliders, Progress).
Some (Segmented Buttons, Split Button, Expressive Switch) are still experimental in Jetpack Compose.
👉 Use @OptIn(ExperimentalMaterial3Api::class) when needed.

Special Message

Welcome to Coding Bihar