Compose Basics - MAD Skills

Intuitive: Thinking in Compose

You describe what your UI should contain, and Compose handles the rest.

With Views, you describe step by step, how to get your UI to look a certain way. You do this by defining UI in XML, find views from XML in code, and then calling setter functions to get your UI to look the way you want. With Compose, you no longer have to write XML. UI can entirely be described in code in Kotlin, taking full advantage of kotlin constructs.

Constructing UI bydescribing what, not how, is a key difference between Compose and Views. It's what makes Compose much more intuitive to work with.

example:

// SurveyAnswer.kt

@Composable
fun SurveyAnswer(answer: Answer) {
    Row {
        Image(answer.image)
        Text(answer.text)
        RadioButton(false, onClick = { /* ... */})
    }
}

In Compose, UI elements are functions, and not objects. This means you can't find a reference to them, and call methods to mutate them. Instead, UI elements are entirely controlled by the state or arguments that you pass. In example above, unlike in Views, the radio button dose't hold its own state that automatically changes to a user event. But rather, the radio button state is controlled by the values that are provided into it.

This is what we mean by what, not how. We're declaring what our UI should look like by providing the necessary states where our UI functions. But we're not telling Compose how it should render that state. So if state controls the UI, how do we go about updating state to update the UI?

In Compose, we do that through events. When a user interacts with the UI element, the UI emits an event such as on click. And the event handler can then decide if the UI state should be changed. If UI state changes, the functions, or UI elements, that depend on that state will be re-executed. This process of regenerating the UI when state chagnes is called recomposition.

The process of converting state into UI and state changes causing UI to regenerate is at the core of how Compose works as a UI framework.

Let's revisit our example and update our implementation, so that the radio button gets toggled when it's clicked.

// SurveyAnswer.kt

@Composable
func SurveyAnswer(answer: Answer) {
    Row {
        /* ... */
        var selected: Boolean = // ...
        RadioButton(selected, onClicked = {
            selected = !selected
        })
    }
}

This way, when it's clicked, it toggles the selection state. With this chagne, we should now see the radio button being toggled when clicking on it. In a production app, the state of the radio button should come from the answer state object.

Summary

1. Describe waht, not how

2. UI elements are functions

3. State controls UI

4. Events control State

Less code: Composable functions

With Jetpack Compose, you describe your UI in kotlin as functions. No more XML needed. Let's dive deeper into these functions and how you can build UI with them.

Let's see how we can build a single-choice question screen of jetsurvey, one of our compose samples. Earlier, we saw that a single answer in the survey can be written and composed as a function containing a row with an image, text, and a radio button. To create a UI componet in Compose, we must annotate a function with @Composable annotation. This annotation tells the Compose compiler that this function is intended to convert data into UI, so an answer into UI. 

Functions with this annotation are also called composable functions, or composables for short. This functions are the building blocks of UI in Compose. It's quick and easy to add this annotation, encouraging you to break your UI into a library of reusable elements. For example, if we wanted to implement a list of possible answers to select from, we can define a new function called SingleChoiceQuestion that takes in a list of answers and then call the SurveyAnswer function that we have just defined. This composable function accepts parameters, which allows to be configured by the app's logic. In this case, it accepts a list of possible answers, so that it can display these options to the UI. This function doesn't return anything, but instead it emits UI. Here, it emits the column layout composable function that's part of the Compose tollkit, which arranges items vertically.

// SingleChoiceQuestion.kt

@Composable
func SingleChoiceQuestion(answer: List<Answer>) {
    Column {
        answers.foreach {
            answer -> SurveyAnswer(answer = answer)
        }
    }
}

Within this column, it emits a survey answer composable function for each answer. Composables are also immutable. You can't hold a reference to them, like holding a reference to a single answer and later update its contents. You need to pass any and all information as parameters when you called it.

Notice that since this function is in Kotlin, we can use the full Kotlin syntax and control flow to produce our UI. Here, we're using forEach to iterate through each answer and call SureyAnswer to display them.

If we want to conditionally display something else, it's as simple as using an if statement. No view.visibility equals gone or invisible needed. You just call the composable if it needs to be displayed.

// SingleChoiceQuestion.kt

@Composable
func SingleChoiceQuestion(answer: List<Answer>) {
    Column {
        if (answers.isEmpty()) {
            Text("There is no answers to choose from!")
        } else {
            answers.foreach {
                answer -> SurveyAnswer(answer = answer)
            }
        }
    }
}

