This article has participated in the good article call order activity, click to see: back end, big front end double track submission, 20,000 yuan prize pool for you to challenge!

(This part involves more content, the length is relatively long, it is recommended to collect, meditation reading.)

preface

In Q1, the company made a training plan, and some people were required to report training topics as lecturers. At that time, I had just been separated from several Android projects and just saw Jetpack release a new gadget, Compose. I was attracted by its fast and real-time packaging, so I prepared to investigate and submitted this project.

However, the plan can not keep up with the change, as soon as I reported the project, I plunged into the front end of the hot water. From 0 to 1 to learn front-end, while learning while doing projects, while doing projects while sharing, thinking about how to let others learn to do front-end projects, this period of time, really sour.

As time goes by, the topic sharing I reported before is also approaching, and I just remembered that I still owe an explanation. Can’t, the subject that oneself report, stay up late also want to rush out.

Next, I will elaborate this topic from the following aspects:

  • What is Compose
  • How to use Compose gracefully
  • Finally, is Compose worth a try

Noun:

The following terms may vary, so to avoid confusion, here is a list of nouns:

noun parsing note
component A logical unit that controls the part of the UI that a page displays
View A presentable UI and the ability to maintain its own state
Micro parts withcomponentA logical unit that controls the part of the UI that the page displays

For example, for example, “Compose” is the name used in the official document for “micro”, which can be used for various Android views, and also for components used in the H5 front-end.

A: What is “Compose”

Jetpack Compose is a new Android toolkit for building native interfaces. It simplifies and speeds up interface development on Android. Make your app lively and exciting quickly with less code, powerful tools, and the intuitive Kotlin API.

So listen to feel a bit abstract, do not know what to say again.

Let me translate:

Jetpack Compose is a framework for redefining Android layouts based on the Kotlin API, enabling faster implementation of native Android apps. Saves development time, reduces package size, and improves application performance.

Saves development time, reduces package size, and improves application performance. That sounds tempting. Let’s see how it works.

1.1 Android Studio support for Compose

(This section is thanks to Still Fanesi for the practical data.)

This feature is based on the new version of Android Studio’s support for Compose.

The new Version of Android Studio Arctic Fox(currently Canary) adds a number of new tools to support new features for Jetpack Compose, such as live text, animation preview, layout checking, and more.

1.1.1 Powerful Previews

The new Android Studio adds the ability to Preview text changes in real time, either in Preview, emulator, or on the real machine.

1.1.2 Animation Preview

You can view, check, or play animations within AndroidStudio, and you can play them pin by pin.

1.1.3 Layout Inspector

Android Studio Arctic Fox has added support for the Layout monitor for Compose, which analyzes the hierarchy of components in the Compose component. As follows:

1.1.4 Interactive Preview

In this mode, you can interact with interface components, click on components, and see how the state changes. This way, you can get quick feedback on how the interface is reacting and get a quick preview of the animation. To enable this mode, just click the “Interactive” icon and the system will switch to preview mode.

To Stop this mode, click Stop Interactive Preview in the top toolbar.

This is AndroidStudio’s support for Compose, and it’s a big deal.

1.2 Jetpack Compose before and after use

You think Compose is just adding preview functionality? That’s not true.

By switching to the Compose app instead of the regular app, you can dramatically improve your app’s speed and performance.

Let’s look at an example of an official Google revamp.

1.2.1 APK size reduction

The most important indicator that users care about is APK size.

Here is a minimal release APK with resource reduction enabled (using R8) as measured by the APK Analyzer:

A note on the above figures:

The APK file size reported by APK Analyzer is used (instead of the download size). APK size analysis

2. After using Compose, we saw a 41% reduction in APK size and a 17% reduction in methods

1.2.2 Lines of code

The number of lines of source code is not a good measure of software, but it provides a statistical perspective on how far an experiment has gone in terms of slimness.

As you can see from the figure, the number of XML rows has been drastically reduced by 76%. Goodbye to layout files and other XML files such as styles and theme.

At the same time, the total number of lines of Kotlin code goes down.

That’s a big part of why APK has been able to slim down.

1.2.3 Build speed

