Vigorously smart client team Verios

Introduction to the

The official entry document: developer.android.com/jetpack/com…

Jetpack Compose is a new Android native UI development framework that Google announced at Google I/O 2019. It took two years for Jetpack Compose to be released on July 29, 2021. The official introduction has the following features:

  • Less code

    • You can do more with less code and avoid all kinds of errors, making your code concise and easy to maintain.
  • Intuitive Kotlin API

    • Simply describe the interface, and Compose will take care of the rest. When the application status changes, the interface is automatically updated.
  • Speed up application development

    • Compatible with all existing code, convenient anytime anywhere. Fast iterations with live previews and full Android Studio support.
  • powerful

    • Create beautiful apps with direct access to the Android platform apis and built-in support for Material Design, dark themes, animations, and more.

How to understand “new UI framework”? What’s new is that it just throws away the View and ViewGroup stuff that we’ve been working on for years, and it’s a whole new UI framework from top to bottom. That is, the rendering mechanism, the layout mechanism, the touch algorithm and how the UI is written are all new.

Compose has three features: a code writing UI, a declarative UI, and a new UI framework.

Motivation

Reference since: in-depth explanation Jetpack Compose | optimized UI building

The decoupling

Currently, the way the Android interface is written, the layout file is written in layout.xml, and the ViewModel (the logical part of the code) is written in a ViewModel or Presenter. Some API (such as findViewById) links the two. For example, if you change the ID or the type of a View in XML, you need to change the corresponding code in the viewmodel. In addition, if a View is dynamically deleted or added, the layout XML is not updated and therefore needs to be manually maintained in the viewmodel.

The reason for this is that the XML layout and the view model should be one and the same. Can you write the layout file directly in code? Of course it could, but certainly not the way we do it now, new ViewGroup, addView. It’s easy to think of a Kotlin DSL, but it’s important to note that because the UI displayed in different situations may differ, the DSL must contain logic.

Historical baggage

Android has been around for more than a decade, and the traditional Android UI has a lot of legacy issues, some of which are hard to fix officially. For example, View.java has over 30,000 lines of code, and the ListView is obsolete.

In order to avoid repeating this mistake, we use functions instead of types in Jetpack Compose, use combinations instead of inheritance, discard the original Android View System, and directly draw on the canvas:

The main goal is to solve the coupling problem and the exploding base view.Java class. Based on the idea that composition is better than inheritance, a set of uncoupling UI framework is redesigned.

Quick start

Environment to prepare

Refer to the official document: developer.android.com/jetpack/com…

  1. Download the Android Studio Arctic Fox:developer.android.com/studio (note that must be used 2021.7.29 AS published in the future)
  2. Join the rely on
android {
  buildFeatures {
    compose true
  }
    / / compose_version = '1.0.0'
  composeOptions {
    kotlinCompilerExtensionVersion compose_version
    kotlinCompilerVersion '1.5.10'
   }
}

dependencies {
    implementation "androidx.compose.ui:ui:$compose_version"
    implementation "androidx.compose.material:material:$compose_version"
    implementation "androidx.compose.ui:ui-tooling:$compose_version"
    implementation 'androidx. Activity: activity - compose: 1.3.0'
 }
Copy the code
  1. Use Compose in your code

Activity

class MainActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContent {
            HelloWorld()
        }
   }
}    
Copy the code

Fragment

class MainFragment: Fragment() {

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup? , savedInstanceState:Bundle?).: View {
        return ComposeView(requireContext()).apply { 
            setContent { 
                HelloWorld()
            }
       }
   }
}
Copy the code

XML

<androidx.compose.ui.platform.ComposeView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/compose"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />
Copy the code
findViewById<ComposeView>(R.id.compose).setContent {
    HelloWorld()
}
Copy the code

A profound

The first time I wrote Compose, it took me about 2.5 hours to copy and heavily tutor my page (UI only, no logic). The actual experience learning cost is not very high, and if you have the foundation of flutter, it is very easy to learn.

The design draft Compose reconstructions

Show the header account information area code:

