Your assignment: Student Profile Card
Build an editable student introduction card with a local avatar, reusable information rows, and an about-me toggle. Learn how a Compose screen changes when its state changes.
Beginner · 1–2 hours of building
Before you begin
- Create and run an Empty Activity project in Android Studio.
- Read Kotlin functions, variables, strings, and if expressions.
- Know that a composable describes part of the user interface.
By the end, you can…
- Arrange content with Column, Row, spacing, and modifiers.
- Load a bundled vector drawable without a network connection.
- Connect a text field and a button to saved screen state.
- Extract a reusable composable with parameters.
- Check the screen with long text, rotation, and accessibility tools.
- Represent a single selected language with one saved string.
Required features
- Show an avatar, editable display name, learning goal, interests, and a short biography.
- Update the displayed name immediately when the text field changes.
- Show Your name when the field contains only whitespace.
- Allow the user to hide and show the biography with a clearly labelled button.
- Keep the typed name and biography visibility when the activity is recreated on rotation.
- Keep all assets local and make the screen usable when content needs to scroll.
- Offer Kotlin, Java, and Python as favorite-language choices, show the selection on the card, and restore it after rotation.
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 screen is a tree of composables
Column places the profile sections vertically, while Row places the avatar beside the name. Nesting these containers expresses relationships in the design. When the name becomes longer, the text can wrap inside the remaining row space; fixed screen coordinates would make that harder.
State drives what the user sees
The text field emits a new string through onValueChange. Assigning that string to name changes observable state, and Compose updates the readers of that state. displayName is calculated from name, so the field and the card stay consistent without manually finding or modifying a Text view.
Saved screen state has a specific purpose
rememberSaveable stores the small name string and visibility Boolean in Android saved instance state. Rotate after typing Sam and the recreated screen can restore Sam. This is temporary screen restoration, not a user account or a guarantee that edits remain after deliberately starting a fresh app session.
Parameters make presentation reusable
ProfileDetail receives a label and value and renders the same two-line arrangement for Learning, Goal, and Interests. Each call supplies different data. A style change in ProfileDetail updates all three rows, while the screen continues to own its editable state.
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. Create the first screen
Your task: Open the project in Android Studio, locate MainActivity.kt, and place StudentProfile inside the supplied MiniTheme and MiniScreen. Run the app before adding the profile fields.
Open: app/src/main/java/com/mastercoding/miniprojects/profile/MainActivity.kt
How it works: onCreate is the activity entry point. setContent starts the Compose UI, MiniTheme supplies Material styling, and MiniScreen supplies the title, scrolling, and spacing. This gives the profile a working Android host before you design its individual elements.
import androidx.compose.ui.unit.dp
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MiniTheme {
MiniScreen("Student Profile") { StudentProfile() }
}
}
}
}
@Composable
fun StudentProfile() {
var name by rememberSaveable { mutableStateOf("Alex Morgan") }
var showAbout by rememberSaveable { mutableStateOf(true) }
var favoriteLanguage by rememberSaveable { mutableStateOf("Kotlin") }
val displayName = name.trim().ifBlank { "Your name" }
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Display name") },
supportingText = { Text("Edit your name to update the card immediately.") },
singleLine = true,Stop and check
Step 2. Choose and save the screen state
Your task: Inside StudentProfile, declare a saved name string and a saved showAbout Boolean. Derive displayName by trimming name and supplying Your name when the result is blank.
Open: app/src/main/java/com/mastercoding/miniprojects/profile/MainActivity.kt
How it works: Keep the original field value so typing a space does not unexpectedly move the cursor. The derived display name is only a presentation decision. With name equal to three spaces, the editor still contains those spaces but the card shows Your name; no second mutable name variable is needed.
@Composable
fun StudentProfile() {
var name by rememberSaveable { mutableStateOf("Alex Morgan") }
var showAbout by rememberSaveable { mutableStateOf(true) }
var favoriteLanguage by rememberSaveable { mutableStateOf("Kotlin") }
val displayName = name.trim().ifBlank { "Your name" }
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Display name") },
supportingText = { Text("Edit your name to update the card immediately.") },
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
Text("Favorite programming language", style = MaterialTheme.typography.titleMedium)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
listOf("Kotlin", "Java", "Python").forEach { language ->
FilterChip(
selected = favoriteLanguage == language,
onClick = { favoriteLanguage = language },
label = { Text(language) }
)
}
}
Card(modifier = Modifier.fillMaxWidth()) {Stop and check
Step 3. Connect editing to the card
Your task: Add an OutlinedTextField whose value is name and whose onValueChange assigns the new text to name. Add a label, supporting instruction, single-line behavior, and a full-width modifier.
Open: app/src/main/java/com/mastercoding/miniprojects/profile/MainActivity.kt
How it works: Trace one event: the user types M, onValueChange receives the updated string, name changes, and Text(displayName) receives the new display string during recomposition. The text field does not own a separate copy of the student's name, so the two parts cannot drift apart.
val displayName = name.trim().ifBlank { "Your name" }
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Display name") },
supportingText = { Text("Edit your name to update the card immediately.") },
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
Text("Favorite programming language", style = MaterialTheme.typography.titleMedium)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
listOf("Kotlin", "Java", "Python").forEach { language ->
FilterChip(
selected = favoriteLanguage == language,
onClick = { favoriteLanguage = language },
label = { Text(language) }
)
}
}
Card(modifier = Modifier.fillMaxWidth()) {
Column(
modifier = Modifier.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Row(Stop and check
Step 4. Build the card and bundle an avatar
Your task: Create res/drawable/avatar.xml, draw or customize the vector avatar, and load it with painterResource. Arrange the image and name in a Row inside a padded Card; place the role below the name.
Open: app/src/main/java/com/mastercoding/miniprojects/profile/MainActivity.kt
How it works: R.drawable.avatar refers to a resource packaged with the app, so airplane mode does not affect the picture. The size modifier constrains the image in density-independent pixels. Its contentDescription gives a screen reader meaningful information instead of exposing an unexplained graphic.
) {
Image(
painter = painterResource(R.drawable.avatar),
contentDescription = "Illustrated student avatar",
modifier = Modifier.size(72.dp)
)
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(displayName, style = MaterialTheme.typography.headlineSmall)
Text("Android learner", style = MaterialTheme.typography.bodyLarge)
}
}
ProfileDetail(label = "Learning", value = "Kotlin and Jetpack Compose")
ProfileDetail(label = "Favorite language", value = favoriteLanguage)
ProfileDetail(label = "Goal", value = "Build my first useful Android app")
ProfileDetail(label = "Interests", value = "Design, games, and problem solving")
if (showAbout) {
Text("I learn by building small projects and explaining how each screen works.")
}
}
}
Button(onClick = { showAbout = !showAbout }) {
Text(if (showAbout) "Hide about me" else "Show about me")
}
Text("Profile edits survive rotation. This introductory app does not save a permanent account.")
}Stop and check
Step 5. Extract the repeated information layout
Your task: Write ProfileDetail(label, value) as a small composable. Use it three times for the learning topic, goal, and interests, choosing label and body typography from MaterialTheme.
Open: app/src/main/java/com/mastercoding/miniprojects/profile/MainActivity.kt
How it works: The first call provides Learning and Kotlin and Jetpack Compose; the function renders those values without knowing why they were chosen. Passing data as parameters keeps the component reusable and makes it possible to change every detail row's spacing in one place.
@Composable
private fun ProfileDetail(label: String, value: String) {
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text(label, style = MaterialTheme.typography.labelLarge)
Text(value, style = MaterialTheme.typography.bodyLarge)
}
}Stop and check
Step 6. Add a favorite-language selector
Your task: Declare a saved favoriteLanguage string. Add Kotlin, Java, and Python FilterChips, compare each label with favoriteLanguage for its selected state, and assign the clicked language in onClick. Render the same value with ProfileDetail.
Open: app/src/main/java/com/mastercoding/miniprojects/profile/MainActivity.kt
How it works: Tapping Java changes one source value from Kotlin to Java. The chips read that value to move the visible selection, and the card reads it to display Favorite language: Java. Keeping only one selected string prevents multiple independent Booleans from accidentally selecting several languages.
Text("Favorite programming language", style = MaterialTheme.typography.titleMedium)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
listOf("Kotlin", "Java", "Python").forEach { language ->
FilterChip(
selected = favoriteLanguage == language,
onClick = { favoriteLanguage = language },
label = { Text(language) }
)
}
}
Card(modifier = Modifier.fillMaxWidth()) {
Column(
modifier = Modifier.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Image(
painter = painterResource(R.drawable.avatar),
contentDescription = "Illustrated student avatar",
modifier = Modifier.size(72.dp)
)
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(displayName, style = MaterialTheme.typography.headlineSmall)Stop and check
Step 7. Make the biography interactive and verify restoration
Your task: Wrap the biography in if (showAbout), then toggle showAbout from the button and derive the button text from the same Boolean. Hide the biography, edit the name, and rotate the device.
Open: app/src/main/java/com/mastercoding/miniprojects/profile/MainActivity.kt
How it works: Tapping Hide about me changes true to false. Compose omits the biography and the button now says Show about me. After recreation, rememberSaveable restores both values, so the same name and hidden biography remain. The button's next action is always clear from its label.
}
}
Button(onClick = { showAbout = !showAbout }) {
Text(if (showAbout) "Hide about me" else "Show about me")
}
Text("Profile edits survive rotation. This introductory app does not save a permanent account.")
}
@Composable
private fun ProfileDetail(label: String, value: String) {
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text(label, style = MaterialTheme.typography.labelLarge)
Text(value, style = MaterialTheme.typography.bodyLarge)
}
}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.
18 complete text files. Copy the selected file with the code panel button, or download the full project above.
package com.mastercoding.miniprojects.profile
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.FilterChip
import androidx.compose.material3.MaterialTheme
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MiniTheme {
MiniScreen("Student Profile") { StudentProfile() }
}
}
}
}
@Composable
fun StudentProfile() {
var name by rememberSaveable { mutableStateOf("Alex Morgan") }
var showAbout by rememberSaveable { mutableStateOf(true) }
var favoriteLanguage by rememberSaveable { mutableStateOf("Kotlin") }
val displayName = name.trim().ifBlank { "Your name" }
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Display name") },
supportingText = { Text("Edit your name to update the card immediately.") },
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
Text("Favorite programming language", style = MaterialTheme.typography.titleMedium)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
listOf("Kotlin", "Java", "Python").forEach { language ->
FilterChip(
selected = favoriteLanguage == language,
onClick = { favoriteLanguage = language },
label = { Text(language) }
)
}
}
Card(modifier = Modifier.fillMaxWidth()) {
Column(
modifier = Modifier.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Image(
painter = painterResource(R.drawable.avatar),
contentDescription = "Illustrated student avatar",
modifier = Modifier.size(72.dp)
)
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(displayName, style = MaterialTheme.typography.headlineSmall)
Text("Android learner", style = MaterialTheme.typography.bodyLarge)
}
}
ProfileDetail(label = "Learning", value = "Kotlin and Jetpack Compose")
ProfileDetail(label = "Favorite language", value = favoriteLanguage)
ProfileDetail(label = "Goal", value = "Build my first useful Android app")
ProfileDetail(label = "Interests", value = "Design, games, and problem solving")
if (showAbout) {
Text("I learn by building small projects and explaining how each screen works.")
}
}
}
Button(onClick = { showAbout = !showAbout }) {
Text(if (showAbout) "Hide about me" else "Show about me")
}
Text("Profile edits survive rotation. This introductory app does not save a permanent account.")
}
@Composable
private fun ProfileDetail(label: String, value: String) {
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text(label, style = MaterialTheme.typography.labelLarge)
Text(value, style = MaterialTheme.typography.bodyLarge)
}
}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 |
|---|---|
| Launch the app with airplane mode enabled. | The avatar and all profile content load without requesting internet access. |
| Replace Alex Morgan with your own name. | The profile heading changes as each character is typed. |
| Clear the name or enter only spaces. | The card displays Your name and the app remains usable. |
| Hide the biography and then show it again. | The paragraph appears and disappears; the button always describes the next action. |
| Enter Sam, hide the biography, and rotate the device. | Sam remains in the field and on the card, and the biography stays hidden. |
| Try a long name, large font settings, and TalkBack. | Text remains readable, content can scroll, and the avatar and controls have useful spoken descriptions. |
| Choose Java, edit the name, hide the biography, and rotate. | Java remains selected and displayed on the card together with the restored name and hidden biography. |
Your completion checklist
Check an item only after demonstrating it. Progress is saved in this browser.
Troubleshooting: understand the cause
The name resets whenever the UI updates.
Cause: A regular local variable is recreated as the composable executes.
Repair: Store editable values in observable state and use rememberSaveable for these small restorable values.
The field changes but the card still shows Alex Morgan.
Cause: The card contains a hard-coded string or a separate mutable copy of the name.
Repair: Read displayName derived from the same name state used by the text field.
The image resource cannot be found.
Cause: The vector file has a different resource name or was placed outside res/drawable.
Repair: Use a lowercase avatar.xml resource in res/drawable and reference R.drawable.avatar.
The biography button label describes the wrong action.
Cause: Its text is fixed instead of derived from showAbout.
Repair: Calculate both paragraph visibility and button text from the same Boolean.
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.
- Which values must be mutable state, and why does displayName not need its own mutable state?
- What happens between a button tap and the biography disappearing? Describe the event, state update, and UI result.
- How does restoring a name after rotation differ from permanently saving a profile?
- Why is one favoriteLanguage string simpler than one selected Boolean per language?
| 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 an editable goal using a second saved text field.
- Add a dark-theme preview and adjust any hard-coded colors in the avatar.
- Create a second profile by supplying profile data through parameters.
Choose one extension, write its acceptance check first, then implement it without breaking the original scenarios.