Phase 5: Animated Profile Card
Make one offline profile card expand and collapse. A single Boolean controls AnimatedVisibility, a background color, and a demonstration progress value. Students can explain every animation target without handling navigation, networking, or external assets.
Guided workshop: 1–2 hours
Before you start
- State with remember & mutableStateOf
- Animations in Compose
Application structure and responsibilities
- One screen with one main task. Finish the small working version before adding optional features.
- All required source files, Gradle configuration, manifest and resources are included in the downloadable project.
Concepts to explain: One source of truth · rememberSaveable · Animation targets · animateFloatAsState · animateColorAsState · AnimatedVisibility · Text and semantic feedback
See the concept, then explain the implementation
Follow one concrete interaction through the app. Watch which responsibility is active, inspect the state snapshot, and pause to predict what happens next. Open the Kotlin excerpt to connect each visual frame to the implementation.
Separate application state from animated values
Boolean state → new targets → interpolation → readable feedback
Decides collapsed or expanded
Derive destination values from state
Calculates intermediate display values
Shows motion and text equivalents
Start from the collapsed state
One Boolean describes the card. Initially expanded is false, so the details are hidden and the progress target is 0.25. The percentage is a demonstration of animation, not a measurement of a student's learning.
- expanded
- false
- progress target
- 0.25
- visible percentage
- 25%
Does 25% mean the student completed a quarter of the course?
No. It is a fixed display target for the collapsed demo card. This app does not measure course completion or study time.
Connect this frame to the Kotlin source
app/src/main/java/com/mastercoding/phase5/MainActivity.kt
var expanded by rememberSaveable { mutableStateOf(false) }
val progress by animateFloatAsState(
targetValue = if (expanded) 1f else 0.25f,Misconception: An animation changes the business meaning gradually.
The Boolean changes immediately. Motion presents the transition between visual values.
Misconception: Every tween is linear.
A tween has an easing curve. Do not infer exact halfway values from half the duration unless linear easing is explicitly chosen.
Misconception: The demo progress measures actual learning.
The app explicitly maps collapsed to 25% and expanded to 100%. It does not track study time or completed lessons.
Pause, step, or scrub through the interaction. This teaching model explains the included source; it does not execute Android or run tests in the browser. Verify the behavior with the downloaded app.
Download the complete project and run it
Each ZIP includes the application source, build scripts, Gradle wrapper, manifest, resources and README. The full solution is also readable below; no feature is left as an unfinished TODO.
- Extract the ZIP. In Android Studio, open the project folder containing
settings.gradle.kts. - Use JDK 17 or 21 and install Android SDK Platform 36 plus the Build Tools requested by Android Studio.
- Allow the first Gradle sync to download dependencies. No backend account or API key is needed.
- Select an emulator or device running API 23 or newer, then run the
appconfiguration. - Start with the expected results below. Open
README.mdandTOOLCHAIN.mdfor the exact project instructions.
.\gradlew.bat :app:assembleDebug :app:testDebugUnitTestKeep the first version small
Implementation roadmap
Animated Profile Card: build it one step at a time
Follow the implementation in order. Predict the result of each step, then explain the change in your own words.
Run the small offline screen
The Ada card starts collapsed with a Show details button and demonstration progress at 25%.
Step-by-step implementation and lecture solution
Follow these steps in order. Each code window shows the relevant part of a real project file; the complete, unabridged files are in the source browser and ZIP. Pause at the prediction prompt before running the next behavior.
Step 1: Run the small offline screen
Open the extracted project folder containing settings.gradle.kts, sync in Android Studio, and run app on API 23+. Follow MainActivity through MaterialTheme and Scaffold to AnimatedProfileScreen. Padding accounts for system bars, and a scrollable column keeps text reachable on short screens.
Open: app/src/main/java/com/mastercoding/phase5/MainActivity.kt
import kotlin.math.roundToInt
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
MaterialTheme {
Scaffold { innerPadding ->
AnimatedProfileScreen(Modifier.padding(innerPadding))
}
}
}
}
}
@Composable
fun AnimatedProfileScreen(modifier: Modifier = Modifier) {
var expanded by rememberSaveable { mutableStateOf(false) }Expected behavior
Pause and explain this step
List the visible elements before reading the animation code. Emphasize that this example requires no profile service or image file.
Step 2: Choose one state and two targets
expanded is a Boolean created with rememberSaveable. Its false/true values choose collapsed/expanded content, 0.25/1.0 progress, and two theme colors. The targets are derived from expanded; there is no independent progress-state timer or color-state flag to synchronize.
Open: app/src/main/java/com/mastercoding/phase5/MainActivity.kt
@Composable
fun AnimatedProfileScreen(modifier: Modifier = Modifier) {
var expanded by rememberSaveable { mutableStateOf(false) }
val progress by animateFloatAsState(
targetValue = if (expanded) 1f else 0.25f,
animationSpec = tween(durationMillis = 700),
label = "demo progress"
)
val cardColor by animateColorAsState(
targetValue = if (expanded) MaterialTheme.colorScheme.primaryContainer
else MaterialTheme.colorScheme.surfaceVariant,
animationSpec = tween(durationMillis = 400),
label = "profile card background"
)
// English labels are teaching text. Localize with strings.xml as a small extension.
Column(
modifier = modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(24.dp),
verticalArrangement = Arrangement.spacedBy(20.dp)Expected behavior
Pause and explain this step
Create a two-row table: false → collapsed/25%/surfaceVariant, true → expanded/100%/primaryContainer.
Step 3: Animate a number and display its meaning
animateFloatAsState moves progress toward the selected target using a 700 ms tween. LinearProgressIndicator consumes the animated fraction. The percentage text is computed from the same value, giving a readable equivalent to the visual bar. The screen explicitly labels this as demo progress, not measured study completion.
Open: app/src/main/java/com/mastercoding/phase5/MainActivity.kt
fun AnimatedProfileScreen(modifier: Modifier = Modifier) {
var expanded by rememberSaveable { mutableStateOf(false) }
val progress by animateFloatAsState(
targetValue = if (expanded) 1f else 0.25f,
animationSpec = tween(durationMillis = 700),
label = "demo progress"
)
val cardColor by animateColorAsState(
targetValue = if (expanded) MaterialTheme.colorScheme.primaryContainer
else MaterialTheme.colorScheme.surfaceVariant,
animationSpec = tween(durationMillis = 400),
label = "profile card background"
)
// English labels are teaching text. Localize with strings.xml as a small extension.
Column(
modifier = modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(24.dp),
verticalArrangement = Arrangement.spacedBy(20.dp)
) {Expected behavior
Pause and explain this step
Pause on targetValue, animationSpec, and label. Ask which one controls the final number and which one controls the duration.
Step 4: Animate the card color
animateColorAsState chooses primaryContainer or surfaceVariant from the current Material theme and interpolates over 400 ms. CardDefaults.cardColors applies that animated color. Color is decorative feedback; the explicit state label and button text still communicate whether details are open.
Open: app/src/main/java/com/mastercoding/phase5/MainActivity.kt
label = "demo progress"
)
val cardColor by animateColorAsState(
targetValue = if (expanded) MaterialTheme.colorScheme.primaryContainer
else MaterialTheme.colorScheme.surfaceVariant,
animationSpec = tween(durationMillis = 400),
label = "profile card background"
)
// English labels are teaching text. Localize with strings.xml as a small extension.
Column(
modifier = modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(24.dp),
verticalArrangement = Arrangement.spacedBy(20.dp)
) {
Text("Animated Profile Card", style = MaterialTheme.typography.headlineLarge)
Text("One Boolean controls the details, color, and practice progress.")
Card(colors = CardDefaults.cardColors(containerColor = cardColor), modifier = Modifier.fillMaxWidth()) {
Column(Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text("Ada", style = MaterialTheme.typography.headlineMedium)Expected behavior
Pause and explain this step
Compare the 400 ms color duration with the 700 ms progress duration. Ask students to change only one and observe the difference.
Step 5: Connect the button and visibility transition
The button flips expanded. AnimatedVisibility reads that Boolean, combines fadeIn with expandVertically on entry, and fadeOut with shrinkVertically on exit. Its content is ordinary Column and Text composables. The framework runs the transitions; no manual frame loop or delay is required.
Open: app/src/main/java/com/mastercoding/phase5/MainActivity.kt
Text(if (expanded) "Hide details" else "Show details")
}
AnimatedVisibility(
visible = expanded,
enter = fadeIn(tween(400)) + expandVertically(tween(400)),
exit = fadeOut(tween(300)) + shrinkVertically(tween(300))
) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("Today's practice", style = MaterialTheme.typography.titleMedium)
Text("1. Explain the expanded state.")
Text("2. Find each animation target.")
Text("3. Collapse the card and observe the reverse motion.")
}
}
}
}
Text("The progress is a visual demonstration: collapsed = 25%, expanded = 100%. It does not track real study time.")
Text("Try turning system animations off. The state labels and percentage still explain the result.")
}Expected behavior
Pause and explain this step
Trace click → Boolean change → new targets → animated UI. Tap during an unfinished transition and explain that the newest target determines the result.
Step 6: Check restoration and usable feedback
Rotate with details open to check that expanded restores. Try larger text and scroll to all content. Disable system animations and verify that the state label, percentage, progress semantics, and button label still explain the result. A recreated composition starts at its restored target rather than replaying a saved animation frame. English teaching labels are inline; localization is an extension.
Open: app/src/main/java/com/mastercoding/phase5/MainActivity.kt · README.md
Text("Ada", style = MaterialTheme.typography.headlineMedium)
Text("Android learner")
Text(if (expanded) "Details expanded" else "Details collapsed")
Text("Demo progress: ${(progress * 100).roundToInt()}%")
LinearProgressIndicator(progress = { progress }, modifier = Modifier.fillMaxWidth())
Button(onClick = { expanded = !expanded }) {
Text(if (expanded) "Hide details" else "Show details")
}
AnimatedVisibility(
visible = expanded,
enter = fadeIn(tween(400)) + expandVertically(tween(400)),
exit = fadeOut(tween(300)) + shrinkVertically(tween(300))
) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("Today's practice", style = MaterialTheme.typography.titleMedium)
Text("1. Explain the expanded state.")
Text("2. Find each animation target.")
Text("3. Collapse the card and observe the reverse motion.")
}Expected behavior
Pause and explain this step
Ask students which information is essential and how it is communicated when motion and color are unavailable.
Complete source code — every file
Start with MainActivity.kt, then follow the file guide. The selector also contains the tests, manifest, resources and complete Gradle configuration. The binary Gradle wrapper JAR is included in the download.
| File | What to explain |
|---|---|
| app/src/main/java/com/mastercoding/phase5/MainActivity.kt | The full runnable screen, saved expanded state, float and color animations, button event, and visibility transitions. |
| README.md | Project setup, lecture walkthrough, animation and accessibility checks, and small follow-up exercises. |
The full runnable screen, saved expanded state, float and color animations, button event, and visibility transitions.
15 complete text files. Copy the selected file with the code panel button, or download the full project above.
package com.mastercoding.phase5
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
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.unit.dp
import kotlin.math.roundToInt
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
MaterialTheme {
Scaffold { innerPadding ->
AnimatedProfileScreen(Modifier.padding(innerPadding))
}
}
}
}
}
@Composable
fun AnimatedProfileScreen(modifier: Modifier = Modifier) {
var expanded by rememberSaveable { mutableStateOf(false) }
val progress by animateFloatAsState(
targetValue = if (expanded) 1f else 0.25f,
animationSpec = tween(durationMillis = 700),
label = "demo progress"
)
val cardColor by animateColorAsState(
targetValue = if (expanded) MaterialTheme.colorScheme.primaryContainer
else MaterialTheme.colorScheme.surfaceVariant,
animationSpec = tween(durationMillis = 400),
label = "profile card background"
)
// English labels are teaching text. Localize with strings.xml as a small extension.
Column(
modifier = modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(24.dp),
verticalArrangement = Arrangement.spacedBy(20.dp)
) {
Text("Animated Profile Card", style = MaterialTheme.typography.headlineLarge)
Text("One Boolean controls the details, color, and practice progress.")
Card(colors = CardDefaults.cardColors(containerColor = cardColor), modifier = Modifier.fillMaxWidth()) {
Column(Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text("Ada", style = MaterialTheme.typography.headlineMedium)
Text("Android learner")
Text(if (expanded) "Details expanded" else "Details collapsed")
Text("Demo progress: ${(progress * 100).roundToInt()}%")
LinearProgressIndicator(progress = { progress }, modifier = Modifier.fillMaxWidth())
Button(onClick = { expanded = !expanded }) {
Text(if (expanded) "Hide details" else "Show details")
}
AnimatedVisibility(
visible = expanded,
enter = fadeIn(tween(400)) + expandVertically(tween(400)),
exit = fadeOut(tween(300)) + shrinkVertically(tween(300))
) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("Today's practice", style = MaterialTheme.typography.titleMedium)
Text("1. Explain the expanded state.")
Text("2. Find each animation target.")
Text("3. Collapse the card and observe the reverse motion.")
}
}
}
}
Text("The progress is a visual demonstration: collapsed = 25%, expanded = 100%. It does not track real study time.")
Text("Try turning system animations off. The state labels and percentage still explain the result.")
}
}Definition of done
Check an item only after demonstrating it. Progress is saved in this browser.
Try the reference solution yourself
Run each action and compare the result. Record the device, action and observed outcome. For the testing phase, run the included unit tests and the Compose test separately: a JVM test does not prove on-device UI behavior.
| Action | Expected result |
|---|---|
| Launch and let the screen settle. | Details are hidden, progress is 25%, and Show details is visible. |
| Tap Show details. | Details fade/expand in, progress settles at 100%, the background changes, and the button becomes Hide details. |
| Tap Hide details. | Details fade/shrink out and progress settles at 25%. |
| Tap repeatedly during motion, then stop. | The card settles at the newest selected state without any extra timer running. |
| Rotate while expanded. | The expanded choice is restored and the new composition starts at the restored target. |
| Disable system animations and use a larger font. | State and progress remain understandable, with all content reachable by scrolling. |
Common mistakes and repairs
- Starting a coroutine for every frame duplicates work already handled by animate*AsState.
- Separately storing an animated percentage or card color can disagree with expanded; derive the targets from one Boolean.
- A color change alone does not explain state; keep clear text and semantic progress feedback.
- The progress is intentionally a demo mapping between 25% and 100%, not real learning analytics.
- rememberSaveable restores the chosen Boolean, not the exact intermediate animation frame.
Instructor lecture plan
| Minutes | Classroom activity |
|---|---|
| 0–10 | Run the finished app first. Ask students to describe its input, state and expected output. |
| 10–30 | Walk through the source files in the solution order. Pause at each named function and predict the next change. |
| 30–50 | Rebuild the main feature together using the guided steps; compare behavior with the complete reference source. |
| 50–65 | Pairs reproduce an edge case and explain one callback or data-flow path to the class. |
Instructor discussion and answer notes
Create a two-row table: false → collapsed/25%/surfaceVariant, true → expanded/100%/primaryContainer.
expanded is a Boolean created with rememberSaveable. Its false/true values choose collapsed/expanded content, 0.25/1.0 progress, and two theme colors. The targets are derived from expanded; there is no independent progress-state timer or color-state flag to synchronize. Expected result: The code has one mutable UI value, and changing it selects all the new targets together.
Pause on targetValue, animationSpec, and label. Ask which one controls the final number and which one controls the duration.
animateFloatAsState moves progress toward the selected target using a 700 ms tween. LinearProgressIndicator consumes the animated fraction. The percentage text is computed from the same value, giving a readable equivalent to the visual bar. The screen explicitly labels this as demo progress, not measured study completion. Expected result: Opening the card changes progress from 25% toward 100%; closing it moves back toward 25%.
Compare the 400 ms color duration with the 700 ms progress duration. Ask students to change only one and observe the difference.
animateColorAsState chooses primaryContainer or surfaceVariant from the current Material theme and interpolates over 400 ms. CardDefaults.cardColors applies that animated color. Color is decorative feedback; the explicit state label and button text still communicate whether details are open. Expected result: The background changes smoothly when the card expands and returns on collapse.
Assessment rubric
| Criterion | Points | Evidence |
|---|---|---|
| Working core behavior | 40 | Demonstrate the project's acceptance checklist and expected scenarios. |
| Understand the implementation | 30 | Explain the named state owner, event callback and source files without reading a copied script. |
| Edge cases and verification | 20 | Reproduce the listed boundary/error cases and record the observed outcome. |
| Clear classroom demonstration | 10 | Show the app, trace one action and describe one small extension. |
Total: 100 points. Ask students to show the working app, explain one complete event/data path, reproduce an edge case, and make one small change themselves. Keep any limitations explicit.
Optional extensions after the core works
- Change one animation duration and compare the pace.
- Move labels into Android string resources.
- Create a second card with its own independent expanded state.
- Replace the sample practice text while keeping the same state and animation structure.
After the intervening lessons, continue with Tested Tip Calculator.
For deeper study, use the official Compose course and Android architecture guide.