@Composable
fun AccountArea(a) {
    Row(
        verticalAlignment = Alignment.CenterVertically,
        modifier = Modifier
            .height(64.dp)
            .fillMaxWidth()
 ) {
        Image(
            painter = painterResource(id = R.drawable.avatar),
            contentDescription = "My profile picture",
            modifier = Modifier
                .size(64.dp)
                .clip(shape = CircleShape)
        )

        Spacer(modifier = Modifier.width(12.dp))
        Column(
            modifier = Modifier.align(Alignment.CenterVertically)
        ) {
            Text(text = "Lena Hua love Life", style = MyTabTheme.typography.h2, color = MyTabTheme.colors.textPrimary)
            Spacer(modifier = Modifier.height(10.dp))
            Row {
 GradeOrIdentity(text = "Select grade")
                Spacer(modifier = Modifier.width(12.dp))
                GradeOrIdentity(text = "Choose your identity")
            }

     }

        Spacer(modifier = Modifier.weight(1f))
        Image(
            painter = painterResource(id = R.drawable.icon_header_more),
            contentDescription = "Enter personal information Settings",
            modifier = Modifier
                .size(16.dp)
        )
    }
}


@Composable
fun GradeOrIdentity(text: String) {
    Row(
        horizontalArrangement = Arrangement.Center,
        verticalAlignment = Alignment.CenterVertically,
        modifier = Modifier
            .size(74.dp, 22.dp)
            .clip(MyTabTheme.shapes.medium)
            .background(MyTabTheme.colors.background)
    ) {
        Text(text = text, style = MyTabTheme.typography.caption, color = MyTabTheme.colors.textSecondary)
        Spacer(modifier = Modifier.width(2.dp))
        Image(
            painter = painterResource(id = R.drawable.icon_small_more),
            contentDescription = text,
            modifier = Modifier
                .size(8.dp)
        )
    }
}
Copy the code

The general structure is a combination of Row and Column, plus “controls” such as Text, Image, Spacer and so on, much like the Flutter. Everything in the Flutter is a Widget. Column, ListView, GestureDetector, and Padding are widgets, and what about Compose?

@ Composable functions

Maybe intuitively you think Row/Text/Image is some kind of View, right? No, they are both functions, the only special one being the @Composable annotation. This function has a rule: the @composeable function must be called in another @composeable function (similar to the coroutine keyword suspend), and we need to add this annotation to our own functions. So one idea is that everything in Compose is a function, and the developer is doing it by combining the @composeable function.

// Composable Fun Example(a: () -> Unit, b: @composable () -> Unit) {a() // Composable Fun Example(a: () -> Unit, b: @composable () -> Unit)} () - > Unit, b: @ Composable () - > Unit) {a () () / / / / allow b allowed}Copy the code

Modifier

In the existing Android View system, setting the width and height, Margin, click event and background color are achieved by setting a certain property of the View. The Flutter is implemented by wrapping a layer of widgets (e.g. Padding, SizedBox, GestureDetector, DecoratedBox). We said just now, all all functions, Compose the Compose is to select for each @ Composable functions provide parameters such as height/width/margin, or package again an @ Composable functions? The answer is the former. Compose abstracts them into the modifier parameter, and sets the size, behavior, and appearance of the modifier in series. Modifier function is very powerful, in addition to setting width and height, click, Padding, background color, but also to set the aspect ratio, scrolling, weight, even draw, layout, drag, zoom can do.