Build speed is an indicator that developers care a lot about.

A few caveats here:

“Full access to Compose” uses the latest version of Dagger/Hilt, which uses the new ASM API in Android Gradle Plugin 7.0. Other versions use an older version of Hilt, which uses a different mechanism and can seriously slow down the time to generate the dex file.

In addition, the things that the Kotlin and Compose plug-ins do for us, such as location memorization, fine-grained reassembly, and so on, reduce build time by an astounding 29%.

2. How to use Compose gracefully

Well, that’s a lot of good things about Compose. Now, how can we use it?

2.1 to prepare

Before you can start using Compose, you need to have some basics in place.

  • Download Android Studio Arctic Fox or higher
  • Kotlin version 1.4.32 or higher
  • The Kotlin language is accessible

2.2 Quickly build up Compose

How to quickly build a new project for Compose and how to migrate an old project into a Compose project.

I’ve covered the practical steps in “Compose,” Android’s latest UI framework, in great detail, so I won’t go into detail here.

2.3 How to learn Compose quickly

First of all, congratulations on having made it here. After reading this article, you are ready to start developing.

You can also learn how to use Compose basics quickly in the “Getting Started” sample tutorial on the website.

You can also watch [instructional videos] on YouTube,

Or download [Demo Collection] from gitHub. There are many demo examples available, as shown below:

2.4 Compose programming idea

Now that you know how to build it, and how to write a demo, you need to understand some essential and important components of Compose before you can finally use it in your project.

The first is the idea of programming.

2.4.1 Declarative programming paradigm

Before Compose, our most common way to update the interface is to use the findViewById() method to find the UI control, And update the controls by calling button.settext (String), Container.addChild (View), or img.setimageBitmap (Bitmap).

This manual layout improves code readability, but it is also error-prone. For example, after the A control is removed, A value is assigned to the A layout in another piece of code. This may cause code exceptions.

Over the past few years, the industry has moved toward declarative interface models that greatly simplify the engineering associated with building and updating interfaces. The technique works by conceptually regenerating the entire screen from scratch and then executing only the necessary changes. This approach avoids the complexity of manually updating the stateful view hierarchy.

Simply put, declarative layouts allow you to update only the layout that needs to be updated, rather than refreshing the entire screen for a small change, which is a big performance improvement.

Compose is a declarative interface framework.

One of the challenges of regenerating an entire screen is that it can be costly in terms of time, computing power and battery use. To mitigate this cost, **Compose intelligently chooses which parts of the interface need to be redrawn at any given time. ** This has a certain effect on the way you design interface components, as described below.

2.4.2 Simple composable functions

With Compose, you can build an interface by defining a set of composable functions that take data and emit interface elements.

A simple example below is the Greeting component, which accepts String copywriting and emits a Text component that displays a Greeting message.

GreetingComponent resolution:

1. This function is annotated with @composable. All combinable functions must have this annotation; This annotation tells the Compose compiler that this function is intended to convert data into an interface.

2. The widget accepts a String, so it can greet the user by name.

3. This function can display text in the interface. To do this, it calls the Text() composable function, which actually creates the Text interface element. Composable functions emit an interface hierarchy by calling other composable functions.

4. This function returns nothing. The Compose functions that emit the interface do not need to return anything, because they describe the desired screen state rather than constructing the interface widget.

5. This function is fast, idempotent, and has no side effects.

  • 5.1 This function behaves the same way when called multiple times with the same argument, and it does not use other values such as global variables or pairsrandom()The call.
  • 5.2 This function describes the interface without any side effects, such as modifying properties or global variables.

2.4.3 Declarative paradigm shifts

In previous XML layout programming, it was common to extend the layout by adding XML layout files. Each View maintained its own state internally and provided getters and setters to allow logic to interact with the View.

In the declarative methods of Compose, the View is relatively stateless and does not provide a setter or getter.

In fact, views are not provided as objects.

You can update the interface by calling the same composable function with different arguments. This makes it easy to provide state to architectural patterns such as viewModels, as described in the Application Architecture Guide.

The composable function is then responsible for transitioning the current application state to the interface with each observable data update.

