Compose, which was first announced at Google IO in 2019, was a bit of a bumper. it’s hard to overwrite the 10 year old system, and what about future relationships with the likes of Flutter?

In just two years, it was announced at the IO conference that Compose 1.0 is coming. In fact, Google’s commitment to Compose is evident in the Beta campaign for the company earlier this year. For example, it’s possible to switch between a programming language and a UI framework in a short period of time. So there’s no doubt that Compose will become the new UI development standard.

With the stable release coming, now is a great time to learn about Compose. Let’s take a look at the content on GoogleIO and see what changes Compose will bring to Android development


1. Why use “Compose”?


Jetpack Compose is Android’s modern toolkit for building native UI.

This is the official definition of Compose. You can see why Compose is more “modern” by comparing it to Android’s existing view system

The current Android architecture hasn’t changed much since 2010, and it has changed a lot in terms of hardware specs and APP complexity over the past decade, making it a bit of a throwback.

In recent years, the emergence of React and other declarative frameworks has changed the way of development of the front end. Android just borrowed the ideas of React to develop Jetpack Compose, a declarative UI framework. Using Compose can significantly reduce the time of page creation and improve the efficiency of UI development.


2. Declarative VS imperative


The current Andoird view system belongs to the traditional imperative development mode, which generally uses XML layout, and then obtains the control reference through findViewById to update the state and refresh the UI imperatively. Imperative view architecture has the following features:

  • The UI is mutable: the control accepts commands and refreshes the UI by changing itself
  • UI holds State: Controls change by changing their State

As the interface becomes more and more complex and there are more and more controls, it is difficult to keep the State of each control synchronized, and the Bug of inconsistent UI display occurs frequently. A lot of effort goes into how to update all of the updated controls accurately and without missing anything.

Declarative UIs have the opposite characteristics of imperative UIs and can compensate for imperative’s shortcomings:

  • UI is immutable : @ComposableThe function does not return any referencable handle and cannot be changed by the outside world.
  • UI does not hold State: @ComposableFor functions that cannot hold state, the data displayed must be passed in as arguments.

A declarative UI runs as a “pure function” that reexecutes to refresh the UI when State changes.

KeyPoint: Compose uses the @composable function to build the UI to better implement the declarative UI features


3. Kotin-based DSL


Declarative UIs need to be supported by a matching DSL language, such as JSX in React. With Android embracing Kotlin in full force today, Kotlin DSL is pretty much the only option, but the good news is that Kotlin DSL is powerful enough to use.

KeyPoint: The process of assembling the UI using a DSL is essentially the process of defining the @composable function.

@Composable
fun MessageList(messages: List<String>) {
    Column {
        if (message.size == 0) {
            Text("No messages")}else {
            message.forEach { message ->
                Text(text=messag)
            }
        }
    }
}
Copy the code

In the example above, MessageList is a UI component that displays a list of messages, with the message parameter being the displayed data. DSLS allow us to intuitively write multi-layered nested UIs, such as columns, texts, and so on in MessageList.

The Kotlin DSL is Turing-complete, allowing us to build the UI while simultaneously adding logic to display “NO message” when there is NO message. This is something that a tokenized DSL such as JSX cannot do.

When the message changes, the MessageList is reexecuted, a process called recomposition. Composee’s UI is refreshed through constant reorganization.


4. High-performance restructuring


When data changes, reorganizations are triggered, and there are concerns about whether extensive reorganizations will affect performance.

Compose uses a similar idea to React VirtualDom to implement local refreshes via diff to improve performance. In contrast, Compose does not use a tree structure, but diff on a linear structure such as Gap Buffer. But it’s essentially the same. You can think of the Gap Buffer as a DFS processed array of tree structures, and the element of the array marks its position in the tree by key.

Compose generates a key with location information for the Composable at compile time and stores it in the corresponding location of the Gap Buffer array. The key can be used by the runtime to determine whether the Composable node has changed its position to participate in the reorganization.

The Gap Buffer also records the State (or Parameters) associated with the Composable object, and only when the associated State changes will the Composable participate in the reorganization and the function be re-executed.

The KeyPoint: Compose compiler ensures that the Composable avoids unnecessary recomposing as much as possible, which helps improve Compose’s redrawing performance


5. The State management


The core of Compose is the state-driven UI, and the UI is the product of the State changes. In the traditional view system, State is just a property of the UI control, and once the UI is created, it persists.

KeyPoint: Traditionally, State is subordinated to the UI, and in Compose, the UI is subordinated to State

To better understand the relationship between State and UI, look at an example of a Checkbox:

