Techbase Academy
Android · Lesson 22

Material Theming and Simple Animation: Superheroes and 30 Days

10 phases30 Days of CodingAndroid Studio
Phase 1 of 10
Lesson Introduction

You can now build lists, grids and interactive screens. But if you hand your phone to a friend, the first thing they will judge is not your code. It is how the app looks and feels. Today you learn how to make an app look like it belongs on a modern phone, and how to make it feel alive with small, tasteful animations.

Think of a restaurant. The food (your features) matters most, but the plates, the lighting, the uniforms and the way the waiter brings dishes to the table all shape the experience. Material Design 3 is Google's complete design kit for apps: colours, type, shapes and motion that work together. Theming means defining those choices once so that every screen of your app uses them automatically.

By the end of this lesson you will be able to:

  • Explain Material Design 3 and the idea of a theme.
  • Define a ColorScheme with lightColorScheme and darkColorScheme and understand colour roles such as primary and onPrimary.
  • Turn on dynamic colour safely on Android 12 and above.
  • Support dark theme with isSystemInDarkTheme().
  • Define custom Typography and Shapes and use them through MaterialTheme.
  • Apply edge-to-edge basics with Scaffold insets.
  • Animate size, colour and visibility with animateContentSize, animateDpAsState, animateColorAsState and AnimatedVisibility.
  • Build an expand and collapse card.
  • Complete the Superheroes practice app and the 30 Days project.

How this fits the bigger picture: This lesson follows Unit 3, Pathway 3 of Google's Android Basics with Compose course. It covers Intro to Material Design with Compose, Material Theming with Jetpack Compose, Simple animation with Jetpack Compose, Practice: Build Superheroes app and Project: Create a 30 Days App. Our apps are original, so we use fictional heroes and our own 30-day plan. Lesson 40 revisits theming in much more depth and lesson 41 does the same for animation.

✏️ Your Task
Practise what you just learned about Lesson Introduction. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 2 of 10
Prerequisite Concepts

1. Composables, modifiers and layouts (lessons 15 and 16)

You will use Row, Column, Card, Text, Image and Modifier chains.

2. State (lessons 17 and 19)

An animation needs a state to react to. A card has an expanded state, a Boolean. When it flips, the animation runs from the old look to the new one. If remember and mutableStateOf feel unfamiliar, revisit lesson 17.

3. Lazy lists and resources (lesson 21)

The Superheroes app is a LazyColumn of cards driven by a data class with @StringRes and @DrawableRes ids, exactly as in the Affirmations app.

4. Lambdas and function parameters with defaults (lessons 08 and 12)

Our theme function takes defaults like darkTheme: Boolean = isSystemInDarkTheme() and a content lambda.

✏️ Your Task
Practise what you just learned about Prerequisite Concepts. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 3 of 10
Conceptual Understanding: Material Design 3 and Themes

What is Material Design?

Material Design is Google's design system: a shared set of rules, components and tools for building consistent, good-looking apps. Material 3 (often written M3 or Material You) is the current generation, and androidx.compose.material3 is its Compose library. Every Button, Card and TopAppBar you have used already comes from it.

A design system saves you from re-deciding things. You do not choose a new shade of blue for every button. You say "use the primary colour", and the system gives the same one everywhere.

The three pillars of a theme

text
MaterialTheme(
    colorScheme = ...,   // which colours, and what each is for
    typography  = ...,   // which text styles (sizes, weights, fonts)
    shapes      = ...    // how rounded the corners are
) {
    // your whole app lives here
}

Anything inside reads the values through MaterialTheme.colorScheme, MaterialTheme.typography and MaterialTheme.shapes. Change the theme in one place and the entire app changes.

Colour roles

A colour scheme is not a list of colours. It is a list of jobs, and each job has a colour. This is why dark mode is easy: you simply provide a second set of colours for the same jobs.

Role Meaning Typical use
primary Your brand's main colour Filled buttons, active controls
onPrimary Text and icons drawn on primary Label inside a filled button
primaryContainer A softer version of primary Highlighted cards, chips
onPrimaryContainer Content drawn on the container Text on a highlighted card
secondary, tertiary Supporting accents Less important actions, contrast
background and surface The page and card colours Screen and card backgrounds
onBackground, onSurface Content drawn on those Ordinary text
error Warnings Error messages

The golden rule: always pair a colour with its "on" colour. Text on a primary background must use onPrimary. The pair has been chosen to have enough contrast, so people with weak eyesight and people in bright sunlight can still read it (lessons 46 and 47 measure contrast).

Where do the colours come from?

Designers create schemes from a single seed colour using the Material Theme Builder tool, which can export Kotlin code. You can also write them by hand, as we will. Choose a primary that suits your app, then derive lighter and darker tones.

Dynamic colour

On Android 12 (API 31) and above, the phone can build a colour scheme from the user's wallpaper. Your app then matches the user's personalised look. You ask for it with dynamicLightColorScheme(context) or dynamicDarkColorScheme(context). On older phones those functions do not exist, so you must check the version and fall back to your own colours.

Dark theme

Many people prefer dark screens, especially at night, and on OLED phones dark screens can save some battery. Android tells you whether the user chose dark through isSystemInDarkTheme(). Your theme then picks darkColorScheme or lightColorScheme. A well-themed app needs no per-screen dark mode code at all.

Typography and shapes

M3 gives you a type scale, named styles that you use rather than hard-coding sizes: displayLarge, headlineSmall, titleMedium, bodyLarge, labelMedium and so on, fifteen in all. Use a style by name: Text("Hello", style = MaterialTheme.typography.headlineSmall). Shapes work likewise: small, medium and large (and two more, extraSmall and extraLarge) describe corner radii, and components pick the right one for their size.

