Android

Android Compose 介绍与实践

2021-06-15  本文已影响0人  TTTqiu

简介

Jetpack Compose 是 Google 官方 2019 年推出的UI框架,它可简化并加快 Android 的 UI 开发工作。使用更少的代码、强大的工具和直观的 Kotlin API,快速构建 App 的 UI。2021年马上就将迎来 Compose 的正式版,是时候来了解一下这个官方强推的,布局机制、渲染机制、具体写法等可以说是全新的UI框架了。

先来看一段简单的 Compose 代码:

Column {
    Text("Hello world")
    Image()
}

OK这就是一个完整的UI界面了,对比原来定义在 xml 文件中的方式有着天壤之别,展现一个UI不再是去创建一个 TextView 之类的控件,而是变成了一次函数调用。虽然 Text 以大写开头,但它其实就是一个普通函数,严格说是个带 @Composable 注解的 Compose 函数:

@Composable
fun Text(...) {
    ...
}

来看一段完善一些的 Compose 代码:

@Composable
fun NewsStory() {
    MaterialTheme {
        val typography = MaterialTheme.typography
        Column(
            modifier = Modifier.padding(16.dp)
        ) {
            Image(
                painter = painterResource(R.drawable.header),
                contentDescription = null,
                modifier = Modifier
                    .height(180.dp)
                    .fillMaxWidth()
                    .clip(shape = RoundedCornerShape(4.dp)),
                contentScale = ContentScale.Crop
            )
            Spacer(Modifier.height(16.dp))

            Text(
                "A day wandering through the sandhills " +
                     "in Shark Fin Cove, and a few of the " +
                     "sights I saw",
                style = typography.h6,
                maxLines = 2,
                overflow = TextOverflow.Ellipsis)
            Text("Davenport, California", style = typography.body2)
            Text("December 2018", style = typography.body2)
        }
    }
}

以 Column、Row 代替 LinearLayout 等布局,以 Text、Image 等代替 TextView、ImageView 等控件,以 Modifier 等用作细节和修饰,所以其实 Compose 就是这样由多个函数调用组合起来,形成一个完整的 UI 界面。

Compose 改变了原有的基于 xml 和 View 的体系,纯在代码中实现页面UI,那么它比起老的方式有什么优势呢?

Compose 的特点

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

这是官方对 Compose 的定义,比起旧有体系,Compose 更加 “现代”。

现有的 Android 视图体系从 2010年以来没有发生太大变化,10年间无论从硬件规格还是APP复杂度都发生了极大变化,这套已经跑了10年的技术体系也已经显得有些落伍。

声明式 vs 命令式

说起 Compose 最大的特点,就是它是声明式的,而现有体系是命令式的

高性能的重组(重绘)

在上面的例子里,当 message 发生变化时,MessageList 重新执行,这个过程叫重组(recomposition)。Composee 的 UI 正是通过不断重组来实现刷新。

但如果数据变化时会触发重组,大面积的重组是否会影响性能呢?

Compose 会通过在 Gap Buffer 这样的线性结构上进行 diff 实现局部刷新。 Gap Buffer 可以理解为一个树形结构经 DFS 处理后的数组,数组单元通过 key 标记其在树上的位置信息。Compose 在编译期为 Composable 生成带有位置信息的 key,存入到 Gap Buffer 数组的对应位置。运行时可以根据 key 来识别 Composable 节点是否发生了位置变化,以决定是否参与重组。同时,Gap Buffer 还会记录 Composable 对象关联的状态(State 或 Parameters),仅仅当关联状态变化时,Composable 才会参与重组,函数才会重新执行。

布局层级嵌套

做 Android 开发的都知道一个规矩:布局文件的界面层级要尽量地少,因为层级的增加会大幅拖慢界面的加载。这种拖慢的主要原因就在于各种 Layout 的重复测量。虽然重复测量对于布局过程是必不可少的,但这也确实让界面层级的数量对加载时间的影响变成了指数级。

而 Compose 是不怕层级嵌套的,因为它从根源上解决了这种问题。它解决的方式也非常巧妙而简单——它不许重复测量。

Compose 通过一种叫做 Intrinsic Measurement(固有特性测量)的机制,避免了随着层级增多,重复测量导致绘制时间指数式增加的性能陷阱,也就是说,使用 Compose 时疯狂嵌套,和把所有组件写在同一层级里,性能上是一样的!这是比起原体系的一大进步。

配合其他 Jetpack 组件

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

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

Compose 可以配合现有 Jetpack 组件的使用,例如 ViewModel、LiveData 等,对于一个标准的 Jetpack MVVM项目,将很容易将 UI 部分替换成 Compose。

Composalbe 中调用 viewModel() 可以获取当前 Context 的 ViewModel, observeAsState() 将 LiveData 转换为 Compose State 并建立绑定。当 LiveData 变化时,ConversationScreen 会发生重组,内部的 MessageLit 、MessageItem 由于依赖了参数 messages,都会参与重组。

功能完备的UI系统

Compose目前的 UI 系统功能完备,可以完全覆盖 Android 现有视图系统的所有能力。

开发中预览

