Currently, there is an ongoing Chinese manual project for Jetpack Compose, which aims to help developers better understand and master the Compose framework. It is still under construction, and everyone is welcome to follow and join! Project address: github.com/compose-mus… This article was written by me and has been published in the manual. Please check it out.

I met MaterialTheme

The MaterialTheme is a Material Design-based theme style template provided by Jetpack Compose. By configuring the Theme style template, the MaterialTheme allows all components in the custom view system to change their styles according to the theme switch.

When I create a new project, Android Studio will generate a Theme method for me by default.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContent {
            // Look here, I created a project named ComposeStudy ~
            // It is worth noting that the custom view we declare is passed in as a lambda argument.
            ComposeStudyTheme {  
               Surface(color = MaterialTheme.colors.background) {
                    Greeting("Android")}}}}}Copy the code

Next, let’s see what the generated Theme method does for us.

@Composable
fun ComposeStudyTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable()() - >Unit
) {
    val colors = if (darkTheme) {
        DarkColorPalette
    } else {
        LightColorPalette
    }
    MaterialTheme(
        colors = colors, / / color
        typography = Typography, / / font
        shapes = Shapes, / / shape
        content = content // The declared view)}private val DarkColorPalette = darkColors(
    primary = Purple200,
    primaryVariant = Purple700,
    secondary = Teal200
)
private val LightColorPalette = lightColors(
    primary = Purple500,
    primaryVariant = Purple700,
    secondary = Teal200
)
Copy the code

Here we see the MaterialTheme. But before we look up, you can see that Android Studio by default helps us generate two color palettes (Light and Dark), selecting one based on the Boolean value passed in and passing it to the MaterialTheme. You can see that the palettes for these two colors use the return values of the darkColors and lightColors methods, respectively. Let’s look at the implementation of these two.

fun lightColors(
    primary: Color = Color(0xFF6200EE),
    primaryVariant: Color = Color(0xFF3700B3),
    secondary: Color = Color(0xFF03DAC6),
    secondaryVariant: Color = Color(0xFF018786),
    background: Color = Color.White,
    surface: Color = Color.White,
    error: Color = Color(0xFFB00020),
    onPrimary: Color = Color.White,
    onSecondary: Color = Color.Black,
    onBackground: Color = Color.Black,
    onSurface: Color = Color.Black,
    onError: Color = Color.White
): Colors = Colors(
    primary,
    primaryVariant,
    secondary,
    secondaryVariant,
    background,
    surface,
    error,
    onPrimary,
    onSecondary,
    onBackground,
    onSurface,
    onError,
    true
)
Copy the code

As you can see, lightColors passes the passed parameters to the Colors constructor. The Colors constructor properties have no default values. LightColors helps us generate many default values for the properties. You can see that the two palettes are essentially just a difference in the Colors member property configuration, knowing the nature of the palette allows you to customize the theme style configuration.

Simply use the MaterialTheme

The following example assumes that the current requirement is to have our custom text color vary depending on the theme. The color is red when the theme is bright and blue when the theme is dark. Here we use the primary property of Color for storage, but we can also use other member properties.

@Composable
fun CustomColorTheme(
    isDark: Boolean,
    content: @Composable() () - >Unit
) {
    var BLUE = Color(0xFF0000FF) 
    var RED = Color(0xFFDC143C)
    val colors = if (isDark) {
        darkColors(primary = BLUE) // Set primary to blue
    } else {
        lightColors(primary = RED) // Set primary to red
    }
    MaterialTheme(
        colors = colors,
        typography = Typography,
        shapes = Shapes,
        content = content
    )
}
Copy the code

Finish configuration can be in our custom view system, our view of the Text color configuration for MaterialTheme. The colors. The primary.

@Composable
fun SampleText(a) {
    Text(
        text = "Hello World",
        color = MaterialTheme.colors.primary
    )
}
@Preview(showBackground = true)
@Composable
fun DarkPreview(a) {
    CustomColorTheme(isDark = true) { SampleText(); }}@Preview(showBackground = true)
@Composable
fun LightPreview(a) {
    CustomColorTheme(isDark = false) {
        SampleText()
    }
}
Copy the code

We created previews of both themes at the same time, and you can Preview all of them through the Preview window of Android Studio.

How does the MaterialTheme do this

To get a better understanding of how MaterialTheme works, we need to dive into the source code.

Note that the content parameter passed in is actually a custom layout system declared in the Theme. Its type is a Lambda with a Composable annotation (for this class of lambda with a Composable annotation, it is simply called Composable).

The colors we care about are marked by remember and assigned the value rememberedColors. If the MaterialTheme composable is recompose, it checks for changes in colors and decides to update.

Next, the CompositionLocalProvider method is used to provide rememberedColors to LocalColors via the infix providers. Let’s go back to the custom view and see how we get the current theme color palette from the MaterialTheme.

object MaterialTheme {
    val colors: Colors
        @Composable
        @ReadOnlyComposable
        get() = LocalColors.current
    val typography: Typography
        @Composable
        @ReadOnlyComposable
        get() = LocalTypography.current
    val shapes: Shapes
        @Composable
        @ReadOnlyComposable
        get() = LocalShapes.current
}
Copy the code

You can see that the colors property of the MaterialTheme class singleton is used to obtain the current theme color, indirectly using LocalColors.

In summary, we use the MaterialTheme function to assign a value to LocalColors for the custom Theme and the MaterialTheme singleton to obtain the value indirectly from LocalColors. So what is LocalColors?

internal val LocalColors = staticCompositionLocalOf { lightColors() }
Copy the code

The declaration shows that it is actually a CompositionLocal whose initial value is the colors configuration returned by lightColors().

The MaterialTheme method provides some CompositionLocal for our custom view composable through the CompositionLocalProvider method, which contains all the topic configuration information.

CompositionLocal introduction

Many times you need to share some data (such as topic configuration) in the Composable tree. One way to do this is by explicitly passing parameters. As the number of parameters increases, the composable parameter list becomes increasingly bloated and difficult to maintain. When the Composable needs to pass data to each other and maintain their own privacy, using explicit parameter passing can cause unexpected trouble and crashes.

To address these pain points, Jetpack Compose provides CompostionLocal for completing the composable tree. CompositionLocals are hierarchical and can be restricted to subtrees with a composable as the root, which are passed down by default. Of course, CompositionLocals can be overwritten by a composable in the current subtree. This causes new values to continue to be passed down in the Composable.

Jetpack Compose provides a compositionLocalOf method for creating a CompostionLocal instance.

import androidx.compose.runtime.compositionLocalOf

var LocalString = compositionLocalOf { "Jetpack Compose" }
Copy the code

Somewhere in the Composable tree, we can provide a value for CompositionLocal using the CompositionLocalProvider method. Typically at the root of a Composable tree, but it can be anywhere, and can be used in multiple locations to override the values available to the subtree. Our example chose to use CompositionLocalProvider in the Composable contained in the Column.

import androidx.compose.runtime.CompositionLocalProvider

setContent {
    CustomColorTheme(true) {
        Column {
            CompositionLocalProvider(
                LocalString provides "Hello World"
            ) {
                Text(
                    text = LocalString.current,
                    color = Color.Green
                )
                CompositionLocalProvider(
                    LocalString provides "Ruger McCarthy"
                ) {
                    Text(
                        text = LocalString.current,
                        color = Color.Blue
                    )
                }
            }
            Text(
                text = LocalString.current,
                color = Color.Red
            )
        }
    }
}
Copy the code

In practice, you can see that while all of the Composable rely on the same CompositionLocal, the actual values are different.

Customize your theme scheme

Examples are from: github.com/RugerMcCart…

By reading the first two articles, I believe that you have the ability to customize the theme solution. Let’s use the #AndroidDevChallange challenge week 3 as an example to see how this works in a real project. Background color, text color and image resources are all different under different theme schemes. It is important to note that font styles can also be configured for all text through the theme, with the desired effect as shown in the figure below.

Configuring color Styles

First, let’s learn how to configure color styles. In fact, this is the same as in the MaterialTheme chapter. We just need to generate the colors for each theme. According to the project requirements, we carry out the following configuration.

private val BloomLightColorPaltte = lightColors(
    primary = pink100,
    secondary = pink900,
    background = white,
    surface = white850,
    onPrimary = gray,
    onSecondary = white,
    onBackground = gray,
    onSurface = gray,
)

private val BloomDarkColorPaltte = darkColors(
    primary = green900,
    secondary = green300,
    background = gray,
    surface = white150,
    onPrimary = white,
    onSecondary = gray,
    onBackground = white,
    onSurface = white850
)

@Composable
fun BloomTheme(theme: BloomTheme = BloomTheme.LIGHT, content: @Composable() () - >Unit) {
    CompositionLocalProvider(
        LocalWelcomeAssets provides if (theme == BloomTheme.DARK) WelcomeAssets.DarkWelcomeAssets else WelcomeAssets.LightWelcomeAssets,
    ) {
        MaterialTheme(
            colors = if (theme == BloomTheme.DARK) BloomDarkColorPaltte else BloomLightColorPaltte,
            typography = Typography,
            shapes = shapes,
            content = content
        )
    }
}
Copy the code

Just configure the Color where our view needs it.

Text(
    text = "Beautiful home garden solutions",
    textAlign = TextAlign.Center,
    color = MaterialTheme.colors.onPrimary // I'm here
)
Copy the code

Configuring font Styles

Let’s learn how to configure font styles. The second parameter, typography, is the type of font you configured in Android Studio by default.

MaterialTheme(
  	colors = colors,
  	typography = Typography,
  	shapes = Shapes,
  	content = content
)
Copy the code

If it is a new project, Android Studio generates type.kt under the ui.theme package, which contains the implementation of Typography. The variable named Typography indirectly calls the constructor of the Typography class.

val Typography = Typography(
    body1 = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Normal,
        fontSize = 16.sp
    )
)
Copy the code