@Composable
fun MessageList(messages: List<String>) {

    Column {
    
        var selectAll by remember { mutableStateOf(false} Checkbox (checked = selectAll, onCheckChange = {checked -> selectAll = checked})... }}Copy the code

SelectAll is a state that the CheckBox can refer to and modify. If the Checkbox is checked, the selectAll changes and the Checkbox is reorganized and updated.

In contrast to the Checkbox recombination, selectAll can persist across the recombination through remember {}. Composable draws heavily on React Hooks, such as remember{}, which uses useMemo().


6. Data flows in one direction


The selectAll state changes, the Checkbox restructures, and the data always flows one-way from top to bottom. As we zoom in on the range, the data for MessageList is also passed in from the upper layer by parameter

@Composable
fun ConversationScreen(a) {
    val viewModel: ConversatioinViewModel = viewModel()
    val message by viewModel.messages.observeAsState()
    MessageLit(messages)
}

@Composable
fun MessageList(message: List<String>){... }Copy the code

Compose can be used with existing Jetpack components such as ViewModel, LiveData, etc. For a standard Jetpack MVVM project, it would be easy to replace the UI part with Compose.

The Composalbe calls viewModel() to obtain the viewModel of the current Context, and observeAsState() converts LiveData to Compose State and establishes the binding. When the LiveData changes, the ConversationScreen is reorganized and the MessageLit and MessageItem are reorganized because they depend on the messages parameter.

All the sub-composalbe data comes from the top-level ScreenState, which is called the Single Source Of Truth. We can infer the current state of the UI simply by paying attention to ScreeState, which is also good for testing.

KeyPoint: Composable data flows one-way from top to bottom, with all data coming from a single trusted source at the top level.


7. Side effects and Lifecycle


We can also manipulate the data directly in the Composalbe without using the ViewModel. However, data manipulation involves IO and should not be repeated with the reorganization. It should be treated as a SideEffect.

@Composable
fun ConversationScreen(a) {
    
    var message = remember { mutableStateOf(emptyList()) } 
    val scope = rememberCoroutineScope()
    
    SideEffect {
        scope.launch {
            message = apiService.getMessage()
        }
    }
    
    MessageLit(messages)
}

Copy the code
  • SideEffect {... }Handle side effects once when the Composable is first displayed on the tree and not repeated with the reorganization.
  • rememberCoroutineScope()You can get the current Composalbe associated withCoroutieScopeWhen the Composable is removed from the tree, its coroutines are cancelled.

LaunchedEffect can also be used directly when side effects are used in coroutines, making it easier to:

//LaunchedEffect provides a CoroutineScope to launch coroutines directly
LaunchedEffect {
    message = apiService.getMessage()
}
Copy the code

Here’s the definition of the Composalbe lifecycle:

  • Enter: Mount to the tree. Display for the first time, similar to the React Mounting
  • Composition: Retooled the UI, analogous to React Updating
  • Leave: Removed from the tree and no longer displayed, similar to React Unmonting

Composalbe introduces a lifecycle to facilitate processing of logic that is not pure (logic that cannot be executed repeatedly following reorganization), and Compose provides functions such as SideEffect{} to handle this logic

The closer the Composalbe is to pure functions, the easier it is to reuse, so the fewer side effects, SideEffect, LaunchedEffect, and so on, the better. It is recommended to move them into the ViewModel as much as possible.


8. Fully functional UI system


KeyPoint: Compose’s current UI system is fully functional and covers all the capabilities of Android’s existing view system.

Various UI components

All common UI components can be found in Compose, and even Material Designe controls like Card, Fab, and AppBar are available right out of the box.

Card {
    Text("Card Content")
}

FloatingActionButton() {
    Icon(Icons.Filled.Favorite)
}

TopAppBar(
    ...
)
Copy the code

List the List

The list for Compose is so simple that you don’t need to write any more annoying Adapters.

@Composable
fun MessageList(list: List<Message>) {
    Column {
   
        ...     
        LazyColumn { // this :LazyListScope
            items(list.count) { index : Int ->
                when(list[index].type) { Unread -> UnreadItem(message) Readed -> ReadedItem(message) } } } ... }}Copy the code
  • LazyColumnLazy loading of list entries is ensured
  • items(count) {... }Create count items. {… } item (Composalbe, MultiTypewhenStatement processing, simple and efficient.

Layout of the Layout

Compose provides Composalbe, a container class that is easy to use and powerful for laying out subcomponents.

Row {
    / / material Horizontal LinearLayout
}
Column {
    / / material Vertical LinearLayout
}
Box {
    / / material FragmeLayout
}
Copy the code

Unlike existing view systems, Compose’s layout does not allow multiple measurements, and there are no performance problems even if the layout is nested at multiple levels.

We can also easily customize the layout, through a simple function call to complete the process of measure and layout

Layout(
    content = content,
    modifier = modifier
) { measurables, constraints ->
    
    // Measure the composables
    val placeables = measurable.measure(constraints)
    
    // Layout the comopsables
    layout(width, height) {
        placeables.forEach { placeable ->
            placeable.place(x, y)
        }
    }
}
Copy the code

Modifier operator

Compose decorates the Composable’s appearance through a series of chained calls to the Modifier operator. Operators such as size, background, padding and click events can be added.

Modifier modified before Modifier modified
@Composable
fun FollowBtn(modifier: Modifier) {

    Text(
        text = "Follow",
        style = typography.body1.copy(color = Color.White),
        textAlign = TextAlign.Center,
        modifier = modifier // Add more decorations to the exterior.clickable(onClick = {... }) .shadow(3.dp, shape = backgroundShape)
            .clip(RoundedCornerShape(4.dp))
            .background(
                brush = Brush.verticalGradient(
                    colors = listOf( Red500, orange700),
                    startY = 0f,
                    endY = 80f
                )
            )
            .padding(6.dp)
    )
}
Copy the code

The chain call of the Modifier is easy to further decorate on the basis of the outer decoration, and the configuration of the Modifier is also conducive to multiplexing between multiple Composalbe.

Animation Animatioin

The Compose animation is also based on the continuous recombination of the State driver

@Composable
fun AnimateAsStateDemo(a) {
    var isHighLight by remember { mutableStateOf(false)}val color by animateColorAsState (
        if (isHighLight) Red else Blue,
    )    
    val size by animateDpAsState (
        if (isHighLight) LargeSize else SizeSize,
    )

    Box(
       Modifier
          .size(size)
          .background(color)
    )
 
}

Copy the code

When the isHighLight state changes, Color and Size are transitioned as animations

themes

Compose’s Theme breaks away from the use of XML. A Theme will typically appear as a top-level Composalbe, and all internal ComposalBes will apply the configuration of the current Theme.

Switching themes is also made very easy. For example, set different colors for the custom theme based on the system Dark/Light theme:

@Composalbe
fun YellowTheme(
    content: @Composalbe() - >Unit
) {
    val colors = if (isSystemInDarkTheme()) {
        YellowThemeDark
    } else {
        YellowThemeLight
    }
    MaterialTheme(colors, conent)
}
Copy the code

Application custom theme in APP:

@Composalbe
fun TopicScreen(a) {
    YellowTheme {
        Scaffold(
            backgroudndColor = MaterialTheme.colors.surface
        ) {
            TopicList()
        }
    }
}
Copy the code
Light Dark


9. Live preview during development


The current xmL-based previews are so weak that many developers are used to seeing the UI on a real machine.

For example, the KeyPoint: Compose preview mechanism can be used for the same thing as the real thing.

When we preview the Composable below

@Composable
fun Greeting(name: String) {
    Text (text = "Hello $name!")}Copy the code

Simply create a Composalbe with no parameters and add the @Preview annotation

@Preview
@Composable
fun PreviewGreeting(a) {
    Greeting("Android")}Copy the code

The @Preview Composalbe does not compile into the final product and has no effect on package size. And @preview can be run directly on the device as an entry point, and because of that, no parameters can be added to its signature.


10. Good interoperability


KeyPoint: Compose can be used with the existing View architecture. You can introduce Compose for an existing project and switch over gradually.

For example, for example, WebView, MapView, and so on, some functional controls still require native View support for Compose. With good interoperability, you can get more native support for Compose.

@Composable
fun WebView(a) {
    val url = remember { mutableStateOf(...) }

    // Add Adds view to Compose
    AndroidView(
        modifier = Modifier.fillMaxSize()
        factory = { context ->
            // Create a native WebView for Compose
            WebView(context).apply {
                ...
            }
        },
        update = { view ->
            // This is where you can update the native CustomView when the Satte changes
            view.loadUrl(url)
        }
    )
}
Copy the code


11. Summary & Outlook


Admittedly, Compose is quite different from current UI development methods, and the learning curve is relatively steep. This article stops short of introducing the Compose feature, and its main purpose is to provide you with a mental warm-up before you begin your systematic study.

Finally, a review of the keypoints outlined in the previous article:

Compose characteristics KeyPoint
Declarative framework Compose uses the @composable function to build its UI, implementing the declarative UI
Kotlin-based DSL The process of assembling the UI for the DSL is essentially the process of defining the @composable function
High-performance recombination The compiler ensures that the Composable skips unnecessary reorganizations as much as possible to improve redrawing performance
State management UI depends on State, and State changes drive UI refreshes
Unidirectional data flow A Composable’s data flows one-way from top to bottom, with all data coming from a single trusted source at the top level
Treatment of side effects Compose’s lifecycle mechanism can handle side effects
The UI system Compose’s CURRENT UI is fully functional and covers all of the capabilities of Android’s existing view system
Real-time preview For example, the Compose preview mechanism makes it possible to do the same thing as the real thing
interoperability Compose can coexist with the existing View architecture, you can introduce it for an existing project

The future of Compose

In July this year, Compose 1.0 will be released, and soon the official version of AS will support the development of Compose.

Now, more and more official Jetpack libraries and common three-party libraries have started to add support for Compose. As its ecology becomes more and more perfect, it is believed that it will not be long before we can see the arrival of Compose in a real project.

The release of projects such as the Compose Desktop and the Compose Web will give Compose cross-platform capabilities, allowing you as an Android developer to expand your development field and even extend your career.

If you missed Flutter, don’t miss Compose. Happy Composing!

Official Study Materials

  • Goo. Gle/compose – pat…
  • Goo. Gle/compose – Sam…
  • Goo. Gle/compose – doc…

Chinese Learning Program

  • docs.compose.net.cn/