jetpack compose Phase 2 build interface and understand Compose programming ideas

Goal: In this issue, we will build a simple login page, understand the programming idea of ​​Compose, and use some basic widgets.

Today's process is to first roll out the code and explain the relevant knowledge points. Without further ado, let's start directly.


Widgets used in this issue, detailed APIs are currently introduced on the Internet, and you can search them yourself, so I won’t go into details here.

Column 

Text

TextField

Button


The page we are going to implement today is the login page. The login page must have two input boxes to enter the user name and password, and then a login button.

First, we open the MainActivity file and delete the code that comes with the template.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApplication2Theme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {

                }
            }
        }
    }
}

Here we want to use the Column widget, which we can understand as a vertical LinerLayout, and place two Texts for display at the same time. Let's look at the effect first.

Surface(...) {
    Column() {
        Text(text = "我是Text1")
        Text(text = "我是Text2")
    }
}

The renderings are as follows:

 We also need two input boxes and a button. Here we use the TextField and button provided by material. The code is as follows:

Column() {
    Text(text = "请登录")
    TextField(value = "我是输入框1", onValueChange = {})
    TextField(value = "我是输入框2", onValueChange = {})
    Button(onClick = {  }) {
        Text(text = "登录")
    }
}

The effect is as follows:

 

 

 Note: At this time, the input box does not accept any input value. We will build the page first today, and then do the logic later.

The UI page is basically built. Next, let’s adjust the UI. First, use the horizontalAlignment attribute for horizontal center display, and display it horizontally in full screen. At the same time, add padding margins. The modifier attribute of the widget is used here. The code is as follows:

 The renderings are as follows:

So far, the UI page is basically built, but the input box is not fully working yet, we need to adjust the input box now.

...

var userName by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }

TextField(
    modifier = Modifier.fillMaxWidth(),
    value = userName,
    onValueChange = {
        userName = it
    })


TextField(
    modifier = Modifier.fillMaxWidth(),
    value = password,
    onValueChange = {
        password = it
    })

...

There will be a detailed explanation later on why this is implemented. At this point, we find that the input box can be input by the corresponding input method. After sorting out the code, it finally looks like this.

package com.dali.demo

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.dali.demo.ui.theme.MyApplication2Theme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApplication2Theme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    LoginScreen()
                }
            }
        }
    }
}

@Composable
fun LoginScreen(){
    var userName by remember { mutableStateOf("") }
    var password by remember { mutableStateOf("") }

    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = Modifier
            .fillMaxWidth()
            .padding(start = 16.dp, end = 16.dp),
    ) {
        Spacer(modifier = Modifier.height(48.dp))
        Text(text = "请登录")
        Spacer(modifier = Modifier.height(32.dp))
        TextField(
            modifier = Modifier.fillMaxWidth(),
            value = userName,
            onValueChange = {
                userName = it
            })
        Spacer(modifier = Modifier.height(8.dp))
        TextField(
            modifier = Modifier.fillMaxWidth(),
            value = password,
            onValueChange = {
                password = it
            })
        Spacer(modifier = Modifier.height(16.dp))
        Button(modifier = Modifier.fillMaxWidth(), onClick = { }) {
            Text(text = "登录")
        }
    }
}

At this point, everyone has a preliminary understanding of Compose. Next, let's learn about Compose's programming ideas.

Compose programming ideas

Jetpack Compose is a new declarative interface toolkit for Android. Compose provides a declarative API. We can render the application interface without changing the front-end view in a command way, making it easier to write and maintain the application interface.

@Composable function

To use Compose, we need to define a set of Composable functions that accept data, which can also be called composable functions to build interfaces. Remember the example when we just created the project?

This is a simple composable function that uses the data passed in to display a text widget on the screen.

  • This function is annotated with @Compose. All composable functions must add this annotation. This annotation can tell the Compose compiler that this function will convert data into an interface.
  • This function accepts data, the composable function can accept parameters, these parameters are used to describe the UI interface, and the Greeting function accepts a String to display.
  • This function is used to display text. We can find that Text() is also a composable function when we open the source code. This function will create text elements on the interface. Composable functions emit interface hierarchy by calling other composable functions.
  • This function doesn't return anything, composable functions don't need to return anything since they describe the desired screen state.
  • This function is fast, idempotent and has no side effects. When this function is called multiple times with the same argument, it behaves in the same way, and this function does not make any modifications to global variables.

declarative programming

We can understand Android's view hierarchy as a view tree. Since the state of the application will change due to factors such as user interaction, the interface hierarchy needs to be updated to display data. For example, if we want to display the user name, we need to use the findView function to find the view when the activity is initialized, and then use setText to display it according to the data. If the data has been updated, we need to manually update the view again. This approach can be called an imperative UI system. The declarative UI is to bind the data and the UI in a certain way, and then we only need to maintain the background logic of the data, and do not need to care about how to update the UI.