Going back to the MaterialTheme implementation, you can see that typography provides a CompositionLocal instance of LocalTypography, so there is no need to explain how we use this particular font in our project, which is exactly the same as colors.

@Composable
fun MaterialTheme(
    colors: Colors = MaterialTheme.colors,
    typography: Typography = MaterialTheme.typography,
    shapes: Shapes = MaterialTheme.shapes,
    content: @Composable() - >Unit
) {
    val rememberedColors = remember {
        colors.copy()
    }.apply { updateColorsFrom(colors) }
    val rippleIndication = rememberRipple()
    val selectionColors = rememberTextSelectionColors(rememberedColors)
    CompositionLocalProvider(
        LocalColors provides rememberedColors,
        LocalContentAlpha provides ContentAlpha.high,
        LocalIndication provides rippleIndication,
        LocalRippleTheme provides MaterialRippleTheme,
        LocalShapes provides shapes,
        LocalTextSelectionColors provides selectionColors,
        LocalTypography provides typography // I'm here~
    ) {
        ProvideTextStyle(value = typography.body1, content = content)
    }
}
Copy the code

Now that we understand the principle, we only need to configure the font style according to the actual requirements of the project. Since Android Studio helps to generate type.kt, it means that the official wants us to configure the font style in this file. It’s the norm, but it’s not the norm.