(Example below: a data source is passed as below, applied to each layout, when the interface needs to be refreshed, only the data source needs to be refreshed)

(The following example: when a word layout starts to click on the event, the event is passed up, and finally the data source is changed, and the interface is refreshed)

2.4.4 Dynamic Content

Because combinable functions are written in Kotlin rather than XML, they can be as dynamic as any other Kotlin code. For example, suppose you want to build an interface that greets some users

@Composable
fun Greeting(names: List<String>) {
    for (name in names) {
        Text("Hello $name")
    }
}
Copy the code

This function takes a list of names and generates a greeting for each user.

Combinable functions can be very complex. You can use Kotlin for any logical modifications you want, depending on the functionality, and all of this dynamic tailoring is Compose’s advantage over traditional XML.

2.4.5 restructuring

In the imperative interface model (the XML interface model), if you need to change a View, you can call the setter on that View to change the internal state.

In Compose, you can call the composable function again with new data.

Doing so causes the function to be reorganized — the system redraws the View from the function as needed, using the new data.

The Compose framework can intelligently recompose only components that have changed.

For example, suppose you have the following combinable function that displays a button:

@Composable
fun ClickCounter(clicks: Int, onClick: () -> Unit) {
    Button(onClick = onClick) {
        Text("I've been clicked $clicks times")
    }
}
Copy the code

In the above code, the caller updates the value of CLICKS each time the button is clicked. Compose calls the lambda and Text functions again to display the new value; This process is called recombination. Other functions that do not depend on this value are not reorganized.

Reorganizing the entire interface tree is computationally expensive because it consumes computing power and reduces battery life.

When Compose reorganizes based on new input, it only calls functions or lambdas that may have changed and skips the rest. Compose can be reassembled efficiently by skipping all functions or lambdas with unchanged parameters.

Do not rely on the side effects of executing a composable function, because you may skip the reorganization of the function.

Side effects: Any changes visible to the rest of the application.

For example, the following are all dangerous side effects:

  • Writes the properties of the shared object
  • Update the observables in the ViewModel
  • Update sharing preferences

Here’s an example:

The following code creates a composable item to update the value in SharedPreferences.

@Composable
fun SharedPrefsToggle(
    text: String,
    value: Boolean,
    onValueChanged: (Boolean) -> Unit
) {
    Row {
        Text(text)
        Checkbox(checked = value, onCheckedChange = onValueChanged)
    }
}
Copy the code

The compostable should not be read or written from the shared preference itself, so this code moves the read and write operations to the ViewModel in the backbench coroutine. The application logic uses a callback to pass the current value to trigger the update.

2.4.6 Precautions for using Compose

Here are some things to note when programming in Compose:

  • Combinable functions can be executed in any order.
  • Combinable functions can be executed in parallel.
  • Recombination skips as many composable functions and lambdas as possible.
  • Restructuring is an optimistic operation that may be cancelled.
  • The composable function might run as frequently as each frame of the animation.

In each case, the best practice is to keep the combinable functions fast, idempotent, and without side effects.

Combinable functions can be executed in any order.

For example, suppose you have the following code to draw three screens in a TAB layout:

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

Calls to StartScreen, MiddleScreen, and EndScreen can be made in any order. This means, for example, that you can’t have StartScreen() set a global variable (a side effect) and let MiddleScreen() take advantage of the change. Instead, each of these functions needs to remain independent.

Combinable functions can be executed in parallel.

Compose can be optimized for recombination by running combinable functions in parallel. In this way, Compose can leverage multiple cores and run composable functions (off screen) at a lower priority.

This optimization means that combinable functions may be executed in a background thread pool.

If multiple composable functions call method A in the ViewModel, then method A will be called by multiple threads, and thread synchronization needs to be done.

Also because of the parallel nature of execution, the call may occur on a different thread than the caller. Therefore, all combinable functions should not have side effects (such as modifying a global variable), but should be triggered by callbacks such as onClick that are always executed on the interface thread.

The following example shows a composable item that displays a list with its number of items:

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

This code has no side effect; it turns the input list into an interface. This code is great for displaying small lists. However, if the function writes to a local variable, this is not thread-safe or correct code:

