使用Jetpack Compose構建Android UI

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 點全新的改變:


  1. UI 的變化更新不再跟隨 Android 大版本的發佈而更新
  2. 編寫 UI 代碼不需要掌握龐大繁瑣的技術棧
  3. 簡單直接的狀態控制以及用戶行爲處理
  4. 使用更少的代碼來編寫 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組件提供其他修飾的屬性列表。目前可用的修飾符有:SpacingAspectRatio和修改Flexible Layouts佈局的RowColumn

@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 、會有更完善的動畫機制。一起期待吧~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章