✏️ Your Task
Practise what you just learned about Conceptual Understanding: Material Design 3 and Themes. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 4 of 10
Step-by-Step Build: Creating a Theme

We will theme the Superheroes app. Create a project Superheroes with package com.techbase.superheroes. If the wizard created a ui/theme folder with Color.kt, Theme.kt and Type.kt, we will replace their contents. If your Android Studio version names things differently, create the files yourself in a package com.techbase.superheroes.ui.theme. Any theme named SuperheroesTheme, or whatever the wizard generated from your project name, will do; just be consistent.

Step 1: Colours

Color.kt
package com.techbase.superheroes.ui.theme
import androidx.compose.ui.graphics.Color
// Light theme
val LightPrimary = Color(0xFF006B5A)
val LightOnPrimary = Color(0xFFFFFFFF)
val LightPrimaryContainer = Color(0xFF9CF2DC)
val LightOnPrimaryContainer = Color(0xFF00201A)
val LightSecondary = Color(0xFF8A5100)
val LightOnSecondary = Color(0xFFFFFFFF)
val LightSecondaryContainer = Color(0xFFFFDCBA)
val LightBackground = Color(0xFFFBFDF9)
val LightOnBackground = Color(0xFF191C1B)
val LightSurface = Color(0xFFFBFDF9)
val LightOnSurface = Color(0xFF191C1B)
// Dark theme
val DarkPrimary = Color(0xFF80D5C0)
val DarkOnPrimary = Color(0xFF00382E)
val DarkPrimaryContainer = Color(0xFF005143)
val DarkOnPrimaryContainer = Color(0xFF9CF2DC)
val DarkSecondary = Color(0xFFFFB86C)
val DarkOnSecondary = Color(0xFF4A2800)
val DarkSecondaryContainer = Color(0xFF693C00)
val DarkBackground = Color(0xFF191C1B)
val DarkOnBackground = Color(0xFFE0E3E0)
val DarkSurface = Color(0xFF191C1B)
val DarkOnSurface = Color(0xFFE0E3E0)

Color(0xFF006B5A) reads as: 0x means hexadecimal, FF is full opacity, and 006B5A are the red, green and blue amounts (so here, a deep teal). Notice the pattern: in light mode the primary is dark with white text on it; in dark mode the primary becomes light with dark text. That inversion is what keeps contrast strong in both.

These values are an original palette made for this lesson. For your own app, try the Material Theme Builder at m3.material.io to generate a palette from your brand colour.

Step 2: Typography

Type.kt
package com.techbase.superheroes.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
val AppTypography = Typography(
 displayLarge = TextStyle(
 fontFamily = FontFamily.Default,
 fontWeight = FontWeight.Bold,
 fontSize = 36.sp,
 lineHeight = 44.sp
 ),
 displayMedium = TextStyle(
 fontFamily = FontFamily.Default,
 fontWeight = FontWeight.Bold,
 fontSize = 24.sp,
 lineHeight = 30.sp
 ),
 labelSmall = TextStyle(
 fontFamily = FontFamily.Default,
 fontWeight = FontWeight.Medium,
 fontSize = 14.sp,
 lineHeight = 18.sp
 ),
 bodyLarge = TextStyle(
 fontFamily = FontFamily.Default,
 fontWeight = FontWeight.Normal,
 fontSize = 16.sp,
 lineHeight = 24.sp
 )
)

You only list the styles you want to change; the remaining styles in the type scale keep Material's defaults. Sizes are in sp (scale-independent pixels), which grow when the user raises the phone's font size. Always use sp for text and dp for everything else (lesson 15).

FontFamily.Default is the phone's normal font. To use a custom font, you place a font file in res/font, then write FontFamily(Font(R.font.my_font)). Fonts can also be downloaded on demand. Lesson 40 covers custom, downloadable and variable fonts. Remember that fonts have licences and add to your app size.

Step 3: Shapes

Shape.kt
package com.techbase.superheroes.ui.theme
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Shapes
import androidx.compose.ui.unit.dp
val AppShapes = Shapes(
 small = RoundedCornerShape(50),
 medium = RoundedCornerShape(bottomStart = 16.dp, topEnd = 16.dp)
)

RoundedCornerShape(50) means 50 percent: fully round ends, which gives a square image a perfect circle, or a rectangle a "pill" look. RoundedCornerShape(bottomStart = 16.dp, topEnd = 16.dp) rounds just two corners, which gives cards a distinctive leaf-like shape. Shapes you do not specify keep the defaults.

Step 4: The theme function

Theme.kt
package com.techbase.superheroes.ui.theme
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val LightColors = lightColorScheme(
 primary = LightPrimary,
 onPrimary = LightOnPrimary,
 primaryContainer = LightPrimaryContainer,
 onPrimaryContainer = LightOnPrimaryContainer,
 secondary = LightSecondary,
 onSecondary = LightOnSecondary,
 secondaryContainer = LightSecondaryContainer,
 background = LightBackground,
 onBackground = LightOnBackground,
 surface = LightSurface,
 onSurface = LightOnSurface
)
private val DarkColors = darkColorScheme(
 primary = DarkPrimary,
 onPrimary = DarkOnPrimary,
 primaryContainer = DarkPrimaryContainer,
 onPrimaryContainer = DarkOnPrimaryContainer,
 secondary = DarkSecondary,
 onSecondary = DarkOnSecondary,
 secondaryContainer = DarkSecondaryContainer,
 background = DarkBackground,
 onBackground = DarkOnBackground,
 surface = DarkSurface,
 onSurface = DarkOnSurface
)
@Composable
fun SuperheroesTheme(
 darkTheme: Boolean = isSystemInDarkTheme(),
 dynamicColor: Boolean = false,
 content: @Composable () -> Unit
) {
 val colorScheme = when {
 dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
 val context = LocalContext.current
 if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
 }
 darkTheme -> DarkColors
 else -> LightColors
 }
 MaterialTheme(
 colorScheme = colorScheme,
 typography = AppTypography,
 shapes = AppShapes,
 content = content
 )
}