This function is also fast and free of side effects. It must behave the same way when called multiple times with the same argument. And it shouldn't modify properties or global variables. We say that functions with this property are item potent. This property is necessary for all composable functions, so that UI can be emitted correctly when functions are re-invoked with new values. Notice that the parameters provided to the function entirely control the UI. This is what we mean by transforming state into UI.

The logic in the function will guarantee that the UI can never get out of sync. If the list of answers changes, then a new UI is generated from te new list of answers. By executing this function again and redrawing the UI if necessary. This is called recomposition.

Recomposition happens when a composable is re-invoked with different function parameters. But it also happen when internal state in the function changes.

For example, say the SurveyAnswer composable accepts a parameter if the answer is selected or not. Initially, no answer is selected. In the view world, interacting by tapping on one of the answer UI elements would visually toggle it. But in Compose, since we passed False to all SurveyAnswer composables, all answers would remain unselected even when tapped.

// SingleChoiceQuestion.kt

@Composable
fun SingleChoiceQuestion(answers: List<Answer>) {
    var selectedAnswer: Answer? = null
    answers.forEach {
        answer -> SurveyAnswer(answer = answer, isSelected = false)
    }
}

We need to make the composable recompose whenever the user interacts with the UI. To do that, we need a variable that holds the selected answer. But this isn't enough. We also need to be able to tell Compose that when this variable changes, recomposition should happen as well.

To do that, we need to wrap the answer in a mutable state object. A mutale state is an observable type that is integrated within the Compose runtime. Any changes to the state will automatically schedule a recomposition for any composable functions that read it.

// SingleChoiceQuestion.kt

@Composable
fun SingleChoiceQuestion(answers: List<Answer>) {
    var selectedAnswer: MutableState<Answer> = rememberSaveable { mutableStateOf(null) }
    answers.forEach {
        answer -> SurveyAnswer(
            answer = answer,
            isSelected = (selectedAnswer.value == answer)
        )
    }
}

To create a mutable state, we can call the mutableStateOf method. Since no answer will be selected initially, we initialize this to null. We also need to update the value of isSelected to compare the current answer to the selected answer. Since selected answer is of type MutableState, we have to use the value property to get the selected answer. When this value changes, Compose will automatically re-execute the SurveyAnswer composable so that the selected answer is highlighted.

Creating a state object also requires that it be called within a call to remember. This guarantees that the value is remembered and not reset when the composable recomposes. With what we have now, while the UI survives recomposition, it won't survive configuration changes. To remember values across config chagnes, we can use rememberSaveable.

// SingleChoiceQuestion.kt

@Composable
fun SingleChoiceQuestion(answers: List<Answer>) {
    var selectedAnswer: Answer? by = rememberSaveable { mutableStateOf(null) }
    answers.forEach {
        answer -> SurveyAnswer(
            answer = answer,
            isSelected = (selectedAnswer == answer)
        )
    }
}

Right now, the type of the variable is a MutableState. But we can use Kotlin's delegated property syntax by using the by keyword. Doing so chagnes the type of our variable to an optional answer. This syntax is nice, as we can work directly with the value of the underlying state. No more calls to the value property on a MutableState object.

// SingleChoiceQuestion.kt

@Composable
fun SingleChoiceQuestion(answers: List<Answer>) {
    var selectedAnswer: Answer? by = rememberSaveable { mutableStateOf(null) }
    answers.forEach {
        answer -> SurveyAnswer(
            answer = answer,
            isSelected = (selectedAnswer == answer),
            onAnswerSelected = {answer -> selectedAnswer = answer}
        )
    }
}

With our newly inroduced state, we can pass a lambda function for the onAnswerSelected paramter, so we can perform an action when the user makes a selection. In the definition of this lambda, we can set the value of selected answer to the new one.

Events are the mechanism for how state is updated in Compose. Here, the event onAnswerSelected would be invoked when the user interacts by tapping an answer. The Compose runtime automatically keeps track of where state reads occur so that it can intelligently recompose composables that depend on that state. As a result, you don't need to explicitly observe state or manually update the UI. 

There are some other behavioral properties of composable functions you should be aware of. Because of these behaviors, it's important that your composable functions are side-effect free and behave the same when called multiple times with the same argument.

First, composable functions can execute in any order. Looking at the code for a composable, you might assume that the code runs in order. But this isn't necessarily true. Compose can recognize that some UI elements are higher priority than others. So those elements might be drawn first.

// ButtonRow.kt