@Composable
@Deprecated("Example with bug")
fun ListWithBug(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")
    }
}
Copy the code

In this case, items are modified each time you reorganize. This could be each frame of the animation, or when the list is updated. Either way, the interface displays the wrong number of items. Therefore, Compose does not support such a write operation; By disallowing such write operations, we allow the framework to change threads to perform composable lambda.

Recombination skips as many composable functions and lambdas as possible.

If parts of the interface are not working, Compose will try to recompose only the parts that need to be updated. This means that it can skip something to re-run the composable item of a single button without executing any composable item above or below it in the interface tree.

Every combinable function and lambda can reassemble itself. The following example shows how regrouping can skip some elements when rendering a list:

/**
 * Display a list of names the user can click with a header
 */
@Composable
fun NamePicker(
    header: String,
    names: List<String>,
    onNameClicked: (String) -> Unit
) {
    Column {
        // this will recompose when [header] changes, but not when [names] changes
        Text(header, style = MaterialTheme.typography.h5)
        Divider()

        // LazyColumn is the Compose version of a RecyclerView.
        // The lambda passed to items() is similar to a RecyclerView.ViewHolder.
        LazyColumn {
            items(names) { name ->
                // When an item's [name] updates, the adapter for that item
                // will recompose. This will not recompose when [header] changes
                NamePickerItem(name, onNameClicked)
            }
        }
    }
}

/**
 * Display a single name the user can click.
 */
@Composable
private fun NamePickerItem(name: String, onClicked: (String) -> Unit) {
    Text(name, Modifier.clickable(onClick = { onClicked(name) }))
}
Copy the code

Each of these scopes may be the only one that is executed during reorganization. When the header changes, Compose might jump to Column lambda without executing any of its parents. In addition, when executing Column, Compose may choose to skip LazyColumnItems if the names have not changed.

Executes all combinable functions orlambdaThere should be no side effects. When you need to perform a side effect, it should be triggered by a callback.

Restructuring is an optimistic operation that may be cancelled.

The reorganization is an optimistic move, which means that Compose expects to complete the reorganization before the parameters change again. If one of the parameters changes before the recombination is complete, Compose might cancel the recombination and start over with the new parameters.

When unregrouping, Compose will drop its interface tree from the regrouping.

If there are any side effects that depend on the displayed interface, they will be applied even if the composition action is cancelled. This can lead to inconsistent application state (resulting in state confusion, or repeated assignments).

The composable function might run as frequently as each frame of the animation.

In some cases, it is possible to run a composable function for each frame of an interface animation. If this function performs an expensive operation, such as reading data from device storage space, the interface may stall.

If the combinable function needs data, it should define parameters for the corresponding data and get them from the parameters.

You can move expensive work to a thread other than the constituent operator thread and pass the corresponding data to Compose using mutableStateOf or LiveData.

** Summary: ** Combinable functions should be written as pure functions as possible, data is only obtained from the parameters, change data only from user operation events. All asynchronous data needs to be prepared and passed in as arguments to the function.

3. Compose is worth a try

We talked about Compose’s features, pros and cons, and how to get started and use it properly.

Is Compose worth applying to a project?

These need to be analyzed on a case-by-case basis.

If you are new to the project.

I encourage you to try something new, because clever “partial refresh” is an important way to improve your page performance. And declarative layouts should replace traditional XML layouts in the future.

If you are an existing project retrofit.

First, you can evaluate whether you already have the basic capability for starting up Compose — the flexibility of using the Kotlin language.

Compose can be said to be a derivative tailored for Kotlin and closely combined with View Model. With Kotlin and View Model, Compose can play an extreme role and achieve the previous goal:

  • Build time can be reduced29%
  • The number of XML lines has been drastically reduced76%
  • The APK size has been reduced41%
  • The number of methods is going down17%

If you already have these capabilities, try them out on a small scale, or start with higher-performance pages.

It is recommended to introduce a single page first, and then complete the replacement. Google’s official reinvention case also does so.

Finally, let go and lift it up!

The community needs you and me to build together, more need to walk in the forefront of the practitioners, looking forward to see more and better articles, this is my writing motivation.