📱 Android Dev · Calculator App

Layout + Logic
Deep Dive

An interactive walkthrough of every XML constraint, lifecycle method, and calculator algorithm. Click anything to explore further.

01 / XML LAYOUT

Why Each Constraint Matters

The UI uses ConstraintLayout as the root container for flat, performant positioning. Inside it lives a GridLayout for the button matrix and a TextView for the display.

ConstraintLayout → root GridLayout → buttons 0dp = match_constraint ViewBinding → type-safe refs

📐 Live Constraint Map

Hover over the colored boxes to understand each element's constraints.

ConstraintLayout (parent)
tvDisplay — top+left+right to parent, width=0dp → full width 123 🌙
AC
+/-
%
÷
7
8
9
×
4
5
6
×1.08
0
.
=
GridLayout — top_toBottomOf tvDisplay, bottom_toBottomOf parent, fills remaining space

🔍 Constraint Deep Dives

📺 tvDisplay — Why width=0dp?

Setting layout_width="0dp" in ConstraintLayout means "match constraint" — the view expands to fill the space defined by its left and right constraints. Since both are anchored to parent, it becomes full-width on any screen size.

This is the ConstraintLayout equivalent of match_parent, but works relative to constraints rather than just the parent. If the right constraint was anchored to a button instead of the parent, the display would stop there.
activity_main.xml
<TextView
    android:id="@+id/tvDisplay"
    android:layout_width="0dp"          ← match_constraint
    android:layout_height="wrap_content"
    android:gravity="end"              ← numbers align right
    android:textSize="48sp"
    android:maxLines="2"
    android:ellipsize="start"         ← truncate from left
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"/>

ellipsize="start" means very long numbers get truncated at the start (e.g., ...456789), preserving the most recent digits — exactly right for a calculator.

🟦 GridLayout — How it fills remaining space

The GridLayout uses four constraints simultaneously to fill all remaining vertical and horizontal space:

top → bottom of tvDisplay
+
bottom → parent bottom
+
left → parent left
+
right → parent right
With width=0dp + height=0dp, and constraints on all four sides, the GridLayout expands to fill every remaining pixel — on any phone or tablet. This is the power of ConstraintLayout over LinearLayout.
activity_main.xml
<GridLayout
    android:layout_width="0dp"
    android:layout_height="0dp"
    android:columnCount="4"
    android:rowCount="6"
    app:layout_constraintTop_toBottomOf="@id/tvDisplay"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent">
🌙 Theme Button — Top-right float, why only 2 constraints?

Unlike the display and grid, the theme button only needs top and right constraints — it's a floating button that doesn't need to stretch or participate in the layout flow.

Note: backgroundTint="@android:color/transparent" removes the default Material button background, making it appear as a bare emoji icon. This is intentional — it keeps the UI clean.
activity_main.xml
<Button
    android:id="@+id/btnTheme"
    android:layout_width="wrap_content"
    android:backgroundTint="@android:color/transparent"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    android:layout_marginTop="8dp"
    android:layout_marginRight="8dp"/>
02 / MAINACTIVITY

The Brain: State Variables

The activity manages all calculator logic using five state variables. Understanding what each one does is key to understanding how the calculator works as a state machine.

MainActivity.java — Field Declarations
public class MainActivity extends AppCompatActivity {

    private ActivityMainBinding binding;       // type-safe view references
    private String currentInput = "";         // digit string being built
    private String operator = "";             // pending +, -, *, /
    private double operandA = 0;              // left-hand value
    private boolean isDarkMode = true;       // current theme
    private boolean operatorClicked = false;  // flag: fresh number needed
}

🧩 State Variable Roles

VariableTypePurposeExample Value
currentInputStringDigit characters being typed. String (not double) so we control decimals and display exactly."42.5"
operatorStringStores which math operation is pending. Empty string means no pending op."+"
operandAdoubleThe left-hand number captured when user presses an operator button.42.5
operatorClickedbooleanTrue after an operator is pressed. Tells appendNumber() to start a fresh number rather than appending.true
isDarkModebooleanCurrent theme state. Saved across rotation.true
Why store currentInput as String? If we stored it as a double, we'd lose control over how many decimal places display and couldn't tell the difference between "5" and "5.0". Strings let us append characters naturally and control the display exactly.
03 / LIFECYCLE

