Dynamic Color (Material You)

Starting with Android 12, the system can generate an entire ColorScheme from the colors in the user's wallpaper — this is "Material You," and it's why the same app can look orange on one phone and teal on another without a single line of per-user code. This lesson wires that system capability into RecipeBox's existing RecipeBoxTheme, alongside the static orange lightColorScheme()/darkColorScheme() pair from two lessons ago, which stays in place as the fallback for every device that can't generate dynamic color.

dynamicLightColorScheme() and dynamicDarkColorScheme()

These two functions live in the Material 3 library and both take an Android Context, reading the device's current wallpaper-derived palette and returning a complete ColorScheme — the light and dark variant of the same generated palette:

Signatures

fun dynamicLightColorScheme(context: Context): ColorScheme
fun dynamicDarkColorScheme(context: Context): ColorScheme

Both only work on API 31 (Android 12) and above — the underlying wallpaper color extraction simply doesn't exist on earlier platform versions, which is exactly why this feature needs a version check and a fallback, never a direct unconditional call.

Gating on Build.VERSION.SDK_INT

Build.VERSION_CODES.S is the named constant for API 31. Checking Build.VERSION.SDK_INT >= Build.VERSION_CODES.S before calling either dynamic function is required — calling them on an older device would crash, since the classes they depend on don't exist there yet:

Theme/Theme.kt — combining static and dynamic color

@Composable
fun RecipeBoxTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    dynamicColor: Boolean = true,
    content: @Composable () -> Unit
) {
    val context = LocalContext.current

    val colorScheme = when {
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
        }
        darkTheme -> RecipeBoxDarkColors
        else -> RecipeBoxLightColors
    }

    MaterialTheme(
        colorScheme = colorScheme,
        typography = RecipeBoxTypography,
        content = content
    )
}

The dynamicColor parameter defaults to true but stays a parameter, not a hardcoded check, on purpose — it gives RecipeBox's Settings screen a place to hang a "Use Wallpaper Colors" toggle later, letting a user opt back into the fixed orange brand color even on a device that supports Material You.

Why the Static Fallback Still Matters

Dynamic color is genuinely optional — RecipeBox's brand identity is the warm orange RecipeBoxLightColors/RecipeBoxDarkColors pair built earlier in this section, and that pair is what every pre-Android-12 device, and every device where dynamicColor is turned off, continues to see. Nothing about the earlier lessons' work is wasted — this lesson only adds a branch in front of it.

ConditionColorScheme used
API 31+, dynamicColor = true, light modedynamicLightColorScheme(context)
API 31+, dynamicColor = true, dark modedynamicDarkColorScheme(context)
API < 31, or dynamicColor = false, light modeRecipeBoxLightColors
API < 31, or dynamicColor = false, dark modeRecipeBoxDarkColors

Wiring It Up at the App Root

The call site barely changes from the previous lesson — RecipeBoxTheme's new parameter simply defaults sensibly, so most screens don't need to think about dynamic color at all:

MainActivity.kt

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            // dynamicColor defaults to true; falls back automatically below API 31
            RecipeBoxTheme {
                RecipeApp()
            }
        }
    }
}
Note: Because dynamic color is generated from the live wallpaper on a real device, and this site's code samples aren't a running Android Studio build, the specific hex values dynamic color would produce can't be shown here — only the API that requests them.
Next up: Theming is done — RecipeBox now needs a proper place to hold its recipe list and loading state as it grows. ViewModel in Compose starts the State & Architecture section.