@Composable
fun ButtonRow() {
    MyFancyNavigation {
        StartScreen()
        MiddleScreen()
        EndScreen()
    }
}

Say, for example, you have code that draws three screens in a tab layout. You might assume that the StartScreen is executed first. However, these executions can happen in any order.

Composable functions can also run in parallel. This lets Compose take advantage of multiple cores, which improves the rendering performance of a screen.

// ListComposable.kt

@Composable
fun ListComposable(myList: List<String>) {
    Row(horizontalArrangement = Arrangement.SpaceBetween) {
        Column {
            for (item in myList) {
                Text("Item: $item")
            }
        }
        Text("Count: ${myList.size}")
    }
}

In this code snippet, the code runs side-effect free and transforms the input list to UI.

// ListComposable.kt

@Composable
fun ListComposable(myList: List<String>) {
    var items = 0
    Row(horizontalArrangement = Arrangement.SpaceBetween) {
        Column {
            for (item in myList) {
                Text("Item: $item")
                items++ // Avoid! Side-effect of the column recomposing
            }
        }
        Text("Count: $items")
    }
}

But if the function writes to a local variable such as in the snippet, this code is no longer considered side-effect free and may result in odd behaviors in your UI.

Recomposition skips as much as possible. Compose does its best to recompose only portions of the UI that need to be updated. If a composable doesn't use the state that triggered recomposition, then it will be skipped.

// GreetingScreen.kt

@Composable
fun GreetingScreen(name: String) {
    Column {
        Header()
        Greeting(name = name)
        Footer()
    }
}

In the snippet, if the name string changes, the header and footer composables will not be re-executed since it doesn't depend on that state.

Recomposition is optimistic. This means that Compose expects to finish recomposition before the parameters change again. If a parameter does change before recomposition finishes, Compose might cancel the recomposition and restart it with a new parameter.

Lastly, composable functions might run frequently. This might be the case if your composable function contains an animation that needs to be executed for every frame. This is why it's important to make sure your composable functions are fast to avoid dropped frames.

We coverd a lot. So let's do a quick summary.

Summary

1. Create composables using the @Composable annotation

2. It's quick & easy to create composables

3. Composables accept parameters

4. Use MutableState and remember(rememberSaveable)

5. Composables should be side-effect free

6. Composable can:

        6.1 Execute in any order

        6.2 Run in parallel

        6.3 Be skipped

        6.4 Run frequently

Poswerful: Compose toolkit

So far, you've learned the basics of thinking Compose and how to create a composable function. One thing you might have noticed is that we use components like row, text, and radio button. But we've got so much more for you.

Compose actually ships with a powerful toolkit of UI components out of the box, enabling you to build rich UIs and interactions. Previously, we've looked at how to build a portion o the single-choice question screen in the jetsurvey sample app. Let's see how we can build the rest of the screen to match our design specs, using ghe Compose toolkit.

First, let's talk about one of my favorite topics, styling. Jetpack Compose makes it easy to give your app a consistent look and feel by shipping with an implementation of Material Design from the start.

Material Design is an adaptable system of guidelines, components, and tools that support the best practices of user interface design. Jetpack Compose supports Material Design 2 and 3, the next evolution of Material Design. Material 3 features updated components and Material You customization features, designed to match the new look and feel on Android 12 and above. 

MaterailTheme(
    colorScheme = MyAppsColorScheme,
    typography = MyAppsTypography,
    shapes = MyAppsShapes
) {
    // Content goes here
}

With Material Design, your app can be themed to match your brand by providing custom colors, typography, and shapes -- all in one place. Let's see how jetsurvey uses material theming to style the whole app.

@Composable
fun JetsurveyTheme(
    useDarkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    var colors = if (!useDarkTheme) {
        LightColors
    } else {
        DarkColors
    }

    var systemUiController = rememberSystemUiController()
    DisposableEffect(systemUiController, useDarkTheme) {...}

    MaterialTheme(
        colorScheme = color,
        shapes = Shapes,
        typography = Typography,
        content = content,
    )
}

The jetsurvey code base defines a new composable function, JetsurveyTheme. Here we can see that it uses the MaterialTheme composable function and provides custom values for colors, shapes, and typography. Different color values are provided depending if the system appearance is set to dark or light. This allows the app colors to respond automatically when the system theme is changed by the user.

val = Typography = Typography(
    // Display Large - Montserrat 57/64 . -0.25px
    displayLarge = TextStyle(
        fontFamily = MontserratFontFamily,
        fontWeight = FontWeight.W400,
        fontSize = 57.sp
        lineHeight = 64.sp
        letterSpacing = (-0.25).sp
    ),
    
    ...
)