How it works:

  • lightColorScheme(...) and darkColorScheme(...) build a scheme; roles you leave out use sensible defaults.
  • private val keeps the two schemes inside this file. Only the theme function is public.
  • The when has three branches, tried in order: use dynamic colour if wanted and the phone is Android 12 or newer (Build.VERSION_CODES.S is API 31); otherwise dark colours if the user is in dark mode; otherwise light colours.
  • dynamicColor defaults to false here so that you can see your own palette while learning. The Android Studio template turns it on by default. Try dynamicColor = true on an Android 12+ emulator and change the wallpaper to see the colours follow.
  • content: @Composable () -> Unit is the screen the theme wraps. MaterialTheme(..., content = content) passes it along.
✏️ Your Task
Practise what you just learned about Step-by-Step Build: Creating a Theme. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 5 of 10
Step-by-Step Build: The Superheroes Practice App

The official Practice: Build Superheroes app asks for a scrolling list of heroes with a name, a description and a picture, themed with Material 3. Here is our version, with fictional heroes of our own invention.

The look we want

text
+------------------------------------------+
|        Superheroes   (top app bar)       |
+------------------------------------------+
| +--------------------------------------+ |
| | Captain Danfo                  [ o ] | |
| | Drives through any traffic jam       | |
| +--------------------------------------+ |
| +--------------------------------------+ |
| | Lady Jollof                    [ o ] | |
| | Feeds a whole street from one pot    | |
| +--------------------------------------+ |
+------------------------------------------+

Each card has text on the left and a round picture, 72 dp wide, on the right.

Resources

Add these strings:

strings.xml
<resources>
 <string name="app_name">Superheroes</string>
 <string name="hero1_name">Captain Danfo</string>
 <string name="hero1_description">Drives through any traffic jam and never loses the way.</string>
 <string name="hero2_name">Lady Jollof</string>
 <string name="hero2_description">Feeds a whole street from one enormous pot.</string>
 <string name="hero3_name">Professor Generator</string>
 <string name="hero3_description">Keeps the lights on when everyone else is in darkness.</string>
 <string name="hero4_name">The Mighty Umbrella</string>
 <string name="hero4_description">Stops the rain, shades the sun and still fits in a bag.</string>
 <string name="hero5_name">Agent Network</string>
 <string name="hero5_description">Finds one bar of signal anywhere in the city.</string>
</resources>

For the pictures, create three vector drawables hero1.xml, hero2.xml and hero3.xml with the same simple circle design you used in lesson 21 (any square viewport, a coloured background and a lighter circle). Replace them with real artwork for a finished app. Heroes 4 and 5 reuse hero1 and hero2.

The data

Hero.kt
package com.techbase.superheroes.model
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import com.techbase.superheroes.R
data class Hero(
 @StringRes val nameRes: Int,
 @StringRes val descriptionRes: Int,
 @DrawableRes val imageRes: Int
)
val heroes = listOf(
 Hero(R.string.hero1_name, R.string.hero1_description, R.drawable.hero1),
 Hero(R.string.hero2_name, R.string.hero2_description, R.drawable.hero2),
 Hero(R.string.hero3_name, R.string.hero3_description, R.drawable.hero3),
 Hero(R.string.hero4_name, R.string.hero4_description, R.drawable.hero1),
 Hero(R.string.hero5_name, R.string.hero5_description, R.drawable.hero2)
)

The screen

MainActivity.kt
package com.techbase.superheroes
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Card
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.techbase.superheroes.model.Hero
import com.techbase.superheroes.model.heroes
import com.techbase.superheroes.ui.theme.SuperheroesTheme
class MainActivity : ComponentActivity() {
 override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
 enableEdgeToEdge()
 setContent {
 SuperheroesTheme {
 SuperheroesApp()
 }
 }
 }
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SuperheroesApp() {
 Scaffold(
 modifier = Modifier.fillMaxSize(),
 topBar = {
 CenterAlignedTopAppBar(
 title = {
 Text(
 text = stringResource(R.string.app_name),
 style = MaterialTheme.typography.displayLarge
 )
 }
 )
 }
 ) { innerPadding ->
 HeroList(heroes = heroes, contentPadding = innerPadding)
 }
}
@Composable
fun HeroList(
 heroes: List<Hero>,
 contentPadding: PaddingValues,
 modifier: Modifier = Modifier
) {
 LazyColumn(
 modifier = modifier,
 contentPadding = PaddingValues(
 start = 16.dp,
 end = 16.dp,
 top = contentPadding.calculateTopPadding() + 8.dp,
 bottom = contentPadding.calculateBottomPadding() + 8.dp
 ),
 verticalArrangement = Arrangement.spacedBy(8.dp)
 ) {
 items(heroes, key = { it.nameRes }) { hero ->
 HeroItem(hero)
 }
 }
}
@Composable
fun HeroItem(hero: Hero, modifier: Modifier = Modifier) {
 Card(modifier = modifier.fillMaxWidth()) {
 Row(
 modifier = Modifier.padding(16.dp),
 verticalAlignment = Alignment.CenterVertically
 ) {
 Column(modifier = Modifier.weight(1f)) {
 Text(
 text = stringResource(hero.nameRes),
 style = MaterialTheme.typography.displayMedium
 )
 Text(
 text = stringResource(hero.descriptionRes),
 style = MaterialTheme.typography.bodyLarge
 )
 }
 Spacer(modifier = Modifier.width(16.dp))
 Image(
 painter = painterResource(hero.imageRes),
 contentDescription = null,
 modifier = Modifier
 .size(72.dp)
 .clip(MaterialTheme.shapes.small),
 contentScale = ContentScale.Crop
 )
 }
 }
}
@Preview(showBackground = true)
@Composable
fun SuperheroesLightPreview() {
 SuperheroesTheme(darkTheme = false) {
 SuperheroesApp()
 }
}
@Preview(showBackground = true, uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES)
@Composable
fun SuperheroesDarkPreview() {
 SuperheroesTheme(darkTheme = true) {
 SuperheroesApp()
 }
}

