Phase 5 · Advanced ComposeModule 36~30 min read

Phase 5 Capstone · 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.

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

This capstone has a complete reference solution. Run it first, explain one small feature at a time, then let students rebuild it. The core app works offline; advanced additions are optional exercises.

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.

Phase 5 Interactive concept studio

Separate application state from animated values

Boolean state → new targets → interpolation → readable feedback

Follow this interactionPress Show details once. Follow expanded changing from false to true while the demo bar moves from 25% to 100%.
01Active now
Expanded state

Decides collapsed or expanded

02Responsibility
Animation targets

Derive destination values from state

03Responsibility
Animation system

Calculates intermediate display values

04Responsibility
Profile card

Shows motion and text equivalents

Expanded state
Observe the current stateNo transfer in this frame
State snapshot below

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.

State snapshot
expanded
false
progress target
0.25
visible percentage
25%

Does 25% mean the student completed a quarter of the course?

Connect this frame to the Kotlin source

app/src/main/java/com/mastercoding/phase5/MainActivity.kt

MainActivity.kt
var expanded by rememberSaveable { mutableStateOf(false) }
    val progress by animateFloatAsState(
        targetValue = if (expanded) 1f else 0.25f,
A focused source excerpt. The complete file and runnable project are in the source section below.
Open the complete source section →
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.

  1. Extract the ZIP. In Android Studio, open the project folder containing settings.gradle.kts.
  2. Use JDK 17 or 21 and install Android SDK Platform 36 plus the Build Tools requested by Android Studio.
  3. Allow the first Gradle sync to download dependencies. No backend account or API key is needed.
  4. Select an emulator or device running API 23 or newer, then run the app configuration.
  5. Start with the expected results below. Open README.md and TOOLCHAIN.md for the exact project instructions.
PowerShell
.\gradlew.bat :app:assembleDebug :app:testDebugUnitTest
Run inside the extracted project. macOS/Linux: chmod +x gradlew, then ./gradlew :app:assembleDebug :app:testDebugUnitTest. UI tests need a running emulator: :app:connectedDebugAndroidTest.

Keep the first version small

Run the reference app, trace one input through the code, and reproduce its behavior before changing it. The short English labels are teaching literals; localization is an optional follow-up exercise.

Implementation roadmap

Android concept labInteractive walkthrough

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.

Lesson sequence1 / 6
Step 1 of 6

Run the small offline screen

The Ada card starts collapsed with a Show details button and demonstration progress at 25%.

Up nextChoose one state and two targets
A teaching simulation; diagrams illustrate the concept. Run the Kotlin examples in Android Studio.

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

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) }
Focused excerpt, lines 36–54. Select this file in Complete source code for every line.

Expected behavior

The Ada card starts collapsed with a Show details button and demonstration progress at 25%.
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

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)
Focused excerpt, lines 52–70. Select this file in Complete source code for every line.

Expected behavior

The code has one mutable UI value, and changing it selects all the new targets together.
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

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)
    ) {
Focused excerpt, lines 53–71. Select this file in Complete source code for every line.

Expected behavior

Opening the card changes progress from 25% toward 100%; closing it moves back toward 25%.
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

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)
Focused excerpt, lines 58–76. Select this file in Complete source code for every line.

Expected behavior

The background changes smoothly when the card expands and returns on collapse.
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

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.")
    }
Focused excerpt, lines 82–100. Select this file in Complete source code for every line.

Expected behavior

Details fade and expand into the card, then fade and shrink away when the same button is pressed again.
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

MainActivity.kt
                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.")
                    }
Focused excerpt, lines 76–94. Select this file in Complete source code for every line.

Expected behavior

The chosen state survives recreation, content stays reachable, and the information remains clear with animations disabled.
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.

FileWhat to explain
app/src/main/java/com/mastercoding/phase5/MainActivity.ktThe full runnable screen, saved expanded state, float and color animations, button event, and visibility transitions.
README.mdProject 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.

MainActivity.kt
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

Project acceptance checklist0 / 5 verified

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.

ActionExpected 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

MinutesClassroom activity
0–10Run the finished app first. Ask students to describe its input, state and expected output.
10–30Walk through the source files in the solution order. Pause at each named function and predict the next change.
30–50Rebuild the main feature together using the guided steps; compare behavior with the complete reference source.
50–65Pairs 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

CriterionPointsEvidence
Working core behavior40Demonstrate the project's acceptance checklist and expected scenarios.
Understand the implementation30Explain the named state owner, event callback and source files without reading a copied script.
Edge cases and verification20Reproduce the listed boundary/error cases and record the observed outcome.
Clear classroom demonstration10Show 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.