Jetpack Compose Concepts Every Developer Should Know

作者:Clinton Teegarden

The original link

What is a composer

Jetpack Compose is a new declarative UI framework for Android. Android developers have long been familiar with writing UIs in XML, moving through the hierarchy of views to update those stateful views. Jetpack Compose, on the other hand, writes a stateless UI using Kotlin’s functions.

The Composable function is annotated by @composable. Composable functions must have this annotation so that the compiler knows that the function is adding UI to the view hierarchy. The Composable functions themselves can only be called by other Composable functions, but they can call other standard functions themselves.

If you haven’t already started using Compose, I highly recommend you check out the Compose learning kit here. It provides some basic details and examples to get you started quickly.

Single data flow

Compose is composed based on a single data stream. Therefore, the programming model of Compose has a large requirement for proper implementation. Unlike the traditional Android UI architecture, the Composable function is relatively stateless — that is, the presentation state of the UI should be passed in with its own parameters.

In most basic scenarios, events, whether coming from the UI (button clicks, text input, etc.) or elsewhere (API calls, callbacks, etc.) are handled by handlers and passed to the Composable function to update the UI state. Since the Composable is stateful, the UI is built based on the provided UI state.

In the diagram above, the UI layer is a Composable. Events originate in this layer, such as button clicks, and are passed to Event handlers, such as ViewModel. ViewModel controls UI state through LiveData/StateFlow. As the status changes, these updates are pushed to the Composable for reorganization with the latest updated status.

The following code demonstrates the above individual data flow — collecting state from the ViewModel, sending events to the ViewModel, and then updating the state through the ViewModel.

MVI_ComposeTheme {
    Surface(color = MaterialTheme.colors.background) {
        valstate = viewModel.viewState.collectAsState().value Button(onClick = { viewModel.processEvent()}) { Text(state.message) } }}Copy the code

Combination and recombination

For users composition is the process of Composable function execution and UI creation. Reorganization is the process of updating the UI as the state and data that the Composable is used to present changes. During the reorganization process, Compose knows which Composable is using the changed data and only updates the changed UI components. Other invariant composables are skipped.

Over the life cycle, combination and recombination are actually different.

  • The Composable function may be composed for a frame, such as an animation
  • The Composable functions may be executed in any order
  • The Composable function may execute in parallel

This means that you can’t include the logic that you do when executing in a composite function — sometimes called a side effect.

Compose_Theme {
    MainScreen()
    
    // DO NOT DO THIS
    viewModel.makeAPICall()
}
Copy the code

The stateful Composable bids farewell to the saved instance state

Although the goal is to make the Composables primarily stateless, there are times when parts of the composables need to be stateful, such as remembering the scrolling state, sharing a variable between the composables, and so on. Since regrouping can happen every frame, it would be bad if every regrouping lost the scrolling position.

We can record this value in the Composable by creating a variable:

val myInt = remember{ Random(10).nextInt() }
Copy the code

In the example above, the randomly generated integer is remembered during the composition process and no calculation is required. If the integer is not wrapped by Remember, it will be recalculated at each reorganization.

Furthermore, we can remember when the configuration changes.

val myInt = rememberSaveable{ Random(10).nextInt() }
Copy the code

Sometimes we need a variable to remember when an update of one Composable results in a reorganization of another. In the following example, button clicking increases the counter, which is displayed by the Text Composable. So, when the button is clicked, we want the Text display to change as the calculation increases.

To do this, we’ll use the Compose remember function as in the example above, but the integer will be wrapped in the MutableState object. The MutableState class holds a single value, which is listened on by Compose and causes the associated Composables to be recomposed.

Column{
    // create state for buttonCount and a function to update it -      
    // setButtonCount
    val (buttonCount, setButtonCount) = 
       rememberSaveable { mutableStateOf(0) }
    Button(onClick = {
        setButtonCount(buttonCount + 1)
    }) {
        Text(text = "Press Me!")}// recomposes whenever button is pressed
    Text(text = "Button Pressed $buttonCount")}Copy the code

Slot API

Compose introduces the concept of a slot API. This allows Composable to be highly customizable without requiring the Composable function to provide a large number of implementations for a variety of custom UIs. Since each scenario and implementation may be different, the slots API supports customising the UI with empty slots in the Composable.

For example, there are many buttons that do not display text, some that display loading bars, some that display ICONS on the left, and some that display ICONS on the right. The slot API supports this diversity of custom composables.

Other Composables, like Scaffolds, are implemented entirely with slots. Imagine building your UI with various UI components with these reserved intervals, toolbar, bottomNav, Drawer, Screen Content, and so on.