What the screen shows: a centred title bar reading Superheroes, then five cards. Each card has a bold name, a description, and on the right a circular picture (because shapes.small is a 50-percent rounded corner). Switch the emulator to dark mode (Settings > Display > Dark theme) and the colours change to the dark scheme, with no extra code in the screen.

Points to understand:

  • enableEdgeToEdge() (from androidx.activity) lets the app draw behind the status and navigation bars, as modern apps do. It also makes the system bar icons readable against your colours. New projects from the wizard usually include this call already; if yours does, you do not need to add it twice. It is available in recent versions of the activity library; if it is red, update the dependency.
  • Insets: because content may now sit under the system bars, Scaffold passes innerPadding describing how much space the top bar and system bars take. We add it to the list's contentPadding, so the first card starts below the top bar, the last card clears the navigation bar, and the list still scrolls behind them. That is why we use contentPadding and not Modifier.padding.
  • @OptIn(ExperimentalMaterial3Api::class): some Material 3 components are labelled experimental, meaning their API may change. The OptIn annotation tells the compiler you accept that. Whether CenterAlignedTopAppBar still needs it depends on your library version; leaving it in is harmless.
  • Modifier.weight(1f) inside a Row gives the text column all the space the picture does not use, so a long description wraps instead of pushing the picture off screen.
  • clip(MaterialTheme.shapes.small) cuts the image to the theme's small shape. Change AppShapes and every clipped image changes. That is the point of a theme.
  • Two previews, one light and one dark, let you check both modes without running the app. uiMode = UI_MODE_NIGHT_YES asks the preview for night mode.

Check the contrast

Try this once: in Color.kt change LightOnPrimary to a dark colour and look at a filled button or the title bar. Text on primary becomes hard to read. That is why role pairs exist.

✏️ Your Task

Change the primary colour of your light theme to something you like (for example 0xFF1E88E5). Then change the heroes' picture shape by editing only AppShapes.small, for instance to RoundedCornerShape(8.dp). Notice that no screen code needed to change.

Phase 6 of 10
Step-by-Step Build: Simple Animation

Motion makes an interface understandable. When a card grows smoothly, you see what changed. When it just jumps, your eyes must work out what happened. Compose makes simple animations a one-line change.

The idea: animate from state

You never say "move this by 5 pixels per frame". Instead you describe the target for each state, and Compose animates between them whenever the state changes:

text
 expanded = false  ------ state flips ------>  expanded = true
 padding 8 dp                                   padding 24 dp
 colour  primaryContainer                       colour secondaryContainer
 Compose draws every frame in between, smoothly.

Tool 1: animateContentSize

The simplest tool. Add it to a modifier, and any change to the size of that composable's content is animated.

kotlin
Column(
 modifier = Modifier
 .animateContentSize()
 .padding(16.dp)
) { /* content that can grow or shrink */ }

Import androidx.compose.animation.animateContentSize. Place it before the padding in the chain so the padding is included in the animated size.

Tool 2: animateDpAsState

Animates a Dp value, such as padding, size or elevation.

kotlin
val extraPadding by animateDpAsState(
 targetValue = if (expanded) 24.dp else 0.dp,
 label = "extraPadding"
)

by works here for the same reason it did in lesson 17. animateDpAsState returns a state: its value moves step by step to the target. Imports: androidx.compose.animation.core.animateDpAsState and androidx.compose.runtime.getValue. The label is a name that animation tools use when inspecting your app (lesson 44).

Tool 3: animateColorAsState

Animates between two colours:

kotlin
val containerColor by animateColorAsState(
 targetValue = if (expanded) {
 MaterialTheme.colorScheme.secondaryContainer
 } else {
 MaterialTheme.colorScheme.primaryContainer
 },
 label = "containerColor"
)

Because the colours come from the theme, the animation works in dark mode too.

Tool 4: AnimatedVisibility

Fades and slides content in and out when a condition changes, instead of popping it in:

kotlin
AnimatedVisibility(visible = expanded) {
 Text("More details appear here")
}

Import androidx.compose.animation.AnimatedVisibility.

Controlling the feel: spring and tween

animateContentSize and the animate*AsState functions take an animationSpec. Two common ones:

Spec Behaviour Example
spring(dampingRatio, stiffness) Like a spring: it may overshoot and bounce, and speed depends on stiffness. spring(dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessLow)
tween(durationMillis, easing) A fixed duration with a smooth curve. tween(durationMillis = 300)

Keep animations short and subtle. A card that bounces for two seconds becomes annoying quickly, and some users have turned animations off in their phone's settings for comfort. Lesson 41 covers the whole animation toolbox.

✏️ Your Task
Practise what you just learned about Step-by-Step Build: Simple Animation. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 7 of 10
Step-by-Step Build: The Expandable Hero Card

Now we combine everything: a card that expands to show more, with animated colour and spacing. We upgrade the Hero data to include a longer story, and add a state to each card.

Add a field @StringRes val storyRes: Int to Hero, give each hero a long heroN_story string in strings.xml, and add the extra argument in each Hero(...) line, for instance Hero(R.string.hero1_name, R.string.hero1_description, R.drawable.hero1, R.string.hero1_story). (The solution at the end of the lesson avoids the extra resources for brevity.)