(If you are familiar with The Flutter, one of the criticisms of The Flutter is that it has a very deep level of widgets, which makes it very difficult to find widgets. In Compose, there is no such problem, because of the design of Modifier, it is possible to achieve the same hierarchy as the existing XML layout.

Development and debugging

The ability to preview existing XML layouts is powerful and can be seen quickly during development, and Compose’s support for this as Google’s next generation UI framework is also strong. (This is a joke about the Flutter; you have to compile it on your phone to see the effect). Add the @preview annotation to achieve a Preview, as shown below. You can also navigate directly to the code by clicking on the controls in the preview.

In addition to static previews, Compose supports simple click-throughs and debug animations. The reason this is possible is because Compose actually compiles executable code. (Some cross-platform elements)

Commonly used Composable

In summary, most of the capabilities that Android already has can be achieved with Compose.

Android Compose Flutter instructions
TextView Text Text
EditText TextField TextField
ImageView Image Image If you are loading a network image, Android/Compose should use the third-party library
LinearLayout Column/Row Column/Row
ListView/RecyclerView LazyColumn/LazyRow ListView
GridView/RecyclerView LazyVerticalGrid(experimental) GridView
ConstraintLayout ConstraintLayout There is no
FrameLayout Box Stack
ScrollView Modifier.verticalScroll() SingleChildScrollView
NestedScrollView Modifier.nestedScroll() NestedScrollView Compose is implemented through modifier
ViewPager There is no PageView Compose has an open source solution:Google. Making. IO/accompanist…
padding/margin Modifier.padding() Padding
layout_height/layout_width Modifier.size()/height()/width()/fillMaxWidth() SizedBox
background Modifier.drawbehind{} DecoratedBox

Text

See: Compose Text

@Composable
fun BoldText(a) {
    Text("Hello World", color = Color.Blue,
        fontSize = 30.sp, fontWeight = FontWeight.Bold,
        modifier = Modifier.padding(10.dp)
    )
}
Copy the code

TextField

@Composable
fun StyledTextField(a) {
    var value by remember { mutableStateOf("Hello\nWorld\nInvisible") }    
    TextField(    
        value = value,        
        onValueChange = { value = it },        
        label = { Text("Enter text") },        
        maxLines = 2,        
        textStyle = TextStyle(color = Color.Blue, fontWeight = FontWeight.Bold),
        modifier = Modifier.padding(20.dp)            
    )
}
Copy the code

Image

Image(
    painter = painterResource(R.drawable.header),
    contentDescription = null,
    modifier = Modifier
        .height(180.dp)
        .fillMaxWidth(),
    contentScale = ContentScale.Crop
)
Copy the code

Column / Row

Column indicates a vertical array, and Row indicates a horizontal array. The code for Column is as follows:

Column(modifier = Modifier.background(Color.LightGray).height(400.dp).width(200.dp)) {
    Text(text = "FIRST LINE", fontSize = 26.sp)
    Divider()
    Text(text = "SECOND LINE", fontSize = 26.sp)
    Divider()
    Text(text = "THIRD LINE", fontSize = 26.sp)
}
Copy the code

Box

Box(Modifier.background(Color.Yellow).size(width = 150.dp, height = 70.dp)) {
    Text(
        "Modifier sample",
        Modifier.offset(x = 25.dp, y = 30.dp)
    )
    Text(
        "Layout offset",
        Modifier.offset(x = 0.dp, y = 0.dp)
    )
}
Copy the code

LazyColumn / LazyRow

LazyColumn(modifier = modifier) {
  items(items = names) { name ->
     Greeting(name = name)
        Divider(color = Color.Black)
    }
 }
Copy the code

ConstraintLayout

For details, please refer to: medium.com/android-dev…

Additional dependencies need to be introduced:

Implementation "androidx constraintlayout: constraintlayout - compose: 1.0.0 - beta01"Copy the code
@Composable
fun ConstraintLayoutContent(a) {    
   ConstraintLayout {
    val (button1, button2, text) = createRefs()
    Button(
        onClick = { /* Do something */ } ,
        modifier = Modifier.constrainAs(button1) {
 top.linkTo(parent.top, margin = 16.dp)
        }
 ) {
       Text("Button 1")
    }

    Text("Text", Modifier.constrainAs(text) {
        top.linkTo(button1.bottom, margin = 16.dp)
        centerAround(button1.end)
    } )

    val barrier = createEndBarrier(button1, text)
    Button(
        onClick = { /* Do something */ } ,
        modifier = Modifier.constrainAs(button2) {
            top.linkTo(parent.top, margin = 16.dp)
            start.linkTo(barrier)
        }) {
           Text("Button 2")}}}Copy the code

It is not possible to get the ID of a “View” in a declarative UI, but there seems to be an exception in ConstraintLayout, where the ID is needed to describe the relative position.

The Scroll

See: developer.android.com/jetpack/com…

. Just add Modifier in Compose scroll ()/Modifier verticalScroll ()/Modifier. HorizontalScroll () can be realized.

Column(     
   modifier = Modifier            
      .background(Color.LightGray)            
      .size(100.dp)            
      .verticalScroll(rememberScrollState())    
 ) {     
   repeat(10) {       
       Text("Item $it", modifier = Modifier.padding(2.dp))        
   }    
}
Copy the code

NestedScroll

See: developer.android.com/jetpack/com…

Simple nested scroll only need to add the Modifier. Scroll () can be achieved

val gradient = Brush.verticalGradient(0f to Color.Gray, 1000f to Color.White)
Box( 
    modifier = Modifier
        .background(Color.LightGray)
        .verticalScroll(rememberScrollState())
        .padding(32.dp)
) {
 Column {
    repeat(6) {
        Box(
                modifier = Modifier    
                    .height(128.dp)
                    .verticalScroll(rememberScrollState())
            ) {
                 Text(     
                    "Scroll here",
                    modifier = Modifier
                        .border(12.dp, Color.DarkGray)
                        .background(brush = gradient)
                        .padding(24.dp)
                        .height(150.dp)
                )
            }
       }
    }
 }
Copy the code

If complex nestedScroll is required use Modifier. NestedScroll ().

val toolbarHeight = 48.dp
val toolbarHeightPx = with(LocalDensity.current) { toolbarHeight.roundToPx().toFloat() }
val toolbarOffsetHeightPx = remember { mutableStateOf(0f)}val nestedScrollConnection = remember {
    object : NestedScrollConnection {
            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
                val delta = available.y
                val newOffset = toolbarOffsetHeightPx.value + delta
                toolbarOffsetHeightPx.value = newOffset.coerceIn(-toolbarHeightPx, 0f)
                return Offset.Zero
            }
        }
    }

  Box(
        Modifier
            .fillMaxSize()
            .nestedScroll(nestedScrollConnection)
    ) {
 // our list with build in nested scroll support that will notify us about its scroll
        LazyColumn(contentPadding = PaddingValues(top = toolbarHeight)) {
            items(100) { index ->
                Text("I'm item $index", modifier = Modifier
                    .fillMaxWidth()
                    .padding(16.dp))
            }
 }

  TopAppBar(
            modifier = Modifier
                .height(toolbarHeight)
                .offset { IntOffset(x = 0, y = toolbarOffsetHeightPx.value.roundToInt()) } ,
            title = { Text("toolbar offset is ${toolbarOffsetHeightPx.value}")})}Copy the code