...

Different fonts and styles are set, like weight, size, and line height. This allows text to appear and be styled consistently. 

val Shapes = Shapes(
    extraSmall = RoundedCornerShape(12.dp),
    small = RoundedCornerShape(12.dp),
)

And lastly, shape value is provided, which allows customization of rounded corners for different sizes of shapes in the app. 

To use this custom theme, the theme function should be the outermost function that we invoke. In the survey fragment file, we can see that it is the first line called within set content of the Compose view -- a view that can host composable functions. All the contents within it would then use all the shapes, typography, and colors defined in the theme.

A fundamental Material Design component that is part of the toolkit is called scaffold. A scaffold is a basic layout for arranging material components in common patterns, such as the screen with a small top app bar and floating action button.

From high level, we can see that the survey screen conforms to a scaffold. It has a top app bar, contents, and a bottom bar.  Let's see how the scaffold layout is used in the jetsurvey code base. 

@Composable 
fun SurveyQuestionsScreen(
    question: SurveyState.Question,
    shouldAskPermissions: Boolean,
    onDoNotAskForPermissions: () -> Unit,
    onAction: (Int, SurveyActionType) -> Unit,
    onDonePressed: () -> Unit,
    onBackPressed: () -> Unit
) {
    val questionState = remember(questions.currentQuestionIndex) {
        questions.questionsState[questions.currentQuestionIndex]
    }
    
    Surface(modifier = Modifier.supportWideScreen()) {
        Scaffold(
            topBar = {
                SurveyTopAppBar(
                    questionIndex = questionState.questionIndex,
                    totalQuestionCount = questionState.totalQuestionCount,
                    onBackPressed = onBackPressed
                )
            },
            content = { innerPadding ->
                AnimateContent(
                    targetState = questionState,
                    ...
                )
            },
            bottomBar = { ... }
        )
    }
}

Survey screen file defines a composable function called SurveyQuestionsScreen. This is the composable that shows all the questions in the survey. Here we can see that it declares a scaffold and  provides a couple of parameters to it. First, it provides a value for the top bar parameter, which is composable function lambda representing the top app bar for the screen. Here we can see that it invokes a call to SurveyTopAppBar, which will render the user's progress in the survey. 

Next, it provides a value for the content parameter, which is also a composable function lambda. The content rendered shows the current question, the survey, the user needs to answer.

And lastly, it provides a value for the bottom bar, which, again, is a composable function lambda, making a call to SurveyBottomBar which will render the Next and Previous buttons, so the user can progress through the survey.

Notice that the scaffold is contained within a surface composable function. Let's take a look at that next.

Another fundamental component in Material Design is the concept of a surface.

Surface(
    color = MaterialTheme.colorScheme.surface,
    shape = RoundedCornerShape(8.dp),
    border = BorderStroke(2.dp,
        MaterialTheme.colorScheme.surfaceVariant
    ),
    shadowElevation = 8.dp,
    tonalElevation = 8.dp
) {
    Text("Hello Compose")
}

A surface is a central metaphor in Material Design which content sits on. A surface has a color, a shape, a border, as well as tonal and shadow elevation. 

Apart from surface, chances are you're going to need a lot more components to build any kind of UI you can think of. This is why Compose provides a bunch of other material design components out of the box, like top app bars, buttons, cards, switches, chips, divides, floating action buttons, and a whole lot more. Understanding and building in ways that cater to the diverse needs of your users can be a challenge. To help with this, Material 3 components are also all accessible by default, featuring acceptable color contrast ratios and minimum touch size and more.

And that's only scratching the surface on Material Design. You can check out the Compose Material 3 API reference and the Material Design 3 website at the URLs shown here to learn more: goo.gle/compose-material-refm3.material.io

We've looked at how to arrange components on a single answer in the survey by using the row composable function. But how do we arrange all the different components for the rest of the screen? We do that by using layout.

Row {
    Component1()
    Component2()
    Component3()
}

Compose has three standard layout elements. As we've seen, to horizontally arrange items, we can wrap our UI components in a row composable function. On the other hand, if we want to vertically arrange items, we would use the column coposable function. Another common layout is box.

Box is what you want when you want to put or stack elements on top of another. For example, if you wanted to show an image on the bottom right corner, stack on top of another element. These standard layouts also accept parameters, so you can further control how items are placed. 

