Jetpack Compose Tutorial: Lesson 1

Today, I started Google's Compose Essentials course. Here's a peek into what I explored and learned.

Courses

Composable Functions

Customized Greeting based on Time of Day

We started off with composable functions. These functions let me shape my app's UI programmatically. I found it neat how I could describe my UI's appearance using data. If you've used it before, it's like data binding but much cleaner. No more crazy debugging and weird states.

Part 1: Adding a Text Element

The first task asked me to add a text element To the screen. The crucial part of this code is in this snippet here.

setContent {
            Text("Hello world!")
           }

Even though the exercise is simply a copy paste of this code into the MainActivity class, it teaches us that instead of loading in a XML file reference ID, we can directly add in a composable object through the lambda function.

This might be fast tracking a little bit further but because there is no preview function, You can't see the changes until you run the app on an emulator or your device.

Part 2: Crafting a Composable Function

The second task modifies this code so that we are creating our own composable function called MessageCard.

@Composable
fun MessageCard(name: String) {
    Text(text = "Hello $name!")
}

The key takeaway here is that since it's a function, it can take in a parameter and generate a UI element based off this parameter. In this case the name parameter changes the string of the text.

Just like the first task, you won't be able to see any changes until you run it on a device. The good thing is that the tutorial website gives you a preview so you don't have to run it yourself.

Part 3: Previewing in Android Studio

A split screen of some preview functions and their corresponding previews in action

The final task of this lesson is to show you how the @Preview annotation can add a preview window in Android Studio. I found this the most interesting element! Normally, to get to a particular UI state, you'd have to wait for the app to install and navigate to the screen with the right credentials. XML had tools attributes for doing this but the results it was difficult to set up and it made the code really difficult to read. Plus, you can only use once.

With Compose, you can preview all the different facets at the same time. I'll be digging further into this to see what it can do.


Deep Dive Challenges

After finishing this Jetpack Compose lesson, I set up some challenges for myself. Here's why - Doing these challenges helps me use what I learned in real situations, making sure I really get it. They also let me explore more parts of Compose that I might not see in the curated lesson.

Plus, figuring out these tasks is like solving real-world problems, giving me more confidence. And once I finish them, I have something to show, which is great for showcasing my skills or maybe even get job opportunities. It's a hands-on way to keep learning and get better at Android development.

Challenge 1: Dynamic Font Size

Description: Create a DynamicFontSizeText Composable function that accepts a message and dynamically adjusts the font size of the text based on the length of the message. For instance, if the message has less than 10 characters, use a large font size. If it has 10-20 characters, use a medium font size. If the message has more than 20 characters, use a small font size.

Implementation: If you want to read about how I iteratively figured out and tested each feature, read on. Or if you want the solution, go straight to the bottom of this section.

First started by creating a scaffold of the composable function. Since we are given a string I figured we pass that in as the parameter. So my code would start looking like this:

@Composable
fun DynamicFontSizeText(message: String) {

}

Next, I try out calculating the length of the message and using that value to run a simple text composable:

@Composable
fun DynamicFontSizeText(message: String) {
    val length = message.length
    when {
        length < 10 -> Text("Short Text")
        else -> Text("Long Text")
    }
}

I added some previews so that I could see each branch in action

@Preview(name = "Short", group = "DynamicFont", showBackground = true)
@Composable
fun DynamicFontPreviewShort() {
    DynamicFontSizeText("Mother")
}

@Preview(name = "Long", group = "DynamicFont", showBackground = true)
@Composable
fun DynamicFontPreviewLong() {
    DynamicFontSizeText("MotherOfPearl")
}

And this was the result.

A preview of my dynamic font in progress

Now that I've figured out how to add branching logic to the composable function, I clean up the branches to regulate the font size based on the requirements of the challenge.

@Composable
fun DynamicFontSizeText(message: String) {
    val length = message.length
    val fontSize = when {
        length < 10 -> 18.sp
        length < 20 -> 14.sp
        else -> 10.sp
    }
    Text(message, fontSize = fontSize)
}

I then set up some test previews to show that each branch works accordingly.

@Preview(name = "Short", group = "DynamicFont", showBackground = true)
@Composable
fun DynamicFontPreviewShort() {
    DynamicFontSizeText("Mother")
}

@Preview(name = "Medium", group = "DynamicFont", showBackground = true)
@Composable
fun DynamicFontPreviewMedium() {
    DynamicFontSizeText("MotherOfPearl")
}

@Preview(name = "Long", group = "DynamicFont", showBackground = true)
@Composable
fun DynamicFontPreviewLong() {
    DynamicFontSizeText("Supercalifragilisticexpialidocious")
}

This is the result. Mission accomplished

A preview of my dynamic font in progress

Challenge 2: Gradient Text