Then replace HeroItem with:

kotlin
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.TextButton
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@Composable
fun HeroItem(hero: Hero, modifier: Modifier = Modifier) {
 var expanded by remember { mutableStateOf(false) }
 val containerColor by animateColorAsState(
 targetValue = if (expanded) {
 MaterialTheme.colorScheme.secondaryContainer
 } else {
 MaterialTheme.colorScheme.primaryContainer
 },
 label = "containerColor"
 )
 Card(
 modifier = modifier.fillMaxWidth(),
 colors = CardDefaults.cardColors(containerColor = containerColor)
 ) {
 Column(
 modifier = Modifier
 .animateContentSize(
 animationSpec = spring(
 dampingRatio = Spring.DampingRatioLowBouncy,
 stiffness = Spring.StiffnessMedium
 )
 )
 .padding(16.dp)
 ) {
 Row(verticalAlignment = Alignment.CenterVertically) {
 Column(modifier = Modifier.weight(1f)) {
 Text(
 text = stringResource(hero.nameRes),
 style = MaterialTheme.typography.displayMedium
 )
 Text(
 text = stringResource(hero.descriptionRes),
 style = MaterialTheme.typography.bodyLarge
 )
 }
 Spacer(modifier = Modifier.width(16.dp))
 Image(
 painter = painterResource(hero.imageRes),
 contentDescription = null,
 modifier = Modifier
 .size(72.dp)
 .clip(MaterialTheme.shapes.small),
 contentScale = ContentScale.Crop
 )
 }
 AnimatedVisibility(visible = expanded) {
 Text(
 text = stringResource(hero.storyRes),
 style = MaterialTheme.typography.bodyLarge,
 modifier = Modifier.padding(top = 12.dp)
 )
 }
 TextButton(onClick = { expanded = !expanded }) {
 Text(if (expanded) "Show less" else "Show more")
 }
 }
 }
}

What the screen shows: each card has a Show more text button. Tapping it flips expanded. The card's background shifts smoothly from the primary container colour to the secondary container colour, the story text fades and slides in, and the card grows to fit it with a slightly springy motion. Tapping Show less plays the same animation in reverse.

Details worth knowing:

  • expanded is plain state per card. Because HeroItem is called once per hero, each hero has its own expanded. (If you want the open/closed state to survive rotation, use rememberSaveable, lesson 19. And because lazy items scroll out of composition, rememberSaveable keeps the state when the item returns to the screen; plain remember does not.)
  • !expanded flips a Boolean: true becomes false and false becomes true.
  • The button text is hard-coded here to keep the example short. In your finished app, put Show more and Show less in strings.xml.
  • If you want an accessibility improvement, replace the text button with an icon button that carries a contentDescription of "Expand" or "Collapse" (lesson 47).
✏️ Your Task
Practise what you just learned about Step-by-Step Build: The Expandable Hero Card. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 8 of 10
Guided Practice Exercises
🎯 Your Challenge

Exercise 1: Pulse on tap

Objective: Use animateDpAsState.

Make a square Box (80 dp) with a coloured background that is clickable. Each tap toggles between 80 dp and 160 dp, animated with animateDpAsState and a spring. Use Modifier.size(size) with the animated value and Modifier.clickable { ... }.

kotlin
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun PulseBox(modifier: Modifier = Modifier) {
 var big by remember { mutableStateOf(false) }
 val boxSize by animateDpAsState(
 targetValue = if (big) 160.dp else 80.dp,
 animationSpec = spring(
 dampingRatio = Spring.DampingRatioMediumBouncy,
 stiffness = Spring.StiffnessLow
 ),
 label = "boxSize"
 )
 Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
 Box(
 modifier = Modifier
 .size(boxSize)
 .background(MaterialTheme.colorScheme.primary)
 .clickable { big = !big }
 )
 }
}

The target depends on big. Each tap changes big, and animateDpAsState moves boxSize towards the new target through a bouncy spring, so the square slightly overshoots before settling. The outer Box centres the square on the screen.

🎯 Your Challenge

Exercise 2: Dark-mode-ready theme for a church noticeboard

Objective: Build a complete theme from your own palette.

Create NoticeTheme with a light and a dark ColorScheme of your own choice (at least primary, onPrimary, background, onBackground, surface and onSurface), a typography with a bold headlineMedium, and a Shapes with a medium corner of 20 dp. Write one screen with a title, a card and a button that uses these styles by role, and two @Preview functions (light and dark).

kotlin
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Shapes
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.Typography
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
private val NoticeLight = lightColorScheme(
 primary = Color(0xFF5E35B1),
 onPrimary = Color.White,
 background = Color(0xFFFFFBFF),
 onBackground = Color(0xFF1C1B1F),
 surface = Color(0xFFFFFBFF),
 onSurface = Color(0xFF1C1B1F)
)
private val NoticeDark = darkColorScheme(
 primary = Color(0xFFD0BCFF),
 onPrimary = Color(0xFF381E72),
 background = Color(0xFF1C1B1F),
 onBackground = Color(0xFFE6E1E5),
 surface = Color(0xFF1C1B1F),
 onSurface = Color(0xFFE6E1E5)
)
private val NoticeTypography = Typography(
 headlineMedium = TextStyle(fontWeight = FontWeight.Bold, fontSize = 28.sp)
)
private val NoticeShapes = Shapes(medium = RoundedCornerShape(20.dp))
@Composable
fun NoticeTheme(
 darkTheme: Boolean = isSystemInDarkTheme(),
 content: @Composable () -> Unit
) {
 MaterialTheme(
 colorScheme = if (darkTheme) NoticeDark else NoticeLight,
 typography = NoticeTypography,
 shapes = NoticeShapes,
 content = content
 )
}
@Composable
fun NoticeScreen() {
 Surface(modifier = Modifier.fillMaxSize()) {
 Column(modifier = Modifier.padding(24.dp)) {
 Text("This Sunday", style = MaterialTheme.typography.headlineMedium)
 Card(modifier = Modifier.padding(vertical = 16.dp)) {
 Text(
 "Choir practice holds after the service.",
 modifier = Modifier.padding(16.dp)
 )
 }
 Button(onClick = { }) { Text("I will attend") }
 }
 }
}
@Preview(showBackground = true)
@Composable
fun NoticeLightPreview() { NoticeTheme(darkTheme = false) { NoticeScreen() } }
@Preview(
 showBackground = true,
 uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES
)
@Composable
fun NoticeDarkPreview() { NoticeTheme(darkTheme = true) { NoticeScreen() } }