Manually manipulating the view will increase the possibility of errors. If a piece of data is presented in multiple locations, it is easy to forget to update the logic of the display. In addition, when two logic updates agree with the view, it is also very easy to cause a state. In general, the complexity of software maintenance grows with the number of views that need to be updated.

In Compose's declarative API, widgets are relatively stateless and will not be provided externally in the form of objects. The update of the interface is realized by calling a uniform composable function with different parameters.

The logic we apply provides data for display to composable functions, which use that data to describe the interface by calling other composable functions, passing data progressively down the widget hierarchy. When the user interacts with the interface, the interface will initiate events such as onClick, which notify the application logic, the application logic updates the data, and the system will call the composable function again with the new data, which will cause the interface elements to be redrawn. This process can be called " restructuring".

The user has interacted with an interface element, causing an event to be fired. The application logic responds to the event, and the system automatically calls the composable function again with new parameters as needed.

recombine

In an imperative UI system, if we want to change the display of a view, we can call methods on the view to change its internal state. In Compose, the composable function needs to be called again with new data. When the data is changed, the function will be reorganized, and the system will re-call the composable function with new data as needed. The Compose framework can intelligently reorganize only the ones that need to be changed. components.

For example, there is such an example, which is used to display a button and display the number of clicks at the same time.

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

Every time the button is clicked, the onClick callback function will be called, and the clicks will be updated in the callback function. This process is called "reorganization" and other functions that do not depend on the value of clicks do not reorganize. In the interface we built today, the principles of the two input boxes are the same. Those who are interested can print the log to check.

Reorganizing the entire interface tree is computationally expensive, and in this way Compose can efficiently reorganize by calling functions or lambdas that may have changed based on new input and skipping the rest.

A composable function may be executed every frame, or it may be executed once and never again. So we should avoid performing expensive operations in composable functions, and if there are such operations, we should perform them in background coroutines and pass the results as parameters to composable functions.

Composable Function Execution Order - Sequential vs. Parallel

In today's example, our composable functions are executed sequentially, but this may not be the case in the actual development process. If a composable function contains changes to other composable functions, or some other variable, the composable function It is not necessarily executed in order.

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

For example, in this code, we draw three screens. If the three screens are independent, then this function will be executed in sequence.

Of course, Compose can also optimize recomposition by running composable functions in parallel, so that multiple cores can be used to run composable functions at a lower priority. Such optimization also means that all composable functions cannot have incidental effects, and should be triggered by callbacks such as onClick on the UI thread.

Compare the following two examples, showing a list and its count

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

In the second example, each reorganization modifies the local variable items, showing the wrong number of lists anyway.

Reorganization skips as much content as possible

Compose will try its best to recompose only the parts that need to be updated, skipping composable functions that do not need to be updated. Every composable function and lambda can be recombined by itself, as is well illustrated in the following example.

@Composable
fun NamePicker(
    header: String,
    names: List<String>,
    onNameClicked: (String) -> Unit
) {
    Column {
        // 当header发生变化,Text 会发生重组
        Text(header, style = MaterialTheme.typography.h5)
        Divider()

        LazyColumn {
            items(names) { name ->
                // 当names发生变化,这个地方会发生重组,header发生变化,则无影响
                NamePickerItem(name, onNameClicked)
            }
        }
    }
}

@Composable
private fun NamePickerItem(name: String, onClicked: (String) -> Unit) {
    Text(name, Modifier.clickable(onClick = { onClicked(name) }))
}

When the header changes, Compose may jump to the Lambda of Column without executing its upper-level widget. When executing Column, if the names have not changed, Compose may choose to skip LazyColumn.

Restructuring is an optimistic operation

Compose starts whenever Compose thinks a composable's parameters may have changed. Reorganization is an optimistic operation, that is, Compose expects the recomposition to complete before the parameters change again. If a parameter changes before the reorganization is complete, Compose may cancel the recomposition and start over with the new parameters.

Compose discards the interface tree from the reorganization when it is unreorganized. If any side effects depend on the displayed interface, that side effect will be applied even if the group operation is canceled. This can lead to inconsistent application state.

Make sure all composable functions and lambdas are idempotent and have no side effects to handle optimistic recombination.

Composable functions may run very frequently

In some cases, a composable function may be run for each frame of the UI animation. If the function performs an expensive operation, such as reading data from device storage, it may cause UI jank.

For example, trying to read device settings, it might read those settings hundreds of times a second, which can have a disastrous effect on the app's performance.

If a composable function requires data, parameters should be defined for the corresponding data. Then move the expensive work to other threads and use  mutableStateOf or  LiveData pass the corresponding data to Compose.

To summarize, when using Compose one should be aware that it is best practice to keep composable functions fast, idempotent, and free of side effects.

  • Composable functions can be executed in any order.
  • Composable functions can be executed in parallel.
  • Restructuring skips as many composable functions and lambdas as possible.
  • Reorganizations are optimistic operations and may be cancelled.
  • A composable function may run very frequently like every frame of an animation

Guess you like

Origin blog.csdn.net/z2008q/article/details/126031161