声明式UI,更简单的自定义,实时带交互的预览功能
Compose 并不是类似于Recyclerview的高级控件,而是直接抛弃了View,ViewGroup那套东西,从上到下鲁了一套全新的框架,直白点说就是它的渲染机制,布局机制,触摸算法,以及UI 具体写法全都是新的。
Compose 实现了声明式UI,替代传统的命令是UI。可能对于我们来说第一个问题就是什么是声明式,什么是命令式UI。
首先看一下声明式UI长什么样。
Compose是用kotlin来写的,它的每一个控件都是一个函数调用。Text()
Text()并不会创建对象,它不是一个构造函数,而是一个普通函数。
为了辨识度,Compose函数开头都是大写。也就是一个Composable。
有人会疑问Text()地层到底是什么,是个Textview吗?其实并不是它是更下层Canvas那套东西。
Compose各个组件都是独立的新实现。
看完它的写法,看刚才的问题什么是声明式UI,它这么写怎么就声明式了,它和我们一直以来的写法有什么区别。
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="hello"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="world"
/>
</LinearLayout>
这是传统写法,还需要在java中 findviewbyId获取控件对象,来进行操作。
其实对于布局来说,写法大同小异,但是为什么传统写法叫命令式,而Compose叫声明式。
对于声明式来说,你只需要把控件声明出来,而不需要手动更新,如左边对应的Textview 它对应的数据改变了,我们怎么去更改数据呢,首先是findviewbyId获取控件对象,然后setText来进行数据变更。
而如果用Compose呢,则不用更新。当数据改变时Compose会自动更新到页面上。
这个自动订阅功能很容易使用,你只要在初始化时加上by mutableStateOf,剩下全由Compose自动搞定。
这个神奇的特性,是利用了kotlin的Property Delegation来实现的,也就说明了为什么Compose只能用kotlin来编写,因为使用到了kotlin的许多特性,而这些特性使用java 不能简单实现。
下面举例说明。
这就是所谓的声明式UI,你只要声明页面是什么样子,不用手动去更新,因为页面会自动更新,传统页面如果数据发生改变,要使用代码,给出详细步骤命令代码去更新,这就是所谓的命令式。
传统的所谓命令式,并不在XML部分,而是在于java部分,java代码去指挥去命令控件去更新,这才是命令式含义所在,而Compose通过订阅机制来更新UI,不需要指挥控件去更新,所以是声明式。
它并不是语言角度的定义,也不是写法角度定义,而是功能角度。
换句话说如果传统的XML能通过数据和页面进行关联,让页面自动更新,而不是让咱们去指定更新,那么它也是声明式。
声明式UI 是强大的功能,并不是优秀的代码风格。
这里就会想到data binding,他们俩都是通过页面绑定数据,来进行自动更新,但是他们俩还是有关键区别的,data binding 通过数据更新只能是页面元素的值,而compose可以更新页面中的任何内容,包括结构。
如:通过一个boolean来判断一个页面的元素展示,当你把变量的值改变后,这个元素会从页面中完全消失,像从来没出现过一样,而不是像设置Visibility这种方式从视觉上隐藏,这两种策略,看起来差不多,但其实是种机制的改变,这种机制给页面带来的灵活性和性能的提升是非常大的。
除了android Compose外苹果的SwiftUi和Flutter 都是使用的是声明式。可见是一种趋势了。
在做页面开发时大家都知道尽量避免布局的嵌套,层级的增加会大幅度拖慢页面的加载,主要是因为各种layout的重复测量。
每增加一层都指数级增加页面的时长,而compose是不怕页面嵌套的,它从根源上解决了这个问题,它不允许重复测量。
它的处理方式是先对子View进行一个粗略的测量,根据内容尺寸,粗略的估计子View的大小。
固有尺寸测量 InTrinsic Measurement.
就是在Compose里疯狂嵌套组件,和把组件放在同以层级它的性能是一样的。
缺点:目前滑动列表场景,是比不过原生RecyclerView的,但是未来可期。
Compose 编程思想
声明性编程范式
长期以来,Android 视图层次结构一直可以表示为界面微件树。由于应用的状态会因用户交互等因素而发生变化,因此界面层次结构需要进行更新以显示当前数据。最常见的界面更新方式是使用 findViewById()
等函数遍历树,并通过调用 button.setText(String)
、container.addChild(View)
或 img.setImageBitmap(Bitmap)
等方法更改节点。这些方法会改变微件的内部状态。
手动操纵视图会提高出错的可能性。如果一条数据在多个位置呈现,很容易忘记更新显示它的某个视图。此外,当两项更新以意外的方式发生冲突时,也很容易造成异常状态。例如,某项更新可能会尝试设置刚刚从界面中移除的节点的值。一般来说,软件维护复杂性会随着需要更新的视图数量而增长。
在过去的几年中,整个行业已开始转向声明性界面模型,该模型大大简化了与构建和更新界面关联的工程设计。该技术的工作原理是在概念上从头开始重新生成整个屏幕,然后仅执行必要的更改。此方法可避免手动更新有状态视图层次结构的复杂性。Compose 是一个声明性界面框架。
重新生成整个屏幕所面临的一个难题是,在时间、计算能力和电池用量方面可能成本高昂。为了减轻这一成本,Compose 会智能地选择在任何给定时间需要重新绘制界面的哪些部分。这会对您设计界面组件的方式有一定影响,如重组中所述。
简单的可组合函数
使用 Compose,您可以通过定义一组接受数据而发出界面元素的可组合函数来构建界面。一个简单的示例是 Greeting 微件,它接受 String 而发出一个显示问候消息的 Text 微件。
声明性范式转变
在许多面向对象的命令式界面工具包中,您可以通过实例化微件树来初始化界面。您通常通过膨胀 XML 布局文件来实现此目的。每个微件都维护自己的内部状态,并且提供 getter 和 setter 方法,允许应用逻辑与微件进行交互。
在 Compose 的声明性方法中,微件相对无状态,并且不提供 setter 或 getter 函数。实际上,微件不会以对象形式提供。您可以通过调用带有不同参数的同一可组合函数来更新界面。这使得向架构模式(如 ViewModel
)提供状态变得很容易,如应用架构指南中所述。然后,可组合项负责在每次可观察数据更新时将当前应用状态转换为界面。
当用户与界面交互时,界面会发起 onClick 等事件。这些事件应通知应用逻辑,应用逻辑随后可以改变应用的状态。当状态发生变化时,系统会使用新数据再次调用可组合函数。这会导致重新绘制界面元素,此过程称为“重组”。
动态内容
由于可组合函数是用 Kotlin 而不是 XML 编写的,因此它们可以像其他任何 Kotlin 代码一样动态。例如,假设您想要构建一个界面,用来问候一些用户。
状态和组合
由于 Compose 是声明式工具集,因此更新它的唯一方法是通过新参数调用同一可组合项。这些参数是界面状态的表现形式。每当状态更新时,都会发生重组。因此,TextField 不会像在基于 XML 的命令式视图中那样自动更新。可组合项必须明确获知新状态,才能相应地进行更新。
fun HelloContent() {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Hello!",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.h5
)
OutlinedTextField(
value = "",
onValueChange = { },
label = { Text("Name") }
)
}
}
如果运行此代码,您将不会看到任何反应。这是因为,TextField 不会自行更新,但会在其 value 参数更改时更新。这是因 Compose 中组合和重组的工作原理造成的。
可组合项中的状态
可组合函数可以使用 remember
可组合项记住单个对象。系统会在初始组合期间将由 remember
计算的值存储在组合中,并在重组期间返回存储的值。remember
既可用于存储可变对象,又可用于存储不可变对象。
mutableStateOf
会创建可观察的 MutableState<T>
,后者是与 Compose 运行时集成的可观察类型。
value 如有任何更改,系统会安排重组读取 value 的所有可组合函数。对于 ExpandingCard,每当 expanded 发生变化时,都会导致重组 ExpandingCard。
在可组合项中声明 MutableState 对象的方法有三种:
val mutableState = remember { mutableStateOf(default) }
var value by remember { mutableStateOf(default) }
val (value, setValue) = remember { mutableStateOf(default) }
布局
Column 竖向布局
Row 横向布局
Box 可将一个元素放在另一个元素上。
如需在 Row 中设置子项的位置,请设置 horizontalArrangement 和 verticalAlignment 参数。对于 Column,请设置 verticalArrangement 和 horizontalAlignment 参数:
滚动修饰符
verticalScroll
和 horizontalScroll
修饰符提供一种最简单的方法,可让用户在元素内容边界大于最大尺寸约束时滚动元素。利用 verticalScroll
和 horizontalScroll
修饰符,您无需转换或偏移内容。
列表
系统会对所有列表项进行组合和布局,无论它们是否可见,因此如果您需要显示大量列表项(或长度未知的列表),则使用 Column
等布局可能会导致性能问题。
Compose 提供了一组组件,这些组件只会对在组件视口中可见的列表项进行组合和布局。这些组件包括 LazyColumn
和 LazyRow
。
内容内边距 contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
内容外边距 verticalArrangement = Arrangement.spacedBy(4.dp)
修饰符
借助修饰符,您可以修饰或扩充可组合项。您可以使用修饰符来执行以下操作:
- 更改可组合项的大小、布局、行为和外观
- 添加信息,如无障碍标签
- 处理用户输入
- 添加高级互动,如使元素可点击、可滚动、可拖动或可缩放
修饰符是标准的 Kotlin 对象。您可以通过调用某个 Modifier
类函数来创建修饰符。您可以将以下函数连在一起以将其组合起来:
修饰符顺序很重要
修饰符函数的顺序非常重要。由于每个函数都会对上一个函数返回的 Modifier 进行更改,因此顺序会影响最终结果。让我们来看看这方面的一个示例:
@Composable
fun ArtistCard(/*...*/) {
}
为什么我们需要一个新的UI 工具?
在Android中,UI工具包的历史可追溯到至少10年前。自那时以来,情况发生了很大变化,例如我们使用的设
备,用户的期望,以及开发人员对他们所使用的开发工具和语言的期望。
以上只是我们需要新UI工具的一个原因,另外一个重要的原因是 View.java 这个类实在是太大了,有太多的代
码,它大到你甚至无法在Githubs上查看该文件,因为它实际上包含了 30000 行代码,这很疯狂,而我们所使用的几
乎每一个Android UI 组件都需要继承于View。
GogleAndroid团队的Anna-Chiara表示,他们对已经实现的一些API感到遗憾,因为他们也无法在不破坏功能的
情况下收回、修复或扩展这些API,因此现在是一个崭新起点的好时机。
这就是为什么Jetpack Compose 让我们看到了曙光。
Jetpack Compose的着重点
包括一下几个方面:
- 加速开发
- 强大的UI工具
- 直观的Kotlin API
Compose API 的原则
Compose是一个声明式UI系统,其中,我们用一组函数来声明UI,并且一个Compose
函数可以嵌套另一个Compose函数,并以树的结构来构造所需要的UI。
在Compose中,我们称该树为UI 图,当UI需要改变的时候会刷新此UI图,比如Compose函数中有 if 语句,那
么Kotlin编译器就需要注意了。
@Composable
fun checkbox ( ... )
@Composable
fun TextView ( ... )
@Composable
fun Edittext ( ... )
@Composable
fun Image ( ... )
在此过程中,Compose函数始终根据接收到的输入生成相同的UI,因此,放弃类结构不会有任何害处。从类结
构构建UI过渡到顶层函数构建UI对开发者和Android 团队都是一个巨大的转变,顶层函数还在讨论之中,还没有发布
release 版。
组合优于继承
Jetpack Compose首选组合而不是继承。 Compose会基于其他部分构建UI,但不会继承行为。
如果你经常关注Android或者对Android有所了解,你就会知道,Android中的几乎所有组件都继承于View类
(直接或间接继承)。比如 EidtText 继承于 TextView ,而同时 TextView 又继承于其他一些View,这样的继承机构
最终会指向跟View即 View.java 。并且 View.java 又非常多的功能。
而Compose团队则将整个系统从继承转移到了顶层函数。 Textview , EditText , 复选框 和所有UI组件都是
它们自己的Compose函数,而它们构成了要创建UI的其他函数,代替了从另一个类继承。
深入了解Compose
Core
基本上,核心包含四个构建模块:
绘制(Draw)
布局(Layout)
输入(Input)
语义(Semantics)
1、Draw — Draw 给了你访问Canvas的能力,因此你可以绘制你要的任何自定义View
2、Layout — 通过布局,我们可以测量事物并相应地放置视图。
3、Input — 开发人员可以通过输入访问事件并执行手势
4、Semantics — 我们可以提供有关树的语义信息。
Foundation
Foundation的核心是收集上面提到的所有内容,并共同创建一个 抽象层 ,以使开发人员更轻松调用。
Material
在这一层,所有的Material组件将会被提供,并且我们可以通过提供的这些组件来构建复杂的UI。
这是Compose团队所做的出色工作中最精彩的部分,在这里,所有提供的View都有Material支持,因此,使用
Compose来构建APP, 默认就Material风格的,这使得开发者少了很多工作。