The screen code contains no colours at all: it uses MaterialTheme.typography.headlineMedium and the default component colours, which read from the theme. That is why the dark preview works without a single if in NoticeScreen. Because Surface uses the theme's surface and onSurface, the Text colour is chosen correctly in both modes.

🎯 Your Challenge

Exercise 3: Animated FAQ list

Objective: Combine LazyColumn, state and AnimatedVisibility.

Build a screen with five questions and answers (a data class Faq(question: String, answer: String)). Each row shows the question; tapping the row reveals or hides the answer with AnimatedVisibility. Only one answer should be open at a time (hold the open index in the parent).

kotlin
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
data class Faq(val question: String, val answer: String)
@Composable
fun FaqList(faqs: List<Faq>, modifier: Modifier = Modifier) {
 var openIndex by remember { mutableStateOf(-1) }
 LazyColumn(modifier = modifier) {
 itemsIndexed(faqs) { index, faq ->
 val isOpen = index == openIndex
 Column(
 modifier = Modifier
 .fillMaxWidth()
 .clickable { openIndex = if (isOpen) -1 else index }
 .padding(16.dp)
 ) {
 Text(faq.question, style = MaterialTheme.typography.titleMedium)
 AnimatedVisibility(visible = isOpen) {
 Text(faq.answer, modifier = Modifier.padding(top = 8.dp))
 }
 }
 HorizontalDivider()
 }
 }
}

The state is hoisted to FaqList: one openIndex for all rows, -1 meaning "none open". Tapping an open row closes it; tapping another row moves the open index there, which closes the old one automatically, because its isOpen becomes false and AnimatedVisibility plays the exit animation.

✏️ Your Task
Practise what you just learned about Guided Practice Exercises. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 9 of 10
Common Beginner Mistakes (and How to Fix Them)

Mistake #1: Hard-coding colours in screens. The wrong code: Text("Hi", color = Color(0xFF006B5A)) in many places. The problem: dark mode and rebranding require editing every file, and the colour may be unreadable in dark mode. The fix: use MaterialTheme.colorScheme.primary and its siblings, and define colours only in the theme.

Mistake #2: Mismatched role pairs. The wrong code: text coloured onSurface on a primary background. The problem: low contrast, so some users cannot read it. The fix: pair primary with onPrimary, primaryContainer with onPrimaryContainer, and so on. Card, Button and Surface already choose the right content colour when you use their defaults.

Mistake #3: Calling dynamic colour on old phones. The wrong code: dynamicLightColorScheme(context) with no version check. The problem: the app crashes on Android 11 or older. The fix: guard it with Build.VERSION.SDK_INT >= Build.VERSION_CODES.S as in our theme.

Mistake #4: Text sizes in dp. The wrong code: fontSize = 16.dp. The problem: it will not compile (a TextUnit is expected), and where it is forced, text will ignore the user's font-size setting. The fix: text always uses sp: fontSize = 16.sp.

Mistake #5: Using Modifier.padding(innerPadding) on a scrolling list under a top bar. The problem: it works, but content cannot scroll behind the bars, so it looks cut off instead of flowing underneath. The fix: pass the inset values to contentPadding, as the Superheroes list does.

Mistake #6: animateContentSize after padding. The wrong code: Modifier.padding(16.dp).animateContentSize(). The problem: the padding is outside the animated area and jumps. The fix: put animateContentSize() before the padding.

Mistake #7: Forgetting by imports for animated values. The symptom: animateDpAsState returns a State<Dp> and using it as Dp fails. The fix: import androidx.compose.runtime.getValue and write val x by animateDpAsState(...), or use .value.

Mistake #8: Over-animating. The problem: long, bouncy effects on every element make the app feel slow and can bother users sensitive to motion. The fix: animate only to explain a change, keep it brief (around 150 to 400 ms) and respect that some users disable animations.

Mistake #9: Testing only in light mode. The symptom: an unreadable screen at night. The fix: add a dark @Preview for every screen and flip the emulator's dark theme switch before you finish.

Mistake #10: Theme not wrapping the content. The symptom: the app ignores your colours. The cause: setContent { SuperheroesApp() } without SuperheroesTheme { ... } around it, so the screen uses defaults. The fix: wrap the root composable in the theme, and in every preview too.

✏️ Your Task
Practise what you just learned about Common Beginner Mistakes (and How to Fix Them). Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 10 of 10
Knowledge Check

Question 1. What three things does MaterialTheme bundle together?

A colorScheme (colours by role), typography (the text styles) and shapes (corner shapes). Content inside the theme reads them via MaterialTheme.colorScheme, .typography and .shapes.

Question 2. Why must you pair primary with onPrimary?

onPrimary is the colour chosen for text and icons drawn on top of primary, with enough contrast to be readable. Using another colour risks unreadable content, especially in dark mode.

Question 3. What is dynamic colour and why do you check Build.VERSION.SDK_INT?

