美文网首页Android开发
Jetpack Compose(一)--介绍

Jetpack Compose(一)--介绍

作者: Felix_Zhou | 来源:发表于2023-05-04 17:47 被阅读0次

    该章内容摘自Jetpack Compose从入门到实践一书中,个人认为该书对Jetpack Compose的介绍比较清晰、易懂。

    Jetpack Compose是什么

    Jetpack Compose(简称Compose)是Android新一代UI开发框架,致力于帮助开发者用更少的代码和更直观的API完成Native UI开发。相对于传统的UI开发方式,Compose具有以下几个方面的优势:

    • 先进的开发范式:Compose采用声明式的开发范式,开发者只需要聚焦在对UI界面的描述上,当需要渲染的数据发生变化时,框架将自动完成UI刷新。
    • 直观易用的API:基于Kotlin DSL打造的API紧贴函数式编程思想,相对于传统的视图开发方式,代码效率更高,实现同样的功能只需要以前一半的代码量。
    • 良好的兼容性:Compose代码与基于Android View系统的传统代码可以共存,用户可以按照喜欢的节奏将既有代码逐步过渡到Compose。
    • 广泛的适用性:Compose最低兼容到API 21,支持市面上绝大多数手机设备的使用;Jetpack以及各种常用三方库也都第一时间与Compose进行了适配。

    上述优势也使得Compose一经发布就广受追捧,目前已经有包括Twitter、Airbnb在内的众多应用采用了Compose开发UI, Compose的成熟度和稳定性也得到了市场的进一步验证。图1-1是一些使用了Compose的产品方对它的评价。

    谷歌为什么要推出Compose

    Andorid系统自诞生至今已发展了十几个年头,这期间智能手机无论是硬件规格还是软件形态都发生了巨大变化,Android应用开发技术也在不断进步:从RecyclerView、ConstraintLayout等各种UI控件的引入,到Architecture Components这样的架构工具,甚至连开发语言也从Java切换到了Kotlin。Andorid系统自诞生至今已发展了十几个年头,这期间智能手机无论是硬件规格还是软件形态都发生了巨大变化,Android应用开发技术也在不断进步:从RecyclerView、ConstraintLayout等各种UI控件的引入,到Architecture Components这样的架构工具,甚至连开发语言也从Java切换到了Kotlin。

    臃肿的父类视图控件也造成了子类视图功能的不合理。以最常见的Button类为例,为了能让按钮具备显示文字的功能,Button被设计成了继承自TextView的子类。这样的设计显然是不妥当的,TextView中许多不适于按钮的功能也会被Button一并继承下来,比如用户肯定不需要一个带有粘贴板的Button,而且随着TextView自身的能力迭代,Button有可能引入更多不必要的功能。另一方面,像Button这类基础控件只能跟随系统的升级而更新,即使发现了问题也得不到及时修复,长期下来积重难返,破窗效应也越发突出。如今很多新的视图组件都以Jetpack扩展库的形式单独发布,目的也是为了不受系统版本的制约。

    类似这样的问题在Android其他传统视图控件中还有很多,究其根源还是在于设计理念的落伍。构筑在基于面向对象思想的设计理念,让各个组件在定义时都偏向于封装私有状态。开发者需要花费大量的精力去确保各组件间状态的一致性,这也是造成命令式UI代码复杂度高的根本原因。因此,谷歌开始考虑寻找一套新的UI开发方式,希望从根本上替换现有的视图体系,彻底根除上述这些问题。谷歌高级工程师Jim Sproch(见图1-4)基于其在前端开发领域丰富的工作经验,开创性地提出了借助Kotlin Compiler Plugin为Android打造声明式UI框架的想法。在他的推动下,谷歌于2017年启动了Jetpack Compose项目(后文简称Compose项目),随后越来越多的工程师加入其中,Compose项目在谷歌内部越发受到重视。

    前面提到的Android传统视图体系中的一些问题,也随着Compose的出现得到了有效解决。表1-1展示了Compose与Android View的比较。


    示图.jpg

    这诸多优点中最大的创新还是对于声明式这一全新开发方式的采用。相对于传统的命令式开发方式,声明式开发大大提高了UI界面的开发效率。前端领域的React、Vue.js等主流开发框架都属于声明式开发框架,所以其先进性早已被广泛验证。

    命令式UI与声明式UI

    命令式和声明式是两种截然不同的编程范式。命令式用命令的方式告诉计算机如何去做事情(how to do),计算机通过执行命令达到结果,而声明式直接告诉计算机用户想要的结果(what to do),计算机自己去想该怎么做。

    Android现有的View视图体系就属于命令式的编程范式,我们使用XML定义的布局是静态的,无法根据响应状态自行更新。开发者需要通过findViewById等获取视图对象,然后通过命令式的代码调用对象方法驱动UI变更,而Compose采用声明式编程范式,开发者只需要根据状态描述UI,当状态变化时,UI会自动更新。

    也许有人会说Data Binding不是可以让XML自己“动”起来吗?没有错,Data Binding其实就是Compose诞生之前的一种声明式UI方案,谷歌曾经寄希望于通过它来提升UI编码效率。可见,声明式UI本身并非新鲜概念,而且其优势也早已被官方认可。

    举个例子,如下:
    传统的UI布局:

    <LinearLayout>
        <TextView>
           ... 
        </TextView>
        
        <Button>
           ... 
        </Button>
    
    </LinearLayout>
    

    声明式UI:

    Column {
        Text()
        Button()
    }
    

    数据更新UI时:
    命令式:

    TextView textView = findViewById(xxx);
    textView.setText(xxx);
    

    声明式:

    var str = xxx
    Column {
        Text(text = str)
        Button()
    }
    

    Compose API设计原则

    • 一切皆为函数
      Compose声明式UI的基础是Composable函数。Composable函数通过多级嵌套形成结构化的函数调用链,函数调用链经过运行后生成一颗UI视图树。视图树一旦生成便不可随意改变。视图的刷新依靠Composable函数的反复执行来实现。当需要显示的数据发生变化时,Compoable基于新参数再次执行,更新底层的视图树,最终完成视图的刷新。

      Composable函数只能在Composable函数中调用,这与挂起函数只能在协程或其他挂起函数中调用类似,都是在编译期保证的。在Compose的世界中,一切组件都是函数,由于没有类的概念,因此不会有任何继承的层次结构,所有组件都是顶层函数,可以在DSL中直接调用。视图构建由传统的实例构建过渡到如今的函数构建,开发者需要适应在心智上的转变,这也是我们学习中最需要关注的地方。

      最佳实践:
      Kotlin编码规范中要求函数的首字母小写,但是Compose推荐Composable使用首字母大写的名词来命名,且不允许有返回值。这样在DSL中书写时可读性更好。有的Composable函数并不代表UI组件,此时可以遵循一般的函数命名规范。

    • 组合优于继承
      组合优于继承,这是在面向对象设计模式中反复强调的原则。之所以反复强调,就是因为它遵守起来并不容易。因为继承用起来太过方便,大家往往难以从组合的视角思考问题。Android传统的视图系统中所有组件都直接或间接继承自View类。TextView继承自View,而Button又继承自TextView,处于末端的子类继承了很多无用的功能,导致出现本书开头提到的“带剪贴板的Button”这样的滑稽例子。而反观Compose,Composable作为函数相互没有继承关系,有利于促使开发者使用组合的视角去思考问题。

      比如Compose中为一个按钮添加文字的代码是下面这样:[插图]在传统视图体系中,按钮的文字可能是Button类的一个属性。而Compose中需要通过Text这个Composable来组合实现。虽然按钮显示文字是一个常见需求,但是对于一个只需要显示图片的IconButton来说,文字的属性就是多余的。Button真正必要的能力就是接收用户点击而已,Compose通过组合的方式让组件的职责更加单一。

    @Composable
    fun CustomButton(str: String) {
        Button(onClick = {}) {
            Text(text = str)
        }
    }
    
    • 单一数据源
      单一数据源(Single Source of Truth)是包括Compose在内的声明式UI中的重要原则。回想一下传统视图的EditText,它的文字变化可能来自用户的输入,也可能来自代码某处的setText。

      状态变化可能不止一个来源,即所谓的多数据源(Multiple Sources of Truth)。多数据源下的状态变化不容易跟踪,而且状态源过度分散会增加状态同步的工作量。比如EditText由于自己持有mText状态,其他组件需要监听它的状态变化,反之它可能也需要监听其他组件的状态变化。

      在Compose中,文本框组件OutlinedTextField的文字状态永远来自其参数value。
    @Composable
    fun OutlinedTextField(
        value: String,
        onValueChange: (String) -> Unit,
        ......
    )
    

    当用户输入文字后,onValueChange会接收到响应,但是文本框文字不会自动更新,仍然需要通过唯一来源value的变更来刷新UI。

    @Composable
    fun HelloComposable() {
        var content by rememberSaveable { mutableStateOf("") }
        HelloContent(content = content, onContentChange = {content = it})
    }
    
    @OptIn(ExperimentalMaterial3Api::class)
    @Composable
    fun HelloContent(content: String, onContentChange: (String) -> Unit) {
        Column {
            Text(text = "Hello $content")
            OutlinedTextField(
                value = content,
                onValueChange = onContentChange,
                label = { Text(text = content) }
            )
        }
    }
    

    在上面的代码中,OutlinedTextField响应用户输入后,通过onContentChange更新外部状态content,当content变化时会驱动HelloContent重新执行,重组中OutlinedTextField也会显示最新的content。

    单一数据源决定了Composable数据流的单向流动,数据(content)总是自上而下流动,而事件(onContentChange)总是自下而上传递

    Compose与View的关系

    我们都知道在传统视图体系中由View与ViewGroup构成视图树,而Compose中也有同样一颗视图树,它由LayoutNode构成,由Composition负责管理,如图所示:


    示图.jpg

    两种树的节点类型不同,但是它们并非全没关系,依然可以共存于一棵树中。就像DOM节点与View也不同,但是可以通过WebView显示在一棵树上,Compose也可以借助这样一个连接点挂载在View树上。使用Android Studio自带的Layout Inspector可以看到这个连接点就是ComposeView,它就是连接View与Compose的桥梁,如图所示:


    示图.jpg

    ComposeView有一个唯一子节点AndroidComposeView,它既是一个ViewGroup,也是LayoutNode视图树的持有者,它实现了LayoutNode视图结构与View视图结构的连接。既然AndroidComposeView已经承担了两套体系的连接,那为什么还要多一层ComposeView呢?

    ComposeView继承自AbstractComposeView,而后者有三个子类,分别对应着Activity窗口、Dialog窗口与PopupWindow窗口。Android平台存在所谓Window的概念,我们在很多场景下会有多窗口需求,例如在页面中弹出一个对话窗。AbstractComposeView的子类负责Android平台各类窗口的适配并生成对应的Composition, ComposeView作为其中一个子类负责Activity窗口的适配。总体来说,ComposeView负责对Android平台的Activity窗口的适配,AndroidComposeView负责连接LayoutNode视图系统与View视图系统。如此的职责划分可以实现上层视图适配与下层窗口适配逻辑的解耦

    Jetpack Compose不只是UI框架

    Compose并非一个简单的SDK,它是由一系列库及配套工具组成的完整的UI解决方案,如图所示:


    示图.jpg

    在开发阶段,Android Studio为我们提供了代码的实时静态检查,以及对Compose UI的实时预览功能,在编译阶段,Compose Compiler Plugin会对@Composable注解进行预处理,通过插入代码,提升了编码效率。在运行阶段,Compose从上到下分为四层,每一层都可以被单独使用,在不同维度提供能力支持,如图所示:


    示图.jpg

    可以只使用Compose的Runtime层构建任何基于数据驱动能力的系统或类库。在这样清晰的分层结构下,我们甚至可以隔离那些平台相关代码,自底向上自己来实现跨平台的UI系统。

    盖章内容摘自Jetpack Compose从入门到实践一书中,个人认为该书对Jetpack Compose的介绍比较清晰、易懂。

    相关文章

      网友评论

        本文标题:Jetpack Compose(一)--介绍

        本文链接:https://www.haomeiwen.com/subject/hscijdtx.html