Taking our current implementation of survey answer, if we want to try to run this, an answer in the survey would actually look something like this(odd). To change that, let's pass in some additional parameters to the row.

@Composable
fun SurveyAnswer(answer: Answer) {
    Row(
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.SpaceBetween
    ) {
        /* ... */
    }
}

To change the vertical alignment of items in the row, let's pass Alignment.CenterVertically for the vertical alignment parameter. With this change, we should now see the items vertically centered. Also if we want to change the horizontal arrangement of items, we can pass a value like Arrangement.SpaceBetween, which will place children so that they are spaced evenly acoss the row. We won't see much of an effect yet with this change, but we'll see why this matters in a bit.

There so much more to lay out with layouts, so make sure to check out the documentation pages to learn more: goo.gle/compose-layout-docs

So far, we've covered styling, components, and layouts.

But how do we control things like sizing, padding, and other appearances? You do that by using modifiers. Modifiers allow you to decrate or augment a composable. Every composable function in the Compose toolkit accepts a modifier as a parameter. Modifiers can be chained together until you achieve your described customization.

Text(
    "Hello Compose!",
    Modifier.background(Color.Magenta)
        .size(200.dp, 30.dp)
        .padding(5.dp)
        .alpha(0.5f)
        .clickable {
            // Called when Text clicked
        }
)

Modifiers allow you to do all sorts of things, like set a background color, change the size, add padding of an element, change the alpha of an element. Modifiers allow to process inputs, like making an element clickable. Additional modifiers might also be available to you, depending on the scope of a composable function.

Box(Modifier.size(150.dp)) {
    Text("Hello Compose"),
    Modifier.align(
        Alignment.BottomEnd
    )
}

For instance, if you are within the scope of a box, the Align modifier allows you to position an element relative to the containing box. Here, we're changing the alignment of the text element to be bottom end, so that it aligns the bottom right corner of the containing box. This modifier wouldn't be accessible outside of the box composable and its scope, which makes modifiers type safe. 

Using modifiers, we can further customize the survey answer composable to match our designs. Let's go ahead and do that.

@Composable
fun SurveyAnswer(answer: Answer) {
    Surface(
        border = BorderStroke(
            1.dp
            MaterialTheme.colorScheme.outline
        ),
        shape = MaterialTheme.shape.small
    ) {
        Row(
            Modifier.fillMaxWidth()
                .padding(16.dp),
            /* ... */
        ) {
            Image(answer.image)
            Text(answer.text)
            RadioButton(/* ... */)
        }
    }

}

I'm assuming here that the composable is called within our app's theme, so that the typography, shapes, and colors match our brand. First, let's further customize the row by passing in a modifier. Since we want the item to occupy the maximum alloted horizontal space, let's call fillMaxWidth. The item has a padding of 16 DP, so let's chain it padding modifier. These modifications are answer item is looking a lot closer to our desired output. Notice, though, there are a few more styling tweaks that we need to make.

To add a border around our item and change its shape, let's wrap the row composable around a surface composable. Doing so, we can change the shape. Let's set that to small. And we can change the border by setting it to 1 DP, as well as its color. And that's it.

We've only covered a few modifiers, and there are bunch more that you can use to modify your composable functions: goo.gle/compose-modifiers  goo.gle/compose-modifiers-list

So make sure to check out the documentation pages by following this link to learn more.

Accelerate development: Compose tooling

Compose allows you to build beautiful UIs quickly. But together with the Android Studio tooling support, you're able to accelerate the development process even further with faster iteration and better debugging.

Previously, we implemented a single answer option in the Jet Survey app. Let's see how Android Studio can help us write this efficiently. Let's go ahead and open the Jet Survey project in Android Studio and see what features it has in store for us. To start writing our composable, simply write C-O-M-P and press Tab or Return to automatically generate the composable.

Let's call it SurveyAnswer. Hit Return and start implementing it. To implement the row inside our survey answer, we simply type W-R and press Tab or Return to generate the row. Similarly, if you want to wrap a component around a row or another layout, you can right-click the component, select Show Context Actions, Surround With Widget, and then Surround With Row. These shortcuts are called life templates that help you generate common code snippets. Check all of them in your Android Studio preferences under live templates. 

@Composable
fun SurveyAnswer() {
    Row {
        Image(pointerResource(id = R.drawable.lenz), contentDescription = "")
    }
}

Next, we can implement the content of our row. We start by adding an image that displays the lens drawable. We'll use the painter resource API to help load this.

