玩看看 Jetpack Compose 也許你會開始有興趣了

GivemepasS
15 min readJan 24, 2022

看到 Android 開發群組突然在討論 Jetpack Compose 的開發,就很好奇是什麼?

https://developer.android.com/jetpack/compose

看了一下,怎麼跟 Flutter 有點像?難道 Android 也要走宣告式 UI 的模式了嗎?查了一下相關資源,有了, Youtube 也有相關教學可以參考。

https://www.youtube.com/watch?v=qvDo0SKR8-k

所以我就來練習一下怎麼寫一個 Jetpack Compose 的 App。

寫之前可能需要看一下相關文件,可以到官方 Release Note 上面看到什麼時候釋出 Jetpack Compose 。

https://developer.android.com/jetpack/androidx/versions/all-channel#july_28_2021

原來在 2021/7/28 就釋出第一版的 UI,所以就可以來看看如果要使用這個元件,要怎麼導入呢?

https://developer.android.com/jetpack/androidx/releases/compose-ui#1.0.0

原來還有官方部落格,可以參考看看 Blog 寫些什麼?

大致上描述 Compose 元件的功能、用途,以及如果你想要佈局完直接可以 preview,那就必須下載 Android Studio Arctic Fox 這版開發 IDE,就可以看到以下畫面。

很棒欸,基本上就是用程式碼取代了xml,而且可以即時的反應佈局,以及資料的呈現方式,這樣一來就不需要等到真的安裝的時候,就看到實際資料套進 UI 的狀況長怎樣,這簡直是一大便利性。

裡面還講到幾個優點,例如可以和現有產品無痛結合、更換 Theme 更為方便,Lazy components 讓列表更有效率,表現更佳等…諸多優點。

看到這些東西,已經躍躍欲試這個新版功能了,那就開始吧!

打開我們的 Android Studio -> New Project

看到有選項可以直接建構一個空的 Compose Activity,我建立了一個 FirstTimeCompose 的專案,選好路徑與專案名稱以後就可以直接來看看預設有哪些東西。

很簡單的開啟一個新專案,可以看到預設的程式碼有一大串,如下。

首先,你會發現繼承的類別變成了 ComponentActivity 了,另外還有一堆奇怪的東西,例如 setContentView 不見了,變成 setContent 的定義,還多了幾個方法,旁邊還多出 preview 的畫面。

一開始先不著急著要開始動手寫,先看看為什麼會有這些轉變?可以看一下 Project 的結構,你會發現 res 下面沒有 layout 資料夾了。

如同前面看到的資料,Compose 變成宣告式 UI 的方式,也就是你必須在程式內定義你的 UI。

再來看一下 Gradle,發現裡面多了一些東西。

看一下版本是多少?

版本是 1.0.1(版本未來會有所變動)。

大致上了解如果要使用 Compose,那這些引入宣告就必須加入程式內。

那就讓我們一起來搞懂 Compose 是什麼東西吧!

一開始有一個 setContent 的宣告,從字面上意義看起來就是要設置整個畫面,最外層用 FirstComposeTheme 包起來,這看起來就是一個字定義的 Theme,我們可以點進去 source code 看看。

原來可以透過程式碼來快速定義不同的 Theme,這樣處理還真方便。

不過由於是一開始,我們就照著官方教學來進行會比較好,所以暫時先把一堆無關的程式碼先移除掉,專注在怎麼處理 Compose 元件,後續有機會再來研究 Theme。

https://developer.android.com/jetpack/compose/tutorial

根據上面連結的官方教學,一開始只需要宣告 Text 在螢幕上即可。

setContent {
Text(text = "Hello world.")
}

如果當你設置了這樣的程式碼,並且執行安裝到手機上,只會有一個 Text 寫上 Hello world 的字樣顯示在畫面上。

接著我們改成用方法呼叫,宣告一個方法來呼叫 Text,並且設置顯示的文字

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MessageCard("Android")
}
}

@Composable
fun MessageCard(name: String) {
Text(text = "Hello $name!")
}

你可以看到以下畫面。

等等,我們的 AS 旁邊怎麼會有一個 Preview 畫面一直顯示 No Preview found 呢?原來我們可以透過 preview 直接看到結果啊?

怎麼解決這個問題呢?其實很簡單,官方教學叫我們要在 @Composable 的方法上面加上 @Preview 這個註解即可。

所以我們在 MessageCard 方法上加上 @ Preview 這個關鍵字,奇怪了怎麼紅紅的呢?官方教學上有說明如下。

The composable function must provide default values for any parameters. For this reason, you can't preview the MessageCard() function directly.

原來 Composable 必須至少提供一個可以傳入任意一個參數的方法,所以解決辦法就是再宣告一個 Composable 的方法來使用 Preview。

這樣一來就可以看到沒有紅字了,也可以在 Preview 畫面看到我們預期的結果了。

原來 Compose 是這樣運作的,那我們可以試著再繼續改動一些屬性看看,先還看看 Text 有哪些屬性,點進去原始碼看一下。

還蠻多屬性的,試著來調整一些好了。

@Composable
fun MessageCard(name: String) {
Text(text = "Hello $name!", color = Color.Blue, modifier = Modifier.padding(3.dp), fontSize = 10.sp)
}

調了顏色、Padding以及字體大小,看一下結果。

有了,原來可以這樣設定,那就簡單多了,現在想要再加入一個 Text 元件看看。