Surviving Rotation: onSaveInstanceState

When you rotate your phone, Android destroys and recreates the activity. Without saving state, every variable resets to its default — your mid-calculation data is gone.

User rotates phone
onSaveInstanceState()
Activity destroyed
Activity recreated
onCreate(bundle)
State restored ✓
💾 Full save + restore code explained
onSaveInstanceState — writing to bundle
// Called automatically by Android before destroying the activity
@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putString("currentInput", currentInput);
    outState.putString("operator", operator);
    outState.putDouble("operandA", operandA);
    outState.putBoolean("isDarkMode", isDarkMode);
    outState.putBoolean("operatorClicked", operatorClicked);
    outState.putString("displayText", binding.tvDisplay.getText().toString());
}
onCreate — restoring from bundle
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    binding = ActivityMainBinding.inflate(getLayoutInflater());
    setContentView(binding.getRoot());

    // Bundle is null on first launch, non-null after rotation
    if (savedInstanceState != null) {
        currentInput = savedInstanceState.getString("currentInput", "");
        operator     = savedInstanceState.getString("operator", "");
        operandA     = savedInstanceState.getDouble("operandA", 0);
        isDarkMode   = savedInstanceState.getBoolean("isDarkMode", true);
        operatorClicked = savedInstanceState.getBoolean("operatorClicked", false);
        binding.tvDisplay.setText(savedInstanceState.getString("displayText", "0"));
    }
}
The second argument in getString("key", "defaultValue") is the fallback if the key doesn't exist. Always provide sensible defaults so the app never crashes on first launch.
04 / METHODS

Calculator Methods — Click to Explore

Each button type maps to a dedicated method. Click any card to see the full code and explanation in a modal.

appendNumber()
0–9 buttons
Appends a digit to currentInput. Resets if operatorClicked is true.
appendDecimal()
. button
Adds a decimal point, prevents duplicates, adds leading zero.
setOperator()
+ − × ÷
Stores operandA and records which operator to use when = is pressed.
calculate()
= button
Performs arithmetic with division-by-zero guard. Formats result cleanly.
clear()
AC button
Resets all state variables and sets display back to "0".
toggleSign()
+/− button
Flips the sign of the current number using string manipulation.
percent()
% button
Divides current value by 100. Preserves integer display if result is whole.
customOperator()
×1.08 button
Multiplies by 1.08 — an 8% VAT/tip calculator. Shows extensibility.
05 / INTERACTIVE SIMULATOR

Live Calculator + State Viewer

This is the exact logic from MainActivity.java running in JavaScript. Watch the state variables update in real time as you press buttons.

0
// Calculator ready. Press any button.
currentInput
""
operator
""
operandA
0
operatorClicked
false
06 / CODE EXPLORER

Browse All Source Files

public class MainActivity extends AppCompatActivity {

    private ActivityMainBinding binding;
    private String currentInput = "";
    private String operator = "";
    private double operandA = 0;
    private boolean isDarkMode = true;
    private boolean operatorClicked = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());
        if (savedInstanceState != null) {
            currentInput    = savedInstanceState.getString("currentInput", "");
            operator        = savedInstanceState.getString("operator", "");
            operandA        = savedInstanceState.getDouble("operandA", 0);
            isDarkMode      = savedInstanceState.getBoolean("isDarkMode", true);
            operatorClicked = savedInstanceState.getBoolean("operatorClicked", false);
            binding.tvDisplay.setText(savedInstanceState.getString("displayText", "0"));
        }
        setupButtons();
        applyTheme();
    }
    // ... (see other tabs)
}
<androidx.constraintlayout.widget.ConstraintLayout
    android:id="@+id/rootLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/tvDisplay"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:textSize="48sp"
        android:gravity="end"
        android:maxLines="2"
        android:ellipsize="start"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"/>

    <GridLayout
        android:id="@+id/gridButtons"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:columnCount="4"
        android:rowCount="6"
        app:layout_constraintTop_toBottomOf="@id/tvDisplay"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent">
        <!-- 20 buttons here -->
    </GridLayout>

