An interactive walkthrough of every XML constraint, lifecycle method, and calculator algorithm. Click anything to explore further.
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.
Hover over the colored boxes to understand each element's constraints.
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.
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.<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:
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.<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">
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.
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.<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"/>
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.
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 }
| Variable | Type | Purpose | Example Value |
|---|---|---|---|
currentInput | String | Digit characters being typed. String (not double) so we control decimals and display exactly. | "42.5" |
operator | String | Stores which math operation is pending. Empty string means no pending op. | "+" |
operandA | double | The left-hand number captured when user presses an operator button. | 42.5 |
operatorClicked | boolean | True after an operator is pressed. Tells appendNumber() to start a fresh number rather than appending. | true |
isDarkMode | boolean | Current theme state. Saved across rotation. | true |
onSaveInstanceStateWhen 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.
// 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()); }
@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")); } }
getString("key", "defaultValue") is the fallback if the key doesn't exist. Always provide sensible defaults so the app never crashes on first launch.Each button type maps to a dedicated method. Click any card to see the full code and explanation in a modal.
This is the exact logic from MainActivity.java running in JavaScript. Watch the state variables update in real time as you press buttons.
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); } }
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.
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("☀️"); } }
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.isDarkMode is saved in onSaveInstanceState, the theme persists even after screen rotation — then applyTheme() is called in onCreate to re-apply it.layout_width="0dp" used for tvDisplay?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.
onSaveInstanceState?operatorClicked flag?appendNumber() clears currentInput first.
if (result == (long) result) achieve?