Declarative UI

If you want to change the color of a View, you need to get a handle to the View using findViewById, etc., and then call its methods to change the UI.

// Imperative style
View b = findViewById(...)
b.setColor(red)
Copy the code

Imperative UIs need to hold the full functional UI handle and then make changes to the UI by calling the relevant methods

To do the same in a declarative way, you simply declare the View and associate state with the corresponding UI property, and then use the state change to drive the redrawing of the View

// Declarative style
View {
    color: state.color,
}
Copy the code

Declarative UIs simply describe the current STATE of the UI, and the framework automatically updates the UI when that state changes

Declarative UI cannot be modified by obtaining the handle of View, which indirectly ensures that UI properties are immutable. State, as the only “Source of Truth”, is absolutely consistent with UI state at any time. So one thing declarative UIs can’t get around is state management.

State management

Reference: developer.android.com/jetpack/com…

If you’ve used a declarative framework, you’re familiar with state management. There are Stateless Widgets and StatefulWidgets in Flutter. The StatefulWidget is used to manage state. Compose, on the other hand, uses State to manage the State (along with remember() in most cases). In the following example, each time the text value changes, the reorganization is triggered, updating the UI.

Regrouping: Rerun certain functions to change the UI when data changes

Why does changing the text trigger updating the UI? This is the ability that Compose gives its State (mutableStateOf() returns a mutableState). State means that this variable is the “State” that Compose cares about and needs to be aware of when that State changes and triggers reorganization. In practice, State is usually used with the remember function, which means that the last State can be remembered when the reorganization occurs. In the following example, when Foo() is reexecuted due to reorganization, the local variable text of the function is assigned to its initial value (an empty string) without remember, which is clearly not what we expected, hence remember.

@Composable
fun Foo() {
  var text: String by remember { mutableStateOf("") }
  Button(onClick = { text = "$text\n$text" }) {
    Text(text)
  }
}
Copy the code

The code above is similar to MobX in Flutter, but compared to MobX, Compose’s reorganization range is more fine-grained and precise. It automatically analyzes the minimum reorganization range. For example, as long as the scope of the recombination is the lambda part of the Button.