One of the challenges when working with drawables is that it's hard to know what the drawable looks like. But as you can see, the image shows an icon in our editor's gutter. This makes it easy to quickly preview and switch to another image. Let's go ahead and switch the image to display spark.

Next, we implement the row's text. And let's give it a custom color. You can see the color showing up in the gutter. Clicking it gives us a color picker then we can use to quickly change the color. Here, you can enter RGB, hex, or easily pick colors out of the material color palette, which is quick and handy.

Finally, we implement the Radio button. Live templates and gutter icons help us write code while making fewer mistakes. Now let's see how we can quickly iterate on our UI using previews.

With our basic composable function, it's time to see how this composable actually looks. It would be great to be able to see our composable while we work on it without having to run the whole app on a device. This is made easy by creating a preview composable. 

Simply type P-R-E-V and press Tab or Return. This generates a composable function with the extra @preview annotation. Let's call it SurveyAnswerPreview and tell it to render our SurveyAnswer composable. Something to keep in mind is that composable functions annotated with the app preview annotation cannot accept arguments, so we must provide all the data needed to preview a composable. Let's also wrap this in our app custom theme.

The preview annotation tells Android Studio that this composable should be shown in the Design View of this file. So when we click the split icon at the top and we press build and refresh, our composable preview shows up.

Using the cog wheel in the gutter, we can specify all kinds of properties of this preview. For example, I can show the preview with a certiain background color using night mode. You can add the preview annotation to the same function multiple times, so we could also choose to add another preview that shows the form item with an extra large font. Closing this pop up should then reflect all the changes that we made into our code. 

Oftentimes, you want to see how your composables look like in differenting configurations -- in light or dark mode, in different font sizes, and so on. With the multipreview feature in Android Studio you can define an annotation class that itself has multiple preview annotations associated with it. Let's take a look.

import android.compose.ui.tooling.preview.Preview

@Preview(
    name = "Small font",
    group = "Font scales",
    fontScale = 0.5f
)
@Preview(
    name = "Large font",
    group = "Font scales",
    fontScale = 1.5f
)
annotation class FontScalePreview

To make a multipreview annotation in a file, define a custom annotation class with a desired preview configurations. Here we're adding a small font and large font version. You can also add another annotation for displaying both dark and light mode. Then simply add that custom annotation to your preview composable. And the different previews will be shown automatically. If you want to see how this preview behaves on an emulator or physical device, we can use the Run icon in the gutter. Instead of following the previous configuration parameters, this will use the configuration of your device. When you update a composable, you have to build and refresh the preview or redeploy to your device.

With live edit of literals, rebuilding isn't necessary for certain constant iterals like integers, strings, and colors. For example, if we update the name of our super hero, we see that change is being updated immediately. Similarly, we can change the color of the text without needding to rebuild. Live edit takes this behavior even further and lets you adapt the contents of a composable function without needing to rebuild. Working on our form ite,, we can add modifiers, properties, or even remove or add child composables. All these changes are automatically reflected on your device.

Live edit is a new experimental feature. So right now, it's not available by default. Read the documentation to learn how you can enable it for your project: goo.gle/compose-live-edit-docs

With previews, multipreview, live edit of literals, and live edit, iterating on your design is a breeze. But what if there's something wrong with your composable and you need to debug it? Android Studio allows you to dig deeper so you can figure out what's wrong.

First, let's open up our Layout inspector. We have to select the process we're interested in. So let's select the Jet Survey app running in our emulator. It shows us the current screen of the app. On the left is a component tree that contains all the nodes in our UI hierarchy. We can expand all of them at once, and we can see what our composables look like. This tree can get quite big. So we can right click on any node in the tree and show only its parents or its subtree to simplify our view. On the right, we see the attributes for each of the composables in our node. For lambda attributes, we can also see a link to the specific line in code where the lambda is defined, which helps a lot when debugging. while implementing a design, we can overlay a picture on top of the layout, which helps to verify if our design implementation is pixel perfect. In our case, we can see that there's still some more work to be done.

From the previous episode, you know that using components from the Compose toolkit will give us that Pixel perfection. I'll leeave that as an exercise for you to do.

And that's it. That covers a lot of the helpful tooling provided by Android Studio to help us develop apps much faster in Compose. 

Summary

1. Live templates

2. Gutter icons

3. @Preview

4. Live edit of literals

5. Live edit

Community tip

goo.gle/compose-pathway

goo.gle/compose-samples

...

derived by videos of @AndroidDevelopers in Ytb

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值