Description: Craft a GradientText Composable function that displays a text with a gradient color. The function should take in a message and two colors. The text should display with a horizontal gradient starting from the first color and transitioning to the second color.

Implementation: I had a look at the parameters of the Text composable and setting the color attribute seemed the closest. From looking at the Color class, it only seems to support single colors rather than gradients.

@Composable
fun Text(
    text: String,
    modifier: Modifier = Modifier,
    color: Color = Color.Unspecified,
    fontSize: TextUnit = TextUnit.Unspecified,
    fontStyle: FontStyle? = null,
    fontWeight: FontWeight? = null,
    fontFamily: FontFamily? = null,
    letterSpacing: TextUnit = TextUnit.Unspecified,
    textDecoration: TextDecoration? = null,
    textAlign: TextAlign? = null,
    lineHeight: TextUnit = TextUnit.Unspecified,
    overflow: TextOverflow = TextOverflow.Clip,
    softWrap: Boolean = true,
    maxLines: Int = Int.MAX_VALUE,
    minLines: Int = 1,
    onTextLayout: (TextLayoutResult) -> Unit = {},
    style: TextStyle = LocalTextStyle.current
)

Looking up Compose Text Gradients on Google yielded this Medium Article

I found it a really valuable resource in going through how to add a gradient color onto a text composable. The article steps through different ways To add a gradient to color the text. The key takeaway is that before compose version 1.2.0, You would need to use the canvas to draw on top of the text. Compose 1.2.0 introduces the brush API which gives you the power to color the text much easier.

The code snippet most relevant to my problem here is this:

Text(
   text = text,
   style = TextStyle(
       brush = Brush.linearGradient(
           colors = GradientColors
       )
   )
)

There is way more to explore down this path but I will limit my exploration to the constraints of this problem for now.

So my composable function will receive a message as a string type and two colors. This will be using the Color class. The only difference is that I am supplying two Colors instead of one and creating a list of them to give to the brush. Just make sure that the Color class comes from androidx.compose.ui.graphics.Color

You can use the default constants in the colors class or make your own using the constructor that takes in an integer for the colour code. This integer can be written in hex format 0xFF123456 where 123456 is the color code and the last two digits FF represents the alpha channel.

@Composable
fun GradientText(message: String, colorStart: Color, colorEnd: Color) {
    val colors = listOf(colorStart, colorEnd)
    Text(
        text = message,
        style = TextStyle(
            brush = Brush.linearGradient(
                colors = colors
            )
        )
    )
}

@Preview(name = "Short", group = "Gradient", showBackground = true)
@Composable
fun PreviewGradientText() {
    GradientText("I am a colorful gradient text", Color.Green, Color(0xFFFF9914))
}

This is the final result:

A preview of my dynamic font in progress

Challenge 3: Animated Blinking Text

Description: Design a BlinkingText Composable function that animates a text to blink (toggle between visible and invisible) at a regular interval. The function should accept the message, blink duration (in milliseconds), and the number of blinks as parameters. Ensure that the blinking stops after the specified number of blinks.

Implementation:

I tried out the basic animations but couldn't get them to work. I looked into using repeatable() and infiniteTransition() but there's a gap in my knowledge.

I tried following the animation guide here but the animation doesn't seem to run when I preview in interactive mode. Link to the guide

val infiniteTransition = rememberInfiniteTransition(label = "infinite transition")
val scale by infiniteTransition.animateFloat(
    initialValue = 1f,
    targetValue = 8f,
    animationSpec = infiniteRepeatable(tween(1000), RepeatMode.Reverse),
    label = "scale"
)
Box(modifier = Modifier.fillMaxSize()) {
    Text(
        text = "Hello",
        modifier = Modifier
            .graphicsLayer {
                scaleX = scale
                scaleY = scale
                transformOrigin = TransformOrigin.Center
            }
            .align(Alignment.Center),
        // Text composable does not take TextMotion as a parameter.
        // Provide it via style argument but make sure that we are copying from current theme
        style = LocalTextStyle.current.copy(textMotion = TextMotion.Animated)
    )
}

Eventually, I hacked my way into using a LaunchedEffect to run a timer coroutine that triggers every X milliseconds.

Here's my final code:

@Composable
fun BlinkingText(message: String, blinkCount: Int, period: Long) {
    var state by remember { mutableStateOf(false) }
    var count by remember { mutableIntStateOf(blinkCount) }

    LaunchedEffect(key1 = Unit) {
        delay(period)
        timer(name = "Ticker", period = period) {
            state = state.not()
            if (!state) {
                count--
            }
            if (count == 0) {
                this.cancel()
            }
        }
    }

    Text("$message $count", Modifier.padding(24.dp).scale(if (state) 0.5f else 1f))
}

@Preview(name = "Short", group = "Blinking", showBackground = true)
@Composable
fun PreviewBlinkingText() {
    BlinkingText("This is my blinking text", 3, 1000)
}