Recommended architecture

Where should these states go? Is it distributed across the @composable functions? Officially, there is a recommended architecture: a ViewModel to hold LiveData and a UI to view it.

Events from the bottom up and states from the top down (unidirectional data flow)

other

As a UI framework, you also need to include Theme and Animation. Please refer to the official document for this part.

What about actual use?

The actual use of Compose might care about this data

Packet size

Reference from: Jetpack compose before and after

Conclusion: Packet size is significantly reduced

Packet size
Methods the number
Compilation speed

performance

In theory there is an advantage to complex layouts because there is no XML -> Java transformation.

In actual use, no authoritative data was found, and the official Demo was quite smooth.

But there are also a number of issues on GitHub poking fun at Compose’s performance: github.com/android/com…

Starting threshold & development efficiency

Advantages: Easier to animate, custom View, preview is powerful.

Disadvantages: Need to be comfortable with declarative writing

Android-developers.googleblog.com/2021/05/mer… The article claims that Compose has improved development efficiency by 56%. If you have a foundation with Flutter, you can try and implement an interface with Compose, and it should work pretty well.

In classic View, there is a certain threshold for writing animation, custom View, and custom ViewGroup, but in Compose, it will be much easier to use the API provided by the system, such as animateXXXAsState, Canvas, and Layout.

Compatibility with existing libraries

In actual use, the infrastructure of Compose may not be perfect. For example, Glide, ViewPager, SwipeRefresh, RxJava, and so on, for example, are compatible for Compose. However, we have to admit that many libraries do not have a version for Compose. There is no Fresco, TTVideoPlayer, WebView, CoordinatorLayout, etc. Official also provides a solution AndroidView, write as follows:

@Composable
fun CustomView() {
    val selectedItem = remember { mutableStateOf(0) }
   // Adds view to Compose
    AndroidView(
        modifier = Modifier.fillMaxSize(), // Occupy the max size in the Compose UI tree
        factory = { context ->
            // Creates custom view
            CustomView(context).apply {
               // Sets up listeners for View -> Compose communication
                myView.setOnClickListener {
                    selectedItem.value = 1
                }
           }
       } ,
        update = { view ->
            // View's been inflated or state read in this block has been updated
            // Add logic here if necessary
            view.coordinator.selectedItem = selectedItem.value
        }
   )
}
Copy the code

I tried to package QrCodeView (a TWO-DIMENSIONAL code scanning View) as Compose in the above way, but I ran into some problems.

  1. It’s uncomfortable to mix imperative and declarative

  2. Code error caused by unfamiliarity with Compose

    1. Such as: The composable function executes only when the traditional View.onAttachtoWindow () is used. So in the Activity. The onCreate/onStart/onResume want AndroidView do some operation takes some skill. In many cases, you need to browse the official Sample to find the answer.
  3. The other weird thing is that you don’t think about Compose. Many third-party libraries will have various incompatibility issues with Compose, which requires a little bit of stepping.

If you have a Composable version of the ViewGroup, such as a waterfall flow Layout, don’t worry about migrating. It’s more realistic to rewrite the Composable function for Compose, which provides a Layout() function that makes it easier to customize your Layout.

Is it cross-platform?

According to the cross-platform feature of Kotlin, Compose actually has cross-platform potential, and the official Demo has been given, but of course it is in a very original state.

Compose For Desktop: Compose For Desktop UI Framework

Compose The For The Web: Technology Preview: Jetpack Compose For Web | The Kotlin Blog

Reference Documentation (read more)

  1. Get started with Jetpack Compose
  2. Jetpack Compose: Pathway
  3. Compose Academy
  4. In-depth explanation Jetpack Compose | implementation principle
  5. UI building in-depth explanation Jetpack Compose | optimization
  6. Compose Preview’s UX design tour
  7. Jetpack Compose is more than just a UI framework!
  8. Juejin. Cn/post / 693751…
  9. Mp.weixin.qq.com/s/VUe4JgdDd…
  10. Intelligiblebabble.com/compose-fro…
  11. Dev. To/zachklipp/s…
  12. Column: medium.com/mobile-app-…
  13. Medium.com/mobile-app-…
  14. Betterprogramming. Pub/deep dive – I…
  15. Dev. To/zachklipp/s… (Compose Status Management)