Jetpack Compose 是一個獨立的 UI 工具包,它結合了響應式編程模型和 Kotlin 編程語言的簡潔性和易用性,旨在簡化 UI 開發。
它是完全聲明性的,意味著可以通過調用一系列將數據轉換為UI的函數來描述UI。當基礎數據更改時,框架會自動調用這些函數,從而更新視圖層次結構。
現在的版本還是 0.1.0-dev02,處于非常早期的版本,官方也再三強調非常有可能產生變化且無法用于生產環境。不過簡單了解下 Compose 還是不錯的。
1. 準備
要啟動新的Compose項目,請打開Android Studio 4.0,然后選擇啟動新的Android Studio項目:
創建新項目時,從可用模板中選擇“
Empty Compose Activity”,注意
minimumSdkVersion 至少為21及以上,“Language” 必須為kotlin:
2. Jetpack Compose構建UI的特點
Button 繼承自 TextView,理論上我們只需要一個文本 + 可點擊的區域就可以了,但是由于 TextView 的特性,它本身是可以長按出現復制、選擇功能的,但是一個 Button 要這些功能有什么用呢?Jetpack Compose 的核心: 組合優于繼承,所有的 UI 都是通過組合實現,不存在繼承關系。
目前的 UI 構建方式來說,寫一個自定義 View 需要實現測量和布局,響應用戶的行為需要實現大量的 Listener 事件,同時還要配合 XML 自定義屬性,非常繁瑣。而且以目前的View代碼量體積來說,想要完全優化重構是不現實的。發布一個全新的 UI 構建庫,從根本上解決問題,所以 Google 推出了全新的 Android UI 組件庫 Jetpack Compose。
Jetpack Compose 試圖改變原有的 UI 構建方式,同時帶來以下 4 點全新的改變:
- UI 的變化更新不再跟隨 Android 大版本的發布而更新
- 編寫 UI 代碼不需要掌握龐大繁瑣的技術棧
- 簡單直接的狀態控制以及用戶行為處理
- 使用更少的代碼來編寫 UI
說了這么多,用一下看看吧。
3. 使用Compose構建UI
新創建好的MainActivity
長這樣:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
Greeting("Android")
}
}
}
}
@Composable
fun Greeting(name: String) {
Text(text = "Hello $name!")
}
@Preview
@Composable
fun DefaultPreview() {
MaterialTheme {
Greeting2("Android")
}
}
使用setContent
用來定義布局,但不是使用XML文件,而是在其中調用Composable
函數。要創建可組合函數,只需將@Composable
注釋添加到函數。該函數可以調用其他的@Composable
函數。
@Composable
fun Greeting(name: String) {
Text(text = "Hello $name!") //Text是library提供的可組合函數。
}
可組合函數是帶有@Composable注釋標記的Kotlin函數
@Preview("Text preview")
@Composable
fun DefaultPreview() {
Greeting(name = "Android")
}
用@Preview
標記任何一個無參數的Composable
函數并Build項目,就可以在Android Studio中看到預覽。
遵循單一職責原則。@Composable
函數負責單個功能,該功能完全由該函數封裝。例如,如果要為某些組件設置背景色,則必須使用Surface
可組合功能。
給Text
設置背景色,我們需要定義一個Surface
包裹它。
@Composable
fun Greeting(name: String) {
Surface(color = Color.Yellow) {
Text (text = "Hello $name!")
}
}
Modifiers
Modifiers
是為UI組件提供其他修飾的屬性列表。目前可用的修飾符有:Spacing
,AspectRatio
和修改Flexible Layouts
布局的Row
和Column
。
@Composable
fun Greeting(name: String) {
Surface(color = Color.Yellow) {
//Spacing 為文本添加填充
Text(text = "Hello $name!", modifier = Spacing(24.dp))
}
}
點擊Build & Refresh
按鈕查看預覽:
請注意,@Composable注釋僅對創建UI的函數是必需的。它可以調用常規函數和其他Composables函數。如果某個功能不滿足這些要求,則不應使用@Composable注解。
創建通用Container
@Composable
fun MyApp(child: @Composable() () -> Unit) {
MaterialTheme {
Surface(color = Color.Yellow) {
child()
}
}
}
該函數以Composable
函數(在此稱為)的 lambda 作為參數,該 lambda child
返回Unit
。我們返回Unit
是因為所有Composable
函數都必須返回Unit
。
在
@Composable()
將 Composable 函數用作參數時,需要添加注解:
fun MyApp(child: @Composable() () -> Unit) { ... }
后面代碼就可以如此調用:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApp {
Greeting("Android")
}
}
}
}
@Preview("Text preview")
@Composable
fun DefaultPreview() {
MyApp {
Greeting("Android")
}
}
將UI組件提取到Composable函數中,以便我們可以重復使用它們而無需復制代碼。比如使用不同的參數重用同一Composable函數。以垂直順序排列,我們使用ColumnComposable函數(類似于垂直LinearLayout)。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApp {
MyScreenContent()
}
}
}
}
@Composable
fun MyScreenContent() {
Column {
Greeting("Android")
Divider(color = Color.Black)
Greeting("there")
}
}
@Composable
fun Greeting(name: String) {
Text (text = "Hello $name!", modifier = Spacing(24.dp))
}
@Preview("MyScreen preview")
@Composable
fun DefaultPreview() {
MyApp {
MyScreenContent()
}
}
Divider
是提供的可組合函數,用于創建水平分隔線。
可以像Kotlin中的任何其他函數一樣調用compose函數。可以添加語句來影響UI的顯示方式,構建UI非常方便。
@Composable
fun MyScreenContent(names: List<String> = listOf("Android", "there")) {
Column {
for (name in names) {
Greeting(name = name)
Divider(color = Color.Black)
}
}
}
不知道你有沒有這種想法,這里的for循環會不會就是把 Text 翻譯為 TextView,然后此方法就是接收一個 List<String> 對象,返回一個 List<TextView>?
顯示布局邊界看下:
事實并非如此,它所有的可繪制元素都不是 Android 原生的 View,其頂層View為AndroidComposeView,內部在維護的 ComponentNode負責繪制。
數據流
通過將對象作為參數傳遞給Composable函數,數據向下流動。
@Composable
fun MyExampleFunction(items: List<Item>) {
Column {
for (item in items) {
RenderItem(item = item)
}
}
}
@Composable
fun RenderItem(item: Item) {
Row {
Text(text = item.name)
WidthSpacer(4.dp)
Text(text = item.description)
}
}
RenderItem從調用Composable函數接收其所需的數據作為參數。如果我們要處理Item單擊,則使用lambda 將信息從層次結構的底部傳遞到頂部。
@Composable
fun MyExampleFunction(items: List<Item>, onSelected: (Item) -> Unit) {
Column {
for (item in items) {
RenderItem(item = item, onClick = { onSelected(item) })
}
}
}
@Composable
fun RenderItem(item: Item, onClick: () -> Unit) {
Clickable(onClick = onClick) {
Row {
Text(text = item.name)
WidthSpacer(4.dp)
Text(text = item.description)
}
}
}
數據隨參數向下流動,事件隨lambda向上流動。
使用@Model管理狀態
對狀態更改做出反應是Compose的核心。如果數據發生更改,則可以使用新數據調用Composable函數將數據轉換為UI,從而更新UI。
Compose使用自定義的Kotlin編譯器插件,當基礎數據發生更改時,可以重新調用函數以更新UI視圖。
Compose提供了@Model注解,該注解可以放在任何類上。如果數據發生更改,從@Model參數讀取值的可組合函數將自動被調用。該@Model注解將導致編譯器重寫類,使它可觀察和線程安全。可組合函數將自動訂閱它讀取的類的任何可變變量。如果它們發生變化,將重新組合讀取這些字段。
舉個栗子,比如做一個計數器,跟蹤用戶單擊多少次Button
:
@Model
class CounterState(var count: Int = 0)
在CounterState加上注解@Model,任何將此類作為參數的Composable函數在count值更改時將自動重新組合。定義Counter為一個Composable函數,該函數采用CounterState一個參數,并發出Button,顯示單擊了多少次。
@Composable
fun Counter(state: CounterState) {
Button(
text = "I've been clicked ${state.count} times",
onClick = {
state.count++
}
)
}
每次count更改時,Button都會重新構成并顯示的新值count。
@Composable
fun MyScreenContent(
names: List<String> = listOf("Android", "there"),
counterState: CounterState = CounterState()
) {
Column {
for (name in names) {
Greeting(name = name)
Divider(color = Color.Black)
}
Divider(color = Color.Transparent, height = 32.dp)
Counter(counterState)
}
}
感覺有種JS上Object.setProperty
的即時感,確實如官方所說, Jetpack Compose 受到了 React、Litho、Vue、Flutter 的啟發。
布局
與屏幕中心對齊,我們可以使用列的crossAxisAlignment參數:
@Composable
fun MyScreenContent(
names: List<String> = listOf("Android", "there"),
counterState: CounterState = CounterState()
) {
Column(crossAxisAlignment = CrossAxisAlignment.Center) {
for (name in names) {
Greeting(name = name)
Divider(color = Color.Black)
}
Divider(color = Color.Transparent, height = 32.dp)
Counter(counterState)
}
}
@Preview("MyScreen preview")
@Composable
fun DefaultPreview() {
MyApp {
MyScreenContent()
}
}
刷新預覽:
@Composable
fun Counter(state: CounterState) {
Button(
text = "I've been clicked ${state.count} times",
onClick = {
state.count++
},
style = ContainedButtonStyle(color = if (state.count > 5) Color.Green else Color.White)
)
}
Compose 對 ConstraintLayout 的支持還正在進行中。對于現有的布局控件,以后應該都是會添加支持的。
Compose 提供了 VerticalScroller 和 HorizontalScroller 來生成列表,在使用上與 RecyclerView 是完全不同的體驗:
@Composable
fun MyApp() {
VerticalScroller {
Column {
repeat(20) {
Row(mainAxisSize = LayoutSize.Expand) {
Container(height = 48.dp) {
Text("Item $it", modifier = Spacing(left = 16.dp))
}
}
}
}
}
}
事實上,Scroller 與 ScrollView 更接近,只提供了一個滾動的功能,并沒提到有對 View 進行回收復用。
兼容現有UI的構建方式
上圖:
使用 Jetpack Compose 編寫的 View,可以無縫的通過 xml 在原有視圖上使用,只需要增加一個 @GenerateView 注解。
原有的 View 也支持 Jetpack Compose 寫法。目前在預覽版里@GenerateView
注解還無法使用,不免有些遺憾~
自定義view
@Preview
@Composable
fun errorView() {
val checkBox = @Composable {
Draw { canvas: Canvas, parentSize: PxSize ->
val size = parentSize.width.value
val outer = RRect(0f,0f,size,size).withRadius(Radius(10f, 10f))
canvas.drawRRect(outer, Paint().apply {
color = Color.Red
})
}
Draw { canvas: Canvas, parentSize: PxSize ->
val paint = Paint().apply {
color = Color.White
strokeCap = StrokeCap.round
strokeWidth = 10f
isAntiAlias = true
}
val size = parentSize.width.value
val leftStart = Offset(size / 4, size / 4)
val leftEnd = Offset(size / 4 * 3, size / 4 * 3)
val rightStart = Offset(size / 4 * 3, size / 4)
val rightEnd = Offset(size / 4, size / 4 * 3)
canvas.drawLine(leftStart, leftEnd, paint = paint)
canvas.drawLine(rightStart, rightEnd, paint = paint)
}
}
Layout(children = checkBox) { _, _->
layout(IntPx(200), IntPx(200)){}
}
}
Jetpack Compose 中實現自定義 View 的過程也非常簡單,我們只需要關注 Draw 和 Layout 這兩個方法就好了,繪制過程和之前一樣,還是經過 measure、layout、draw ,但寫法很精簡。
Jetpack Compose帶給我們一種Android新的構建UI方式的實踐,從語法上來看還是有些Flutter的影子,聲明式UI和數據驅動帶給我們更多想象力。在未來的計劃中,Jetpack Compose 會支持 Kotlin 協程、會支持現有的 Android Arch Componet 、會有更完善的動畫機制。一起期待吧~