目前的基于 xml 的预览效果很鸡肋,导致很多开发者都习惯于实机运行查看UI。Compose 预览机制可以做到与真机无异,真正的所见所即得。

预览时只需创建一个无参的 Composalbe,并添加 @Preview 注解即可。

@Preview
@Composable
fun PreviewGreeting() {
    Greeting("Android")
}

与现有体系良好的互操作性

Compose 能够与现有 View 体系能一起使用,比如在现有布局中使用 Compose,或在 Compose 布局中使用旧视图体系。所以迁移到 Compose 很方便,可以为一个已有项目先引入 Compose,再逐渐切换,不要求一次性将旧UI全替换为新的,有很大的缓冲空间。

实践

现在来尝试动手写一个简单的 Compose 界面。

配置 Kotlin
plugins {
    id("org.jetbrains.kotlin.android") version "1.4.32"
}
配置 Gradle
android {
    defaultConfig {
        ...
        minSdkVersion(21)
    }

    buildFeatures {
        // Enables Jetpack Compose for this module
        compose = true
    }
    ...

    // Set both the Java and Kotlin compilers to target Java 8.

    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }

    kotlinOptions {
        jvmTarget = "1.8"
        useIR = true
    }

    composeOptions {
        kotlinCompilerVersion = "1.4.32"
        kotlinCompilerExtensionVersion = "1.0.0-beta07"
    }
}
添加 Jetpack Compose 工具包依赖项
dependencies {
    implementation("androidx.compose.ui:ui:1.0.0-beta07")
    // Tooling support (Previews, etc.)
    implementation("androidx.compose.ui:ui-tooling:1.0.0-beta07")
    // Foundation (Border, Background, Box, Image, Scroll, shapes, animations, etc.)
    implementation("androidx.compose.foundation:foundation:1.0.0-beta07")
    // Material Design
    implementation("androidx.compose.material:material:1.0.0-beta07")
    // Material design icons
    implementation("androidx.compose.material:material-icons-core:1.0.0-beta07")
    implementation("androidx.compose.material:material-icons-extended:1.0.0-beta07")
    // Integration with observables
    implementation("androidx.compose.runtime:runtime-livedata:1.0.0-beta07")
    implementation("androidx.compose.runtime:runtime-rxjava2:1.0.0-beta07")

    // UI Tests
    androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.0.0-beta07")
}
用 Compose 来重新写一下 账号登录 页面:
class MainActivity : ComponentActivity() {

    private val isLogInning = mutableStateOf(false)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            LoginScreen(isLogInning)
        }
    }

    private fun login() {
        isLogInning.value = true
        Toast.makeText(this, "登录中", Toast.LENGTH_SHORT).show()
    }

    @Composable
    fun LoginScreen(isLogInning: MutableState<Boolean> = mutableStateOf(false)) {
        Column {
            Image(
                painter = painterResource(R.drawable.titlebar_back_light),
                contentDescription = null,
                modifier = Modifier
                    .height(40.dp)
                    .width(15.dp)
                    .absoluteOffset(10.dp)
            )
            Text(
                text = "登录TP-LINK ID",
                fontSize = 30.sp,
                fontWeight = FontWeight.Bold,
                modifier = Modifier.padding(10.dp)
            )
            TextField(
                value = TextFieldValue(),
                onValueChange = {},
                placeholder = { Text(text = "TP-LINK ID") },
                colors = TextFieldDefaults.textFieldColors(
                    backgroundColor = Color.White,
                    placeholderColor = Color.LightGray
                ),
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(horizontal = 10.dp)
            )
            TextField(
                value = TextFieldValue(),
                onValueChange = {},
                placeholder = { Text(text = "密码") },
                colors = TextFieldDefaults.textFieldColors(
                    backgroundColor = Color.White,
                    placeholderColor = Color.LightGray
                ),
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(horizontal = 10.dp)
            )
            Text(
                text = "忘记密码",
                fontSize = 16.sp,
                color = Color.Gray,
                textAlign = TextAlign.End,
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(horizontal = 10.dp, vertical = 20.dp)
            )
            Button(
                content = {
                    if (isLogInning.value)
                        Text("登录中...")
                    else
                        Text("登录")
                },
                onClick = { login() },
                colors = ButtonDefaults.buttonColors(
                    backgroundColor =
                        if (isLogInning.value)
                            Color(0xFFA6B7F7)
                        else
                            Color(0xFF3C65FC),
                    contentColor = Color.White
                ),
                modifier = Modifier
                    .height(65.dp)
                    .fillMaxWidth()
                    .padding(10.dp)
            )
            Row {
                Text(
                    text = "新用户注册",
                    fontSize = 16.sp,
                    color = Color(0xFF3C65FC),
                    modifier = Modifier.padding(10.dp)
                )
                Text(
                    text = "暂不登录",
                    fontSize = 16.sp,
                    color = Color.Gray,
                    modifier = Modifier
                        .padding(10.dp)
                        .offset(205.dp)
                )
            }
        }
    }

    @Preview(showBackground = true)
    @Composable
    fun DefaultPreview() {
        LoginScreen()
    }
}
上一篇 下一篇

猜你喜欢

热点阅读