@Composable
fun MessageCard(name: String) {
Text(
text = "Hello $name!",
color = Color.Blue,
modifier = Modifier.padding(3.dp),
fontSize = 10.sp
)
Text(text = "Hey, guys.")
}

奇怪了?怎麼 preview 會發現疊在一起?

啊!一定是少了佈局,就跟我們在xml 排佈局一樣,外面必須告訴 Android 佈局方式,所以我們來找看看 Compose 有哪些佈局?

https://developer.android.com/jetpack/compose/layouts/basics

裡面有許多佈局元件可以使用,例如 Column、Row、Box…等,我們來使用一些試看看效果。

首先加入 Column 試看看。

@Composable
fun MessageCard(name: String) {
Column {
Text(
text = "Hello $name!",
color = Color.Blue,
modifier = Modifier.padding(3.dp),
fontSize = 10.sp
)
Text(text = "Hey, guys.")
}
}

可以看到 Preview 畫面已經將兩個元件分開了。

Column 本身也可以設定屬性,試看看。

Column(modifier = Modifier.padding(10.dp)) {
Text(
text = "Hello $name!",
color = Color.Blue,
modifier = Modifier.padding(3.dp),
fontSize = 10.sp
)
Text(text = "Hey, guys.")
}

Column是垂直的往下放,我們來試看看 Row 怎麼佈局好了,另外我們放的元件改成沒玩過的 Button 看看,程式碼改成這樣好了。

@Composable
fun MessageCard(name: String) {
Column(modifier = Modifier.padding(10.dp)) {
Text(
text = "Hello $name!",
color = Color.Blue,
modifier = Modifier.padding(3.dp),
fontSize = 10.sp
)
Text(text = "Hey, guys.")
}
Button(onClick = { /*TODO*/ }) {

}
}

你會發現因為 Button 預設要傳入事件,而且因為沒有被納入佈局,因此,會出現前面一樣元件互相覆蓋的問題。

先用 Row 來解決這個問題。

@Composable
fun MessageCard(name: String) {
Row {
Column(modifier = Modifier.padding(10.dp)) {
Text(
text = "Hello $name!",
color = Color.Blue,
modifier = Modifier.padding(3.dp),
fontSize = 10.sp
)
Text(text = "Hey, guys.")
}
Button(onClick = { /*TODO*/ }) {

}
}
}

可以看到是橫的排列。

來調整一下 Button 的屬性好了,如果你想在 Button 上顯示文字,就必須在內部塞入一個 Text 元件。

@Composable
fun MessageCard(name: String) {
Row {
Column(modifier = Modifier.padding(10.dp)) {
Text(
text = "Hello $name!",
color = Color.Blue,
modifier = Modifier.padding(3.dp),
fontSize = 10.sp
)
Text(text = "Hey, guys.")
}
Button(
onClick = { /*TODO*/ },
modifier = Modifier.padding(10.dp),
colors = ButtonDefaults.buttonColors(Color.Cyan)
) {
Text(text = "push me.")
}
}
}

可以看到操作大同小異。

最後我們看一下在手機上執行的結果是否跟 Preview 一致?

基本佈局其實有幾種簡單的模式,

直的排列、橫的排列以及類似像 FrameLayout 的排列,更進階的還有 ConstraintLayout等…,之後遇到再寫一下好了。

Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.End
)

也可以透過程式來調整內容物的位置,就大概類似 gravity 的設定。

接著對於現階段看到 Button 你就想要寫事件控制它,那我們要怎麼設定事件呢?其實跟原本寫程式類似,就指派一個事件給 UI即可。

首先宣告一個參數 counter 來計算我們按了幾次,接著設定一個 onClick 事件,傳給這個組合式元件,透過這個元件來幫我們設定按下的這個事件,所以傳給 Button 吃這個事件,接著在旁邊的 Text 元件上顯示出來,由於”Hey, guys.” 這個 Text 在這邊沒什麼意義,所以我們先拿掉好了。

@Composable
fun MessageCard(counter: Int, onClick: () -> Unit) {
Row {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Hello, $counter!",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.h5
)
}
Button(
onClick = onClick,
modifier = Modifier.padding(10.dp),
colors = ButtonDefaults.buttonColors(Color.Cyan)
) {
Text(text = "click me")
}
}
}

接著我們回到呼叫這個元件的 PreviewMessageCard 方法進行調整,設定了一個變數來幫我們計算次數,並且把事件傳入,事件很簡單就是點一次就把counter 加一次,好了,來執行看看。

@Preview
@Composable
fun PreviewMessageCard() {
var counter = 0
MessageCard(counter = counter) {
counter++
}
}

啊,為啥不會動?來查一下原因,原來所有的 Compose 元件都是根據傳入的參數來進行元件重新組合(recomposition),也就是說當你傳進去的參數並沒有被記憶起來,所以你每次傳進去對他而言,他並沒有變動,因此,這個組合元件並不會跟預期一樣變動數字,那應該怎麼做呢?

官方網站教學有給答案,就是使用 remember 這個關鍵字,並且將變數設定為可變動的值。

var counter by remember { mutableStateOf(0) }

如果你使用 by 這個關鍵字記得 import 以下。

import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
//or
import androidx.compose.runtime.*

這樣一來就可以看到畫面上隨著你點選的次數而更新。

這樣大致上了解到 Compose 怎麼處理 UI 以及佈局,更多的元件操作或佈局流程後面再繼續研究。

--

--