Modifier (Modifier)

The Modifier can be compared to an XML attribute, which is usually used to set the style of the UI. But the modifier is simpler, and there are some strange techniques. Modifier allows you to decorate and modify the Composable’s default implementation. Modify the appearance, add accessibility information, handle UI interaction events and so on can be achieved through the modifier. Since modifiers are only the object of Kotlin, you can also add them to custom modifiers.

Text(text = "Your Text", modifier = Modifier.padding(5.dp))
Copy the code

Modifier is very powerful because it provides the possibility to implement the Composable layer without nesting it to other Composables. For example, in the following example, it is impossible to implement a traditional Android UI without having a lot of views nested.

However, with the Modifier with a Composable can be achieved. This is because the Modifier is in order, using the padding and color in different order to achieve different UI combinations.

Text(text = "Fake Button",
    modifier = Modifier.padding(5.dp)
        .background(Color.Magenta)
        .padding(5.dp)
        .background(Color.Yellow))
Copy the code

Lazy List

LazyLists are a RecyclerViews like Compose. I’ll tell you, I won’t miss writing RecyclerView Adapters, ViewHolders and other template code. The following example shows the column form of the LazyList (scrolling vertically) showing different UI elements based on odd and even numbers. If we use RecyclerView, we need one Adapter, at least two different ViewHolder. For Compose, we only need a LazyColumn column to dynamically add the Composable function of the content.

val listSize = 100
LazyColumn {
    items(listSize) {
        if (it % 2= =0) {
            Text("I am even")}else{
            Text("I am odd")}}}Copy the code

That’s it. This is probably the happiest thing about using Compose.

Constraint Layout

Compose has its own version of the constraint layout that we are familiar with and love from traditional UIs. Constrained layout is highly recommended in traditional UI systems because it allows for a highly customizable UI layout with respect to other views without nesting. With Compose, nesting is no longer an issue, but sometimes tools such as constraints, barriers, weights, and so on are needed to provide it.

ConstraintLayout {
    // Creates references for the three Composables
    val (button1, button2, text) = createRefs()
    Button(
        // constraintAs is like setting the ID, required.
        modifier = Modifier.constrainAs(button1) {
            top.linkTo(parent.top, margin = 16.dp)
        }
    ) {
        Text("Button 1")}// constraintAs is like setting the ID, required.
    Text("Text", Modifier.constrainAs(text) {
        top.linkTo(button1.bottom, margin = 16.dp)
        centerAround(button1.end)
    })
   // Create barrier to set the right button to the right of the             
   button or the text, which ever is longer. 
   val barrier = createEndBarrier(button1, text)
    Button(
        modifier = Modifier.constrainAs(button2) {
            top.linkTo(parent.top, margin = 16.dp)
            start.linkTo(barrier)
        }
    ) {
        Text("Button 2")}}Copy the code

Compose and Navigation

The Navigation in Jetpack Compose can implement a lot of the Jetpack Navigation that we are used to. But Jetpack Compose allows us to Navigate different pages within the same Activity without requiring multiple fragments.

Just create a NavHost and embed a Screen Composable into it.

val navController = rememberNavController()
NavHost(navController = navController, startDestination = "home") {
    composable("home") {val vm: HomeVM = viewModel()
        HomeScreen(vm)
    }
    composable("settings") {val vm: SettingsVM = viewModel()
        SettingsScreen(vm)
    }
    composable("profile") {val vm: ProfileVM = viewModel()
        ProfileScreen(vm)
    }
}
Copy the code

Through androidx. Lifecycle: lifecycle – viewmodel – compose can create viewmodel within Composable:

val vm: MyVM = viewModel()
Copy the code

Viewmodels created within the Composable remain until their scope (Activity/Fragment) is destroyed. This allows you to work with the NavHost Screen as you do with fragments now. In my case, I created the ViewModel in NavigationHost, so this is an example for you to see how it works.

To apply NavHost to BottomNav or any other Navigation component, you can use Scaffold:

Scaffold(
    bottomBar = {
        BottomNavigation {
            // your navigation composable here
        }
    },
) {
    NavHost(
      navController = navController, 
      startDestination = "home") {
        // your screen composables }}Copy the code

summary

The above concept is just an introduction that Compose provides us. Compose is a big departure from the way Android developers often build uIs, but it’s a nice change to see because it greatly simplifies many of the problems of traditional UI architectures. If you haven’t already started using Compose, I highly recommend you check out the Compose materials here. It’s long, but every word is worth your time. Have a pleasant Compose trip.