Your assignment: Quadratic Equation Solver
Build a real-root solver for ax² + bx + c = 0. Learn how input validation, explicit result types, reusable fields, and mathematical branching turn a formula into a reliable small Android app.
Beginner · 3–4 hours of building
Before you begin
- Use saved Compose text-field state and pure calculation functions.
- Understand Kotlin nullable types, sealed results, and when expressions.
- Know the quadratic formula and the discriminant b² − 4ac.
- Run local JVM tests in Android Studio.
By the end, you can…
- Model two roots, a repeated root, and no real roots as distinct results.
- Handle linear and constant equations before using the quadratic formula.
- Accept signed decimal coefficients and safely reject incomplete or nonfinite text.
- Keep mathematical logic independent of composable functions.
- Explain how events change inputs and produce a new derived result.
- Use tests to verify mathematical branches and numeric limits.
Required features
- Accept a, b, and c as signed numeric text and show the equation being solved.
- Distinguish two real roots, a repeated real root, and no real roots.
- When a is zero, solve a remaining linear equation if b is nonzero.
- When both a and b are zero, distinguish infinitely many solutions from no solution.
- Show clear guidance for blank, malformed, nonfinite, and unsupported numeric-range input.
- Keep coefficients through rotation and derive the displayed result from the restored values.
- Round displayed roots to six significant digits and state that complex roots are outside the required scope.
- Provide Solve and Clear buttons, show the current equation, and display the scaled discriminant with its relationship to the original discriminant.
- Hide a previous result when any coefficient changes; Clear empties every coefficient and removes the result.
Set up the project and run the reference app
- Open: Extract the ZIP. In Android Studio choose Open, then select the project folder containing
settings.gradle.kts. Open that folder rather than just a Kotlin file. - Prepare: Use Android Studio’s bundled JDK 17 or 21 in Gradle settings. In SDK Manager install Android SDK Platform 36 and any Build Tools requested during sync.
- Sync: Allow Gradle to download the pinned dependencies. Initial setup requires internet. The app runs offline and needs no backend, login or API key.
- Run: In Device Manager create an emulator with API 23 or newer, or connect a device with USB debugging. Select the
apprun configuration and press Run. - Observe: Try the normal and boundary scenarios from Test the app. Record what changes after each action. Use Logcat to investigate a crash instead of changing several files at once.
- Practice: Copy the extracted folder to a working folder. Keep the build files, manifest, resources and
CommonUi.kt. Rebuild the application files following the steps below. Attempt each task before consulting its reference.
.\gradlew.bat :app:assembleDebug :app:testDebugUnitTestKeep the toolchain consistent
TOOLCHAIN.md for setup details.Understand the topics before coding
For each topic, connect the Kotlin concept to a visible behavior. Identify the value the screen reads and the event that can change that value.
A formula needs a decision tree
The quadratic formula divides by 2a, so a must be checked first. For a = 0, b = 2, c = -4, the problem becomes 2x − 4 = 0 and x = 2. If a and b are both zero, 0 = 0 allows every real x while 5 = 0 allows none. These cases are meaningful outcomes, not parser errors.
The discriminant controls the real-root cases
After confirming a is nonzero, the discriminant determines whether the real solution set has two distinct roots, one repeated root, or no roots. Only a nonnegative discriminant can be passed to the real square-root function. No real roots does not mean the equation is invalid; it means its solutions are outside this app's real-number scope.
Result types keep mathematical meaning explicit
TwoRoots carries two values, RepeatedRoot and Linear each carry one value, and NoSolution or InfiniteSolutions carry no numeric value. A when expression maps each case to a precise message. This avoids using an empty list or zero as an ambiguous code for several different mathematical situations.
Floating-point arithmetic has limits
Double calculations approximate real arithmetic. The reference solver scales all coefficients by the same positive value to reduce overflow, then uses an equivalent stable two-root formula to avoid subtracting nearly equal numbers. Roots remain approximate; extremely imbalanced coefficients can exceed the supported range, and discriminants very close to zero require care in more advanced numerical work.
Three different lifetimes
rememberSaveable restores small supported values after Activity recreation; a ViewModel retains screen state during configuration changes. Neither replaces permanent storage. The notes project uses Room for records that must remain after closing and reopening the app.Build the app step by step
Work in this order. The code panels are focused excerpts from the complete files below. They may begin or end inside a function: use the full source browser when copying a file, including its imports. After every step, run the app and check the stated result.
Step 1. Define the mathematical outcomes
Your task: Create EquationSolver.kt and define the EquationResult cases before drawing the screen. Include TwoRoots, RepeatedRoot, Linear, NoRealRoots, InfiniteSolutions, NoSolution, and Error.
Open: app/src/main/java/com/mastercoding/miniprojects/quadratic/EquationSolver.kt
How it works: Start by describing what the function can legitimately return. For example, x² + 1 = 0 returns NoRealRoots, while missing coefficient text returns Error. Keeping these cases separate lets the UI teach the mathematical difference instead of displaying one generic failure message for everything.
import kotlin.math.sqrt
sealed interface EquationResult {
data class TwoRoots(val first: Double, val second: Double) : EquationResult
data class RepeatedRoot(val root: Double) : EquationResult
data class Linear(val root: Double) : EquationResult
data object NoRealRoots : EquationResult
data object InfiniteSolutions : EquationResult
data object NoSolution : EquationResult
data class Error(val message: String) : EquationResult
}
fun solveEquation(aText: String, bText: String, cText: String): EquationResult {
val a = aText.trim().toDoubleOrNull()?.takeIf { it.isFinite() }
val b = bText.trim().toDoubleOrNull()?.takeIf { it.isFinite() }
val c = cText.trim().toDoubleOrNull()?.takeIf { it.isFinite() }
if (a == null || b == null || c == null) {
return EquationResult.Error("Enter a finite number for every coefficient.")
}
if (a == 0.0) {
if (b == 0.0) return if (c == 0.0) EquationResult.InfiniteSolutions else EquationResult.NoSolution
val root = -c / b
return if (root.isFinite()) EquationResult.Linear(root) else rangeError()
}
// Scaling preserves the roots and avoids overflowing b*b for large coefficients.Stop and check
Step 2. Parse all three coefficients safely
Your task: Write solveEquation with three string parameters. Trim each value, parse with toDoubleOrNull, reject nonfinite numbers, and return Error if any coefficient is unavailable.
Open: app/src/main/java/com/mastercoding/miniprojects/quadratic/EquationSolver.kt
How it works: While the user changes -3 to -4, the field may temporarily contain only a minus sign. The parser returns null and the UI can show guidance until typing is complete. Zero is a valid coefficient and must not be confused with missing input; negative coefficients must also pass validation.
}
fun solveEquation(aText: String, bText: String, cText: String): EquationResult {
val a = aText.trim().toDoubleOrNull()?.takeIf { it.isFinite() }
val b = bText.trim().toDoubleOrNull()?.takeIf { it.isFinite() }
val c = cText.trim().toDoubleOrNull()?.takeIf { it.isFinite() }
if (a == null || b == null || c == null) {
return EquationResult.Error("Enter a finite number for every coefficient.")
}
if (a == 0.0) {
if (b == 0.0) return if (c == 0.0) EquationResult.InfiniteSolutions else EquationResult.NoSolution
val root = -c / b
return if (root.isFinite()) EquationResult.Linear(root) else rangeError()
}
// Scaling preserves the roots and avoids overflowing b*b for large coefficients.
val scale = max(abs(a), max(abs(b), abs(c)))
val normalizedA = a / scale
val normalizedB = b / scale
val normalizedC = c / scale
if (normalizedA == 0.0 || (b != 0.0 && normalizedB == 0.0) || (c != 0.0 && normalizedC == 0.0)) {
return rangeError()
}
val discriminant = normalizedB * normalizedB - 4.0 * normalizedA * normalizedC
if (discriminant < 0.0) return EquationResult.NoRealRoots
if (discriminant == 0.0) {Stop and check
Step 3. Resolve linear and constant equations first
Your task: Before computing a discriminant, branch on a == 0. If b is nonzero, return -c / b as a linear solution after checking finiteness. Otherwise return InfiniteSolutions for c == 0 and NoSolution for nonzero c.
Open: app/src/main/java/com/mastercoding/miniprojects/quadratic/EquationSolver.kt
How it works: Trace 0x² + 2x − 4 = 0: the a branch is taken, b is nonzero, and -(-4) / 2 gives 2. Trace 0x² + 0x + 5 = 0: both variable terms disappear and the constant is nonzero, so no x can satisfy the equation. This ordering avoids division by zero in the quadratic formula.
return EquationResult.Error("Enter a finite number for every coefficient.")
}
if (a == 0.0) {
if (b == 0.0) return if (c == 0.0) EquationResult.InfiniteSolutions else EquationResult.NoSolution
val root = -c / b
return if (root.isFinite()) EquationResult.Linear(root) else rangeError()
}
// Scaling preserves the roots and avoids overflowing b*b for large coefficients.
val scale = max(abs(a), max(abs(b), abs(c)))
val normalizedA = a / scale
val normalizedB = b / scale
val normalizedC = c / scale
if (normalizedA == 0.0 || (b != 0.0 && normalizedB == 0.0) || (c != 0.0 && normalizedC == 0.0)) {
return rangeError()
}
val discriminant = normalizedB * normalizedB - 4.0 * normalizedA * normalizedC
if (discriminant < 0.0) return EquationResult.NoRealRoots
if (discriminant == 0.0) {
val root = -normalizedB / (2.0 * normalizedA)
return if (root.isFinite()) EquationResult.RepeatedRoot(root) else rangeError()
}
// This equivalent form avoids subtracting nearly equal numbers for the smaller root.
val squareRoot = sqrt(discriminant)
val signedRoot = if (normalizedB >= 0.0) squareRoot else -squareRootStop and check
Step 4. Classify the discriminant
Your task: For a nonzero quadratic, scale a, b, and c by their largest absolute coefficient, then compute the discriminant from the scaled values. Return NoRealRoots for a negative value and RepeatedRoot for zero.
Open: app/src/main/java/com/mastercoding/miniprojects/quadratic/EquationSolver.kt
How it works: Dividing the whole equation by the same nonzero number preserves its solutions while reducing the chance of overflowing b × b. The sign of the discriminant still classifies the roots. For (1, 2, 1), the discriminant is zero and -b / (2a) yields -1. For (1, 0, 1), it is negative, so the real square-root step is skipped.
return rangeError()
}
val discriminant = normalizedB * normalizedB - 4.0 * normalizedA * normalizedC
if (discriminant < 0.0) return EquationResult.NoRealRoots
if (discriminant == 0.0) {
val root = -normalizedB / (2.0 * normalizedA)
return if (root.isFinite()) EquationResult.RepeatedRoot(root) else rangeError()
}
// This equivalent form avoids subtracting nearly equal numbers for the smaller root.
val squareRoot = sqrt(discriminant)
val signedRoot = if (normalizedB >= 0.0) squareRoot else -squareRoot
val q = -0.5 * (normalizedB + signedRoot)
val first = q / normalizedA
val second = normalizedC / q
if (!first.isFinite() || !second.isFinite()) return rangeError()
return EquationResult.TwoRoots(minOf(first, second), maxOf(first, second))
}
private fun rangeError() = EquationResult.Error("These coefficients exceed the reliable numeric range. Try values closer in size.")
fun normalizedDiscriminant(aText: String, bText: String, cText: String): Double? {
val a = aText.trim().toDoubleOrNull()?.takeIf { it.isFinite() && it != 0.0 } ?: return null
val b = bText.trim().toDoubleOrNull()?.takeIf { it.isFinite() } ?: return null
val c = cText.trim().toDoubleOrNull()?.takeIf { it.isFinite() } ?: return null
val scale = max(abs(a), max(abs(b), abs(c)))Stop and check
Step 5. Calculate and order two roots
Your task: For a positive discriminant, implement the provided q form of the quadratic formula, calculate q / a and c / q using normalized coefficients, verify both roots are finite, and return them in ascending order.
Open: app/src/main/java/com/mastercoding/miniprojects/quadratic/EquationSolver.kt
How it works: The ordinary plus/minus formula is mathematically correct, but subtracting almost equal floating-point values can erase a small root. Choosing q with the same sign contribution as b avoids that subtraction, and the product-of-roots identity supplies the other root. For the required beginner exercise, understand the discriminant branches first, then use the documented helper for this numerical refinement.
// This equivalent form avoids subtracting nearly equal numbers for the smaller root.
val squareRoot = sqrt(discriminant)
val signedRoot = if (normalizedB >= 0.0) squareRoot else -squareRoot
val q = -0.5 * (normalizedB + signedRoot)
val first = q / normalizedA
val second = normalizedC / q
if (!first.isFinite() || !second.isFinite()) return rangeError()
return EquationResult.TwoRoots(minOf(first, second), maxOf(first, second))
}
private fun rangeError() = EquationResult.Error("These coefficients exceed the reliable numeric range. Try values closer in size.")
fun normalizedDiscriminant(aText: String, bText: String, cText: String): Double? {
val a = aText.trim().toDoubleOrNull()?.takeIf { it.isFinite() && it != 0.0 } ?: return null
val b = bText.trim().toDoubleOrNull()?.takeIf { it.isFinite() } ?: return null
val c = cText.trim().toDoubleOrNull()?.takeIf { it.isFinite() } ?: return null
val scale = max(abs(a), max(abs(b), abs(c)))
val normalizedA = a / scale
val normalizedB = b / scale
val normalizedC = c / scale
return normalizedB * normalizedB - 4.0 * normalizedA * normalizedC
}Stop and check
Step 6. Build reusable coefficient fields
Your task: In QuadraticSolver, declare saved a, b, and c strings and a saved showResult Boolean. Derive result with solveEquation and call CoefficientField three times. Give each field a label and a keyboard that permits negative values; hide the previous result in every edit callback.
Open: app/src/main/java/com/mastercoding/miniprojects/quadratic/MainActivity.kt
How it works: Changing c from 2 to 1 updates c and sets showResult to false, removing stale roots. Solve sets showResult to true so the current derived outcome becomes visible. The field receives its value and callback as parameters rather than owning another coefficient copy. The equation preview reads the same strings as the fields.
@Composable
fun QuadraticSolver() {
var a by rememberSaveable { mutableStateOf("1") }
var b by rememberSaveable { mutableStateOf("-3") }
var c by rememberSaveable { mutableStateOf("2") }
var showResult by rememberSaveable { mutableStateOf(false) }
val result = solveEquation(a, b, c)
Text("Solve ax² + bx + c = 0 for real x.", style = MaterialTheme.typography.titleMedium)
Text("Enter positive, negative, or zero coefficients. Use a dot for decimals.")
CoefficientField(label = "a", value = a, onChange = { a = it; showResult = false })
CoefficientField(label = "b", value = b, onChange = { b = it; showResult = false })
CoefficientField(label = "c", value = c, onChange = { c = it; showResult = false })
Text("Current equation: (${a.ifBlank { "?" }})x² + (${b.ifBlank { "?" }})x + (${c.ifBlank { "?" }}) = 0")
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
Button(onClick = { showResult = true }) { Text("Solve") }
OutlinedButton(onClick = { a = ""; b = ""; c = ""; showResult = false }) { Text("Clear") }
}
val message = when (result) {
is EquationResult.TwoRoots -> "Two real roots: x₁ = ${number(result.first)}, x₂ = ${number(result.second)}"
is EquationResult.RepeatedRoot -> "One repeated real root: x = ${number(result.root)}"
is EquationResult.Linear -> "Linear equation: x = ${number(result.root)}"
EquationResult.NoRealRoots -> "No real roots. The discriminant is negative."
EquationResult.InfiniteSolutions -> "Infinitely many solutions: every real x works."
EquationResult.NoSolution -> "No solution: a nonzero constant cannot equal zero."
is EquationResult.Error -> result.messageStop and check
Step 7. Add Solve, Clear, and a complete result explanation
Your task: Add Solve to reveal the current result and Clear to empty the coefficients and hide output. Use an exhaustive when expression for every EquationResult. For valid quadratics, display normalizedDiscriminant and explain that scaling preserves the sign of b squared minus 4ac.
Open: app/src/main/java/com/mastercoding/miniprojects/quadratic/MainActivity.kt
How it works: For a = 0 and b = 2, Solve says Linear equation rather than pretending the app found a quadratic root. For (1, -3, 2), the normalized discriminant is about 0.111111: all coefficients were divided by 3, so its value is the original discriminant divided by 9. The sign still indicates two roots. Clear removes the equation inputs and result together.
OutlinedButton(onClick = { a = ""; b = ""; c = ""; showResult = false }) { Text("Clear") }
}
val message = when (result) {
is EquationResult.TwoRoots -> "Two real roots: x₁ = ${number(result.first)}, x₂ = ${number(result.second)}"
is EquationResult.RepeatedRoot -> "One repeated real root: x = ${number(result.root)}"
is EquationResult.Linear -> "Linear equation: x = ${number(result.root)}"
EquationResult.NoRealRoots -> "No real roots. The discriminant is negative."
EquationResult.InfiniteSolutions -> "Infinitely many solutions: every real x works."
EquationResult.NoSolution -> "No solution: a nonzero constant cannot equal zero."
is EquationResult.Error -> result.message
}
if (!showResult) Text("Tap Solve to calculate the current equation.")
if (showResult) Text(
message,
style = MaterialTheme.typography.titleLarge,
color = if (result is EquationResult.Error) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurface
)
val discriminant = normalizedDiscriminant(a, b, c)
if (showResult && result !is EquationResult.Error && discriminant != null) {
Text("Normalized discriminant D′ = ${number(discriminant)}")
Text("D′ = (b/s)² − 4(a/s)(c/s), with s = max(|a|, |b|, |c|). Its sign matches D = b² − 4ac; scaling avoids overflow for large coefficients.")
}
Text("Results use floating-point arithmetic and are rounded to six significant digits. Complex roots are outside this exercise.")
}
@ComposableStop and check
Step 8. Verify branches and restore the form
Your task: Run EquationSolverTest, add a decimal-coefficient example, and manually test each acceptance scenario with Solve. Solve a different equation, rotate the device, verify the result, and test Clear.
Open: app/src/test/java/com/mastercoding/miniprojects/quadratic/EquationSolverTest.kt
How it works: One happy-path example cannot verify the decision tree. The tests cover two roots, one root, no real roots, a linear equation, both constant cases, zero roots, invalid input, and large coefficients. Rotation separately checks that the UI restores source state; the solver itself has no Android lifecycle dependency.
}
@Test fun handlesEveryDegenerateEquation() {
assertEquals(2.0, (solveEquation("0", "2", "-4") as EquationResult.Linear).root, 0.0)
assertEquals(EquationResult.InfiniteSolutions, solveEquation("0", "0", "0"))
assertEquals(EquationResult.NoSolution, solveEquation("0", "0", "5"))
}
@Test fun acceptsZeroAndNegativeCoefficients() {
val result = solveEquation("-1", "0", "4") as EquationResult.TwoRoots
assertEquals(-2.0, result.first, 0.000001)
assertEquals(2.0, result.second, 0.000001)
val zeroRoot = solveEquation("1", "-2", "0") as EquationResult.TwoRoots
assertEquals(0.0, zeroRoot.first, 0.0)
assertEquals(2.0, zeroRoot.second, 0.0)
}
@Test fun scalesLargeCoefficientsAndPreservesSmallRoot() {
val large = solveEquation("1e200", "-3e200", "2e200") as EquationResult.TwoRoots
assertEquals(1.0, large.first, 0.000001)
assertEquals(2.0, large.second, 0.000001)
val imbalanced = solveEquation("1", "1e16", "1") as EquationResult.TwoRoots
assertEquals(-1e16, imbalanced.first, 1.0)
assertEquals(-1e-16, imbalanced.second, 1e-25)
}Stop and check
Complete source files
Every text file in the ZIP is available here, including Kotlin, resources, Gradle scripts and the student README. The binary Gradle wrapper JAR is included in the ZIP. Source excerpts and downloads are generated from these same files.
| Start here | Responsibility |
|---|---|
| MainActivity.kt | The launcher Activity installs the Compose UI with setContent. |
| CommonUi.kt | Material 3 theme and reusable screen container with scrolling and system insets. |
| Application Kotlin files | State, rules, navigation or persistence introduced in the guide. |
| app/src/main/res | Images, text resources, and the launch theme. |
| app/build.gradle.kts | SDK settings and the exact libraries used by this app. |
| README.md / TOOLCHAIN.md | Student instructions and setup, usable without the website. |
Android launcher Activity, setContent entry point and screen composition. Start tracing the app here.
19 complete text files. Copy the selected file with the code panel button, or download the full project above.
package com.mastercoding.miniprojects.quadratic
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Button
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import java.util.Locale
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MiniTheme { MiniScreen("Quadratic Equation Solver") { QuadraticSolver() } }
}
}
}
@Composable
fun QuadraticSolver() {
var a by rememberSaveable { mutableStateOf("1") }
var b by rememberSaveable { mutableStateOf("-3") }
var c by rememberSaveable { mutableStateOf("2") }
var showResult by rememberSaveable { mutableStateOf(false) }
val result = solveEquation(a, b, c)
Text("Solve ax² + bx + c = 0 for real x.", style = MaterialTheme.typography.titleMedium)
Text("Enter positive, negative, or zero coefficients. Use a dot for decimals.")
CoefficientField(label = "a", value = a, onChange = { a = it; showResult = false })
CoefficientField(label = "b", value = b, onChange = { b = it; showResult = false })
CoefficientField(label = "c", value = c, onChange = { c = it; showResult = false })
Text("Current equation: (${a.ifBlank { "?" }})x² + (${b.ifBlank { "?" }})x + (${c.ifBlank { "?" }}) = 0")
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
Button(onClick = { showResult = true }) { Text("Solve") }
OutlinedButton(onClick = { a = ""; b = ""; c = ""; showResult = false }) { Text("Clear") }
}
val message = when (result) {
is EquationResult.TwoRoots -> "Two real roots: x₁ = ${number(result.first)}, x₂ = ${number(result.second)}"
is EquationResult.RepeatedRoot -> "One repeated real root: x = ${number(result.root)}"
is EquationResult.Linear -> "Linear equation: x = ${number(result.root)}"
EquationResult.NoRealRoots -> "No real roots. The discriminant is negative."
EquationResult.InfiniteSolutions -> "Infinitely many solutions: every real x works."
EquationResult.NoSolution -> "No solution: a nonzero constant cannot equal zero."
is EquationResult.Error -> result.message
}
if (!showResult) Text("Tap Solve to calculate the current equation.")
if (showResult) Text(
message,
style = MaterialTheme.typography.titleLarge,
color = if (result is EquationResult.Error) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurface
)
val discriminant = normalizedDiscriminant(a, b, c)
if (showResult && result !is EquationResult.Error && discriminant != null) {
Text("Normalized discriminant D′ = ${number(discriminant)}")
Text("D′ = (b/s)² − 4(a/s)(c/s), with s = max(|a|, |b|, |c|). Its sign matches D = b² − 4ac; scaling avoids overflow for large coefficients.")
}
Text("Results use floating-point arithmetic and are rounded to six significant digits. Complex roots are outside this exercise.")
}
@Composable
private fun CoefficientField(label: String, value: String, onChange: (String) -> Unit) {
OutlinedTextField(
value = value,
onValueChange = onChange,
label = { Text("Coefficient $label") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
}
private fun number(value: Double): String = String.format(Locale.US, "%.6g", if (value == 0.0) 0.0 else value)Test the app with concrete scenarios
Run these scenarios on your device or emulator. Write down the actual result beside the expected result. A successful Gradle build checks compilation; it does not demonstrate tapping, accessibility, keyboard behavior or rotation.
| Do this | Expected result |
|---|---|
| Enter a = 1, b = -3, c = 2. Tap Solve for each equation. | The app identifies two real roots, 1 and 2, in ascending order. |
| Enter a = 1, b = 2, c = 1. Tap Solve for each equation. | The app identifies the repeated real root -1. |
| Enter a = 1, b = 0, c = 1. Tap Solve for each equation. | The app says there are no real roots because the discriminant is negative. |
| Enter a = 0, b = 2, c = -4. Tap Solve for each equation. | The app identifies a linear equation with x = 2. |
| Enter (0, 0, 0), then (0, 0, 5). Tap Solve for each equation. | The first has infinitely many solutions; the second has no solution. |
| Enter a = -1, b = 0, c = 4, then (1, -2, 0). Tap Solve for each equation. | The roots are -2 and 2 in the first case and 0 and 2 in the second. |
| Clear a field, enter only a minus sign, paste letters, or try NaN and Infinity. Tap Solve for each equation. | A finite-number message appears and no stale roots remain visible. |
| Enter (1, 0, -9), tap Solve, rotate, and inspect every field. | The inputs remain 1, 0, and -9 and the visible result remains roots -3 and 3. |
| Enter (1e200, -3e200, 2e200). Tap Solve for each equation. | Scaling allows the solver to return approximately 1 and 2 instead of overflowing the discriminant. |
| Solve (1, -3, 2), inspect D-prime, then edit c and tap Clear. | The equation preview matches the fields. D-prime is approximately 0.111111 with an explanation of scaling. Editing hides old roots; Clear empties every coefficient and result. |
Your completion checklist
Check an item only after demonstrating it. Progress is saved in this browser.
Troubleshooting: understand the cause
Setting a to zero produces Infinity or NaN.
Cause: The quadratic formula divides by 2a before the degenerate equation is checked.
Repair: Handle linear and constant equations before entering the quadratic branch.
A negative discriminant produces an unexplained NaN.
Cause: sqrt is called before checking the discriminant sign.
Repair: Return NoRealRoots for a negative discriminant and call the real square-root function only for a positive value.
The app rejects b = 0 or c = 0 as missing input.
Cause: Validation treats zero as false or uses zero as a sentinel for parse failure.
Repair: Use nullable parsing and check null separately from the legitimate number zero.
One root disappears when b is much larger than the other coefficients.
Cause: The ordinary formula subtracts nearly equal floating-point values.
Repair: Use the documented q form in the helper and test an imbalanced example; keep a clear numerical-range error for unsupported cases.
The field resets or a previous equation's roots remain after rotation.
Cause: Inputs were ordinary local variables or the result was stored separately from its dependencies.
Repair: Save only the coefficient strings with rememberSaveable and derive the current result from them.
Submit your app and explain your decisions
- Submit the Android Studio project without build caches,
.gradleor machine-specificlocal.properties. - Include two screenshots demonstrating normal behavior and an edge case.
- Add a short README recording the device/API version, actions, observed outcomes, and any known limitation.
- Answer the questions below using function and state names from your own implementation.
- Why must the a = 0 case be checked before using the quadratic formula?
- How are NoRealRoots, NoSolution, and Error different from one another? Give an example of each.
- Which values must survive rotation, and why can the result be calculated again instead of saved?
- Why can two algebraically equivalent formulas behave differently with floating-point numbers?
| Criterion | Points | What to demonstrate |
|---|---|---|
| Core behavior | 40 | All required actions give the expected result. |
| Validation and lifecycle | 20 | Invalid inputs, empty states, rotation and the project's persistence rules work. |
| Interface and accessibility | 20 | Readable contrast, labeled inputs, meaningful image descriptions, scrollable content and usable touch targets. |
| Code and explanation | 20 | Small named functions, clear state ownership and a correct explanation of one event-to-result trace. |
Extra challenges after the core works
- Add complex roots with separate real and imaginary components and corresponding tests.
- Show a step-by-step discriminant explanation using the original coefficients.
- Add a local history of solved equations after learning persistence.
- Investigate near-zero discriminants and document a scale-aware numerical tolerance policy before changing classification.
Choose one extension, write its acceptance check first, then implement it without breaking the original scenarios.