一文带你了解Android最新 Jetpack Compose UI 框架
转自:掘金 - Andoter
Jetpack Compose 是用于构建原生界面的新款 Android 工具包。它可简化并加快 Android 的 UI 开发工作。使用更少的代码、强大的工具和直观的 Kotlin API,快速构建 App 的 UI。
目前Jetpack Compose 为 Alpha 版。所以需要在 Android Studio 的 Canary 版本才能体验。
1. 创建 Jetpack Compose 项目
在 Android Studio Canary 版本中已经提供了 Compose 的模板,在创建项目时选择 Empty Compose Activity 模板即可。

至此,就完成一个 Compose 项目的创建。除此之外,我们也可以选择导入 Jetpack Compose 示例应用。
2. Compose 函数使用
Compose 是一种以函数为依托的声明式 UI 构建方式。比如在 MainActivity.kt 中显示一个文本。
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Text("Hello Compose!")
        }
    }
}

这个与使用 XML 布局的方式差别很大,setContent 块定义了 Activity 的布局。我们不使用 XML 文件来定义布局内容,而是调用一个 Compose 函数,比如上面的 Text 函数。然后 Jetpack Compose 使用自定义 Kotlin 编译器插件将这些 Compose 函数转换为应用的界面元素。
2.1 Compose 函数
Jetpack Compose 是围绕 Compose 函数构建的,在开发时只需描述应用界面的样式布局和数据依赖关系,而不必关注界面的构建过程。给一个函数添加 @Composable 注解即创建了一个 Compose 函数。注意,Compose 函数只能在其他 Compose 函数的范围内调用。下面我们将上面示例中的 Text 移动到自定义的 Compose 函数中。
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            HelloCompose()
        }
    }
    
    @Composable
    fun HelloCompose() {
        Text("Hello Compose!")
    }
}
2.3 设置点击监听
除了使用 Text 函数,还有其它的基础函数供我们使用,比如 Button、Image 等。那么如何给 UI 控件设置点击监听呢?在 Compose 框架中提供了两种方式:
对于类似 Button函数的这种,提供了onClick函数式接口供外部设置点击监听;对于类似 Text函数这种,没有提供显式接口设置的,通过Modifier类设置点击监听;
Button 函数设置点击事件
@Composable
fun TextButton() {
    Button(
        onClick = {
            Log.d("Andoter", this.javaClass.name)
            Toast.makeText(this@MainActivity, "Button 点击", Toast.LENGTH_SHORT).show()
        }
    ) {
        Text(text = "Hello Compose!", color = Color.Red)
    }
}
通过设置 onClick 函数即可实现点击实现,注意 Button 函数本身没有设置文本内容,需要通过 Text 函数设置显示文本内容。
Text 函数设置点击事件
@Composable
fun ClickedText() {
    val modifier = Modifier.clickable(onClick = {
        Log.d("Andoter", this.javaClass.name)
        Toast.makeText(this@MainActivity, "Button 点击", Toast.LENGTH_SHORT).show()
    })
    Text(text = "Hello Compose!",modifier = modifier.padding(10.dp))
}
通过 Modifier.clickable 的方式实现设置点击事件。Modifier 类不仅能够设置点击事件,还能够设置控件的布局属性。
clickable():设置点击监听padding():在元素周围留出空间fillMaxWidth():使可组合项填充其父项给它的最大宽度preferredSize():指定元素的首选宽度和高度
2.4 预览
在 Compose 框架中为 Compose 函数提供预览能力,通过给 Compose 函数添加 @Preview 注解即可进行预览。在实际的开发中,预览函数不要发布到线上,所以最佳做法是单独创建不会被应用调用的预览函数用于查看实际效果,专门的预览函数可以提高性能,并且有利于以后更轻松地设置多个预览。

(动图图片过大,无法上传,截图处理)
3. 布局
在 Jetpack Compose 中一切的元素都是围绕 Compose 函数展开,所以布局也是通过对应的内置 Compose 函数实现。
3.1 Column 和 Row
二者的特点:
Column:使元素按照竖直方向排列;Row:使元素按照水平方向排列;Stack:将一个元素放在另一个元素上。
这里我们以 Column 函数作为示例。
@Preview
@Composable
fun MultiText() {
    Text(text = "Hello Compose!")
    Text("Ant 学习 Compose!")
}
@Preview
@Composable
fun ColumnText() {
    Column {
        Text(text = "Hello Compose!")
        Text("Ant 学习 Compose!")
    }
}
通过 Column 可将组件按照竖直方向排列,预览效果对比:

3.2 ScrollableRow 和 ScrollableColumn
使用 ScrollableRow 或 ScrollableColumn 可使 Row 或 Column 内的元素滚动。
@Composable
fun ProductList() {
    ScrollableColumn(Modifier.fillMaxSize()) {
        listOf("Ant", "Andoter", "小伟").forEach { value ->
            ProductDetailView(value)
        }
    }
}
@Composable
fun ProductDetailView(text: String) {
    val image = imageResource(id = R.drawable.header)
    Column(modifier = Modifier.padding(16.dp)) {
        val imageModifier = Modifier
            .preferredHeight(180.dp)
            .clip(shape = RoundedCornerShape(5.dp))
            .fillMaxWidth()
            .clickable(onClick = {
                Log.d("Ant", "click");
            })
        Image(image, modifier = imageModifier, contentScale = ContentScale.Crop)
        Spacer(modifier = Modifier.preferredHeight(16.dp))
        Text("Hello Compose!")
    }
}

4. Compose 界面结构
通过上面的介绍,对 Compose 有了一个初步的认识,那么 Compose 函数如何绘制在屏幕上的呢?以什么样的形式展示的呢?我们使用 Layout Inspector 工具查看一个 Compose 页面。

通过左侧的布局结构可以发现,Compose 框架中已经废弃原有的View 体系中的控件(TextView、Button、ImageView 等),而是使用 AndroidComposeView(继承 ViewGroup)、ViewLayerContainer(继承 ViewGroup)和 ViewLayer(继承 View) 控件实现,其中 ViewLayer 代表每个 View 控件视图。
查看 ViewLayer 的调用关系,可以得到视图的生成关系:LayerWrapper → AndroidComposeView -> ViewLayer。

5. Compose 对业务的影响
Jetpack Compose 是一个适用于 Android 的新式声明性界面工具包,同时点击监听的设置方式也发生较大变化,那么对于我来说,最直观的业务影响是无法继续使用原有的插码技术进行点击事件的采集。这块需要进行调研适配。
上面提到设置点击的两种方式,本质上都是通过 Modifier 进行实现,来看下面的一个例子。
@Composable
fun ClickedText() {
    val modifier = Modifier.clickable(onClick = {
        Log.d("Andoter", this.javaClass.name)
        Toast.makeText(this@MainActivity, "Button 点击", Toast.LENGTH_SHORT).show()
    })
    Text(text = "Hello Compose!", modifier = modifier.padding(10.dp))
}
通过 Modifier 给一个 Text 设置点击监听,在点击的时候弹出一个 Toast。反编译看看最后的实现。
/* access modifiers changed from: package-private */
@Metadata(mo23161bv = {1, 0, 3}, mo23164k = 3, mo23165mv = {1, 4, 0})
/* compiled from: MainActivity.kt */
public final class MainActivity$TextButton$1$1 extends Lambda implements Function0<Unit> {
    private final /* synthetic */ MainActivity $this;
    /* JADX INFO: super call moved to the top of the method (can break code semantics) */
    MainActivity$TextButton$1$1(MainActivity mainActivity) {
        super(0);
        this.$this = mainActivity;
    }
    @Override // kotlin.jvm.functions.Function0
    public final void invoke() {
        Log.d(LiveLiterals$MainActivityKt.INSTANCE.mo17059x27db7fde(), this.$this.getClass().getName());
        Toast.makeText(this.$this, LiveLiterals$MainActivityKt.INSTANCE.mo17064x88044b3e(), 0).show();
    }
}
Kotlin 经过处理最终是转换成一个继承 Lambda 并实现 Fuction0 接口的类来托管实现点击监听。这样我们就可以总结出 Hook 条件:
kotlin.jvm.internal.Lambda的子类实现 kotlin.jvm.functions.Function接口被 public和final修饰符的invoke方法
尽管 Hook 点找到了,但是目前还无法突破获取对应的 View,依托 View 读取的属性就无法获取。希望可以一起跟你探讨下。
6. 总结
长期以来,Android 视图层次结构一直可以表示为界面微件树。界面更新方式是使用 findViewById() 等函数遍历树,这种手动操纵视图的方式会提高出错的可能性。在过去的几年中,移动端已开始转向声明性界面模型,比如 Flutter、Swift UI,所以 Jetpack Compose 框架应该也是后续发展的一个方向。
PS:如果觉得我的分享不错,欢迎大家随手点赞、在看。
大家一起在评论区聊聊呗~