It is important to note that since every font has a different style of weight, we need to specify the type of font and the style of weight in the font style configuration.

val nunitoSansFamily = FontFamily(
    Font(R.font.nunitosans_light, FontWeight.Light),
    Font(R.font.nunitosans_semibold, FontWeight.SemiBold),
    Font(R.font.nunitosans_bold, FontWeight.Bold)
)
val bloomTypography = Typography(
    h1 = TextStyle(
        fontSize = 18.sp,
        fontFamily = nunitoSansFamily,
        fontWeight = FontWeight.Bold
    ),
    h2 = TextStyle(
        fontSize = 14.sp,
        letterSpacing = 0.15.sp,
        fontFamily = nunitoSansFamily,
        fontWeight = FontWeight.Bold
    ),
    ....
)
Copy the code

To use it, it is simple to simply pass the font style into the MaterialTheme.

@Composable
fun BloomTheme(theme: BloomTheme = BloomTheme.LIGHT, content: @Composable() () - >Unit) {
    MaterialTheme(
         colors = if (theme == BloomTheme.DARK) BloomDarkColorPaltte else BloomLightColorPaltte,
         typography = bloomTypoGraphy,
         shapes = shapes,
         content = content
    )
}
Copy the code

Just use the style parameter in our view component.

Text(
    text = "Beautiful home garden solutions",
    textAlign = TextAlign.Center,
    style = MaterialTheme.typography.subtitle1, // I'm here
    color = MaterialTheme.colors.onPrimary
)
Copy the code

Configure custom resources

Sometimes we may need to use different multimedia resources, such as pictures, video, audio, and so on, depending on the subject. By looking at the MaterialTheme parameter list, we find no parameters that can be configured. Doesn’t Jetpack Compose have this capability? The answer, of course, is no. The Android team has fully considered the scenarios, but for this particular requirement, we need to make additional custom extensions.

In the previous article, we have described the working principle of MaterialTheme in detail, which, as you might guess, extends the image resources by customizing CompositionLocal, and selects the corresponding multimedia resources according to the different topics.

open class WelcomeAssets private constructor(
    var background: Int.var illos: Int.var logo: Int
) {
    object LightWelcomeAssets : WelcomeAssets(
        background = R.drawable.ic_light_welcome_bg,
        illos = R.drawable.ic_light_welcome_illos,
        logo = R.drawable.ic_light_logo
    )

    object DarkWelcomeAssets : WelcomeAssets(
        background = R.drawable.ic_dark_welcome_bg,
        illos = R.drawable.ic_dark_welcome_illos,
        logo = R.drawable.ic_dark_logo
    )
}

internal var LocalWelcomeAssets = staticCompositionLocalOf {
  	WelcomeAssets.LightWelcomeAssets as WelcomeAssets
}
Copy the code

At the same time, we also want to be able to access our image resources from within the view through the MaterialTheme, so we can do so using the Properties of the Kotlin extended properties (which have no fields behind them and can only delegate to other instances). Note that CompositionLocal can only be used in a Composable (a lambda with a composable annotation), so we need to add @Composable and @ReadOnlyComposable annotations for this property acquisition.

val MaterialTheme.welcomeAssets
    @Composable
    @ReadOnlyComposable
    get() = LocalWelcomeAssets.current
Copy the code

This way we can still get the extended image resource from the MaterialTheme in the view.

Image( painter = rememberVectorPainter(image = ImageVector.vectorResource(id = MaterialTheme.welcomeAssets.background)),  contentDescription ="weclome_bg",
     modifier = Modifier.fillMaxSize()
)
Copy the code

Now that you know the theme configuration for the images, the theme configuration for the other multimedia resources is exactly the same.