</androidx.constraintlayout.widget.ConstraintLayout>
private void calculate() {
    // Guard: need both operator and a second operand
    if (operator.isEmpty() || currentInput.isEmpty()) return;

    double operandB = Double.parseDouble(currentInput);
    double result = 0;

    switch (operator) {
        case "+": result = operandA + operandB; break;
        case "-": result = operandA - operandB; break;
        case "*": result = operandA * operandB; break;
        case "/":
            if (operandB == 0) {
                binding.tvDisplay.setText("Cannot divide by zero");
                currentInput = "";
                operator = "";
                return;          // early exit, no crash
            }
            result = operandA / operandB;
            break;
    }

    // Clean display: show "5" not "5.0"
    if (result == (long) result) {
        currentInput = String.valueOf((long) result);
    } else {
        currentInput = String.valueOf(result);
    }

    binding.tvDisplay.setText(currentInput);
    operator = "";
    operatorClicked = true;   // next digit starts fresh
}
private void appendNumber(String num) {
    // After pressing an operator, the next digit starts fresh
    if (operatorClicked) {
        currentInput = "";
        operatorClicked = false;
    }
    currentInput += num;
    binding.tvDisplay.setText(currentInput);
}

private void appendDecimal() {
    // If just pressed operator, start "0."
    if (operatorClicked) {
        currentInput = "0";
        operatorClicked = false;
    }
    // Prevent "42..5" — only add if no dot yet
    if (!currentInput.contains(".")) {
        if (currentInput.isEmpty()) currentInput = "0";  // "." → "0."
        currentInput += ".";
        binding.tvDisplay.setText(currentInput);
    }
}
07 / THEME TOGGLE

Dynamic Dark / Light Mode

Instead of recreating the activity with a new theme resource, the app changes colors at runtime using setBackgroundColor() and setTextColor(). This gives an instant, seamless switch.

applyTheme() — runtime color switching
private void applyTheme() {
    if (isDarkMode) {
        binding.rootLayout.setBackgroundColor(getColor(R.color.calc_background_dark));
        binding.tvDisplay.setTextColor(getColor(R.color.calc_text_dark));
        binding.btnTheme.setText("🌙");
    } else {
        binding.rootLayout.setBackgroundColor(getColor(R.color.calc_background_light));
        binding.tvDisplay.setTextColor(getColor(R.color.calc_text_light));
        binding.btnTheme.setText("☀️");
    }
}
Why not use recreate()? Calling recreate() would restart the activity, triggering a flash and potentially losing state mid-calculation. Direct color setting is instant and keeps the app in the exact same state.
Because isDarkMode is saved in onSaveInstanceState, the theme persists even after screen rotation — then applyTheme() is called in onCreate to re-apply it.
08 / KNOWLEDGE CHECK

Test Yourself

1. Why is layout_width="0dp" used for tvDisplay?
A
It makes the view invisible
B
It means "match constraint" — stretch between left and right constraints
C
It's required for TextViews to show text
D
It sets a fixed 0px width
✓ Correct! In ConstraintLayout, 0dp means "match constraint" — the view fills the space defined by its opposing constraints. Since both left and right are anchored to parent, it becomes full width on any screen.
2. What happens if you DON'T implement onSaveInstanceState?
A
The app crashes immediately
B
The theme toggle stops working
C
Rotating the screen resets all calculator state (display, operands, operator)
D
Nothing — Android saves state automatically
✓ Correct! Android destroys and recreates the activity on rotation. Without saving state, all Java fields reset to defaults — display shows "0", any mid-calculation is lost.
3. What is the purpose of the operatorClicked flag?
A
It tracks which operator button was pressed last
B
It prevents the operator buttons from being clicked twice
C
It tells appendNumber() to start fresh instead of appending to the previous number
D
It enables the = button
✓ Correct! After pressing +, the next digit press should start a new number — not append to operandA. The flag signals this: if true, appendNumber() clears currentInput first.
4. What does if (result == (long) result) achieve?
A
It checks if the result is negative
B
It detects whole numbers so "5" displays instead of "5.0"
C
It rounds the result to the nearest integer
D
It converts the result to a long for storage
✓ Correct! Casting a double to long truncates the decimal. If the result equals its truncated self (e.g., 5.0 == 5), it has no meaningful decimal part, so we display the cleaner integer form.