Dynamic colour builds the colour scheme from the user's wallpaper on Android 12 (API 31) and above, using dynamicLightColorScheme and dynamicDarkColorScheme. Those functions do not work on older versions, so you must check Build.VERSION.SDK_INT >= Build.VERSION_CODES.S and fall back to your own scheme.

Question 4. Which animation tool would you use to (a) smoothly resize a card when text is added, (b) fade a text block in and out, (c) change a colour between two states?

(a) Modifier.animateContentSize(), (b) AnimatedVisibility, (c) animateColorAsState.

Question 5. Why is animateContentSize() placed before padding in the modifier chain?

Modifiers apply in order. Putting animateContentSize() first means the size it animates includes the padding that comes after it, so the padded area changes smoothly instead of jumping.

Go deeper: official resources

✏️ Your Task
Practise what you just learned about Knowledge Check. Open Android Studio, type the examples by hand, change a value, and observe what happens.
🏗️ Build It — Mini Project
30 Days of Coding

Project goal: This is the Project: Create a 30 Days App. Build a themed, scrolling app with 30 cards, one for each day of a 30-day challenge, where each card expands to show the day's task. It is a polished portfolio piece that uses lists, theming, shapes and animation together.

Requirements:

  1. Create a project 30 Days with package com.techbase.thirtydays.
  2. Create a theme (ThirtyDaysTheme) with your own light and dark ColorScheme, a custom Typography that changes at least two styles, and Shapes with at least one custom corner.
  3. Model each day with a data class Day(val number: Int, val title: String, val task: String) and a list of 30 days about a goal you care about (learning to code, fitness, saving money, learning Yoruba, anything).
  4. Show the days in a LazyColumn of cards, with a key for each item. Each collapsed card shows Day N and the title.
  5. Tapping a card expands it with animateContentSize, shows the task with AnimatedVisibility, and animates the card colour with animateColorAsState.
  6. Use Scaffold with a top app bar titled 30 Days of Coding, edge to edge with correct insets.
  7. Provide light and dark @Preview functions, and test on a real or emulated device in both modes.
  8. Use plain String fields as in the solution below, or (better) move all text to strings.xml with @StringRes ids.

Stretch goal: Add a LazyListState and a floating action button that scrolls back to day 1 with animateScrollToItem(0) (this needs a coroutine scope; see lesson 28 or read ahead).

The solution keeps the thirty days in Kotlin strings to stay readable. In a published app you would move them to strings.xml, as in lessons 21 and the Superheroes app.

