使用Jetpack Compose構建Android UI

Jetpack Compose

Jetpack Compose 是一個獨立的 UI 工具包,它結合了響應式編程模型和 Kotlin 編程語言的簡潔性和易用性,旨在簡化 UI 開發。
它是完全聲明性的,意味著可以通過調用一系列將數據轉換為UI的函數來描述UI。當基礎數據更改時,框架會自動調用這些函數,從而更新視圖層次結構。
現在的版本還是 0.1.0-dev02,處于非常早期的版本,官方也再三強調非常有可能產生變化且無法用于生產環境。不過簡單了解下 Compose 還是不錯的。

1. 準備

要啟動新的Compose項目,請打開Android Studio 4.0,然后選擇啟動新的Android Studio項目:


Android Studio版本

創建新項目時,從可用模板中選擇“
Empty Compose Activity”,注意
minimumSdkVersion 至少為21及以上,“Language” 必須為kotlin:

創建項目

2. Jetpack Compose構建UI的特點

API

Button 繼承自 TextView,理論上我們只需要一個文本 + 可點擊的區域就可以了,但是由于 TextView 的特性,它本身是可以長按出現復制、選擇功能的,但是一個 Button 要這些功能有什么用呢?Jetpack Compose 的核心: 組合優于繼承,所有的 UI 都是通過組合實現,不存在繼承關系。

Code

目前的 UI 構建方式來說,寫一個自定義 View 需要實現測量和布局,響應用戶的行為需要實現大量的 Listener 事件,同時還要配合 XML 自定義屬性,非常繁瑣。而且以目前的View代碼量體積來說,想要完全優化重構是不現實的。發布一個全新的 UI 構建庫,從根本上解決問題,所以 Google 推出了全新的 Android UI 組件庫 Jetpack Compose。

Jetpack Compose 試圖改變原有的 UI 構建方式,同時帶來以下 4 點全新的改變:


Goals
  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中看到預覽。

Text preview

刷新UI

遵循單一職責原則。@Composable函數負責單個功能,該功能完全由該函數封裝。例如,如果要為某些組件設置背景色,則必須使用Surface可組合功能。

Text設置背景色,我們需要定義一個Surface包裹它。

@Composable
fun Greeting(name: String) {
    Surface(color = Color.Yellow) {
        Text (text = "Hello $name!")
    }
}
Text preview

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按鈕查看預覽:

Modifiers

請注意,@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 是提供的可組合函數,用于創建水平分隔線。

preview

可以像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)
        }
    }
}
preview

不知道你有沒有這種想法,這里的for循環會不會就是把 Text 翻譯為 TextView,然后此方法就是接收一個 List<String> 對象,返回一個 List<TextView>?
顯示布局邊界看下:


preview

事實并非如此,它所有的可繪制元素都不是 Android 原生的 View,其頂層View為AndroidComposeView,內部在維護的 ComponentNode負責繪制。


AndroidComposeView

數據流
通過將對象作為參數傳遞給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向上流動

數據隨參數向下流動,事件隨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)
    }
}
Model

感覺有種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()
    }
}

刷新預覽:


preview
@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)
    )
}
preview

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的構建方式
上圖:

GenerateView

使用 Jetpack Compose 編寫的 View,可以無縫的通過 xml 在原有視圖上使用,只需要增加一個 @GenerateView 注解。

GenerateView

原有的 View 也支持 Jetpack Compose 寫法。目前在預覽版里@GenerateView注解還無法使用,不免有些遺憾~

自定義view

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,185評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,656評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,446評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,951評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,189評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,718評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,800評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,420評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,755評論 2 371