Day.kt
package com.techbase.thirtydays
data class Day(val number: Int, val title: String, val task: String)
val days: List<Day> = listOf(
 Day(1, "Set up", "Install Android Studio and run the default app on a device."),
 Day(2, "Say hello", "Print your name in the Kotlin Playground three different ways."),
 Day(3, "Variables", "Store your age and city in variables and print a sentence."),
 Day(4, "Functions", "Write a function that returns the area of a rectangle."),
 Day(5, "Decisions", "Write an if expression that classifies a score as pass or fail."),
 Day(6, "Loops", "Print the 7 times table with a for loop."),
 Day(7, "Review", "Redo days 2 to 6 without looking at your notes."),
 Day(8, "Lists", "Make a list of five foods and print each one in capitals."),
 Day(9, "Maps", "Store three student names with their scores in a map."),
 Day(10, "Lambdas", "Use filter to keep only scores above 50."),
 Day(11, "Text composable", "Show your name in a large Text on screen."),
 Day(12, "Column and Row", "Arrange three Text items in a Column and then a Row."),
 Day(13, "Modifiers", "Add padding and a background to a Text."),
 Day(14, "Review", "Rebuild the birthday card from memory."),
 Day(15, "Images", "Show a picture with a content description."),
 Day(16, "Buttons", "Make a button that increases a counter."),
 Day(17, "State", "Keep two counters in separate state variables."),
 Day(18, "Text fields", "Echo what the user types into a Text below."),
 Day(19, "Lists on screen", "Show 10 items in a LazyColumn."),
 Day(20, "Cards", "Wrap each list item in a Card."),
 Day(21, "Review", "Build a small contact list with cards."),
 Day(22, "Colours", "Create a light and dark colour scheme."),
 Day(23, "Typography", "Change the headline style of your app."),
 Day(24, "Shapes", "Give your cards a custom corner shape."),
 Day(25, "Animation", "Animate a card growing when tapped."),
 Day(26, "Testing", "Write one unit test for a function of your own."),
 Day(27, "Debugging", "Put a breakpoint in a click handler and inspect a value."),
 Day(28, "Icon", "Create an adaptive launcher icon."),
 Day(29, "Polish", "Test in dark mode and with a large font size."),
 Day(30, "Share", "Show your app to a friend and ask for one improvement.")
)
Theme.kt
package com.techbase.thirtydays.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Shapes
import androidx.compose.material3.Typography
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
private val LightColors = lightColorScheme(
 primary = Color(0xFF3F51B5),
 onPrimary = Color.White,
 primaryContainer = Color(0xFFDDE1FF),
 onPrimaryContainer = Color(0xFF00105C),
 secondaryContainer = Color(0xFFFFE0B2),
 onSecondaryContainer = Color(0xFF2B1700),
 background = Color(0xFFFEFBFF),
 onBackground = Color(0xFF1B1B1F),
 surface = Color(0xFFFEFBFF),
 onSurface = Color(0xFF1B1B1F)
)
private val DarkColors = darkColorScheme(
 primary = Color(0xFFB9C3FF),
 onPrimary = Color(0xFF08218A),
 primaryContainer = Color(0xFF2A3990),
 onPrimaryContainer = Color(0xFFDDE1FF),
 secondaryContainer = Color(0xFF5C3F00),
 onSecondaryContainer = Color(0xFFFFE0B2),
 background = Color(0xFF1B1B1F),
 onBackground = Color(0xFFE4E1E6),
 surface = Color(0xFF1B1B1F),
 onSurface = Color(0xFFE4E1E6)
)
private val AppTypography = Typography(
 titleMedium = TextStyle(fontWeight = FontWeight.Bold, fontSize = 18.sp),
 labelMedium = TextStyle(fontWeight = FontWeight.Medium, fontSize = 13.sp)
)
private val AppShapes = Shapes(
 medium = RoundedCornerShape(topStart = 4.dp, topEnd = 24.dp, bottomEnd = 4.dp, bottomStart = 24.dp)
)
@Composable
fun ThirtyDaysTheme(
 darkTheme: Boolean = isSystemInDarkTheme(),
 content: @Composable () -> Unit
) {
 MaterialTheme(
 colorScheme = if (darkTheme) DarkColors else LightColors,
 typography = AppTypography,
 shapes = AppShapes,
 content = content
 )
}
MainActivity.kt
package com.techbase.thirtydays
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.animateContentSize
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
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.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.techbase.thirtydays.ui.theme.ThirtyDaysTheme
class MainActivity : ComponentActivity() {
 override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
 enableEdgeToEdge()
 setContent {
 ThirtyDaysTheme {
 ThirtyDaysApp()
 }
 }
 }
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ThirtyDaysApp() {
 Scaffold(
 modifier = Modifier.fillMaxSize(),
 topBar = {
 CenterAlignedTopAppBar(title = { Text("30 Days of Coding") })
 }
 ) { innerPadding ->
 LazyColumn(
 contentPadding = PaddingValues(
 start = 16.dp,
 end = 16.dp,
 top = innerPadding.calculateTopPadding() + 8.dp,
 bottom = innerPadding.calculateBottomPadding() + 8.dp
 ),
 verticalArrangement = Arrangement.spacedBy(8.dp)
 ) {
 items(days, key = { it.number }) { day ->
 DayCard(day)
 }
 }
 }
}
@Composable
fun DayCard(day: Day, modifier: Modifier = Modifier) {
 var expanded by rememberSaveable { mutableStateOf(false) }
 val containerColor by animateColorAsState(
 targetValue = if (expanded) {
 MaterialTheme.colorScheme.secondaryContainer
 } else {
 MaterialTheme.colorScheme.primaryContainer
 },
 label = "dayCardColor"
 )
 Card(
 modifier = modifier
 .fillMaxWidth()
 .clickable { expanded = !expanded },
 colors = CardDefaults.cardColors(containerColor = containerColor)
 ) {
 Column(
 modifier = Modifier
 .animateContentSize()
 .padding(16.dp)
 ) {
 Text(text = "Day ${day.number}", style = MaterialTheme.typography.labelMedium)
 Text(text = day.title, style = MaterialTheme.typography.titleMedium)
 AnimatedVisibility(visible = expanded) {
 Text(
 text = day.task,
 style = MaterialTheme.typography.bodyLarge,
 modifier = Modifier.padding(top = 8.dp)
 )
 }
 }
 }
}
@Preview(showBackground = true)
@Composable
fun ThirtyDaysLightPreview() {
 ThirtyDaysTheme(darkTheme = false) { ThirtyDaysApp() }
}
@Preview(showBackground = true, uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES)
@Composable
fun ThirtyDaysDarkPreview() {
 ThirtyDaysTheme(darkTheme = true) { ThirtyDaysApp() }
}

What the screen shows: a centred top bar, then 30 cards titled Day 1 / Set up and so on. Tapping a card changes its colour to the secondary container colour, grows it smoothly and reveals the day's task. Tapping again collapses it. The same screen works in light and dark mode because no colour appears outside the theme.

Why it works:

  • The whole card is clickable, which gives a larger touch target than a small button.
  • rememberSaveable holds each card's expanded flag across rotation and across scrolling away and back.
  • items(days, key = { it.number }) gives each card a stable identity, and Day.number is unique.
  • The text colours come from the card's default content colour, which Material 3 derives from the container colour, so contrast is maintained as the card changes colour. Check this by eye in both modes.
  • The Shapes.medium corner is used by Card automatically, so no screen code mentions the corner shape.
1

Open your finished project in Android Studio. Check that the .gitignore file the wizard created is in the project root. It keeps local.properties, build folders and keystores out of Git. Never commit passwords or signing keys.

2

Choose Git > Create Git Repository… (or VCS > Enable Version Control Integration in older versions), select the project folder, then open Git > Commit, tick every file, and write the message “Add Android lesson 22 project”.

3

On github.com click New repository, name it android-lesson-22-material-theming-and-simple-animation-supe, set it to Public, leave Add a README unticked, and click Create repository.

4

Back in Android Studio choose Git > Manage Remotes…, add the repository URL as origin, then choose Git > Push… and push your main branch. Prefer the terminal? Run git remote add origin <your-url> then git push -u origin main.

5

Add a README.md with a screenshot of your app running and a short description. Employers and clients read READMEs first. Your project is now part of your public portfolio. 🎉

Lesson 22 complete! 🎉

You covered:

  • Lesson Introduction
  • Prerequisite Concepts
  • Conceptual Understanding: Material Design 3 and Themes
  • Step-by-Step Build: Creating a Theme
  • Step-by-Step Build: The Superheroes Practice App
  • Step-by-Step Build: Simple Animation
  • Step-by-Step Build: The Expandable Hero Card
  • Guided Practice Exercises
  • Common Beginner Mistakes (and How to Fix Them)
  • Knowledge Check
All 72 lessons in Google Android Studio ·Techbase Academy ·© 2025–2026 Techbase Consultant Services, Ibadan
Esc
↑ ↓ to moveEnter to openEsc to close