來聊聊我所認知的 Android 宣告式 UI 模式。
開始學習 Compose,先來了解一下什麼是 Compose, 它是一種聲明式的 UI 設計,什麼是聲明式的 UI 設計?Android 開發者很常用 XML 來進行開發,而在 Compose 上面可以使用程式碼來達到UI的宣告,透過程式碼我們就可以很容易來進行邏輯的拼裝。
這樣講應該是蠻抽象的或者對於第一次接觸這類的聲明式 UI 的工程師來說,會比較難以理解,假設大家都知道 XML 在 Android 上面是怎麼操作的,對於我們宣告一個 TextView 來說,首先要建立一個 XML 檔案,接著我們用一個佈局 UI 來包覆它,如下面程式碼所示。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/myTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello, ConstraintLayout!"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:padding="16dp"
android:textSize="18sp"
android:textColor="#000000" />
</androidx.constraintlayout.widget.ConstraintLayout>
接著你必須從 MainActivity 裡面把 Button 撈出來,如下面程式碼所示。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
myTextView.text = "Hello, World!"
}
}
這對於很多開發者來說相對熟悉,因為我們常常這樣做,那反過來思考,這會出現很多問題,譬如說,如果你想要從伺服器取值出來,如下所示。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 啟動協程執行非同步任務
GlobalScope.launch(Dispatchers.Main) {
// 在 IO 執行緒進行非同步操作(例如網路請求)
val result = fetchData()
// 切換回主執行緒設定 UI
withContext(Dispatchers.Main) {
myTextView.text = result
}
}
}
}
此時需求若為依據非同步回來的文字決定 TextView 顯示或不顯示,程式碼可能長這樣。
// 啟動協程執行非同步任務
GlobalScope.launch(Dispatchers.Main) {
// 在 IO 執行緒進行非同步操作(例如網路請求)
val result = fetchData()
// 判斷非同步回應是否為空
if (result.isNotEmpty()) {
// 非空,切換回主執行緒設定 UI
withContext(Dispatchers.Main) {
myTextView.text = result
}
} else {
// 如果為空,可以選擇隱藏 TextView
withContext(Dispatchers.Main) {
myTextView.visibility = View.GONE
}
}
}
這乍看之下好像沒什麼問題,但是如果類似的區塊一多,你就會出現很多畫面上的判斷,整個程式碼閱讀起來也會變得相對複雜,此時,若我們改用 Compose 寫會是怎樣的狀況?
@Composable
fun AsyncTextView() {
// Simulate an asynchronous task
LaunchedEffect(Unit) {
asyncResponse = fetchData()
}
// Display TextView only if the response is not empty
if (asyncResponse.isNotEmpty()) {
Text(
text = asyncResponse,
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
color = Color.Black
)
}
}
對,你沒看錯,我們只需要判斷非同步回來的字串是否有值,就可以透過判斷式來決定要不要顯示元件,這樣就是一個基礎的宣告式 UI 的邏輯了。
除了這樣以外還有狀態可以讓你的 UI 自動變化
狀態是什麼東西?想像一下,假設你現在被監控,你的一舉一動都被盯著,規則是一旦你做出逃跑的行為,馬上就會有警察衝出來抓住你,這樣就是你的所有狀態被監聽的意思。
畫面也是一樣,今天我們會透過 Android 內部的機制,去監控一個變數,一旦這個變數出現了變化,那我們畫面就會做出相對應的行為。
舉個例子,我們有一個變數 count,只要 Button 按一次,count 就會加 1,此時我們的 Text 這個畫面就會顯示多 1 的文字。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
// 使用 MutableState 來管理計數器的狀態
var count by remember { mutableStateOf(0) }
// Composable 函數,顯示計數器的值和一個按鈕
CounterApp(count = count, onCountChange = { newCount ->
count = newCount
})
}
}
}
@Composable
fun CounterApp(count: Int, onCountChange: (Int) -> Unit) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
// 顯示計數器的值
Text(text = "Count: $count", fontSize = 24.sp)
// 添加一個按鈕,點擊時增加計數器的值
Button(onClick = {
onCountChange(count + 1)
}) {
Text(text = "點擊加1")
}
}
}
我們將使用 MutableState 來管理這個計數器的狀態,在 Jetpack Compose 中,remember 是一個關鍵字,用於創建和保持 Compose 函數中的可變狀態,以確保在 Compose 樹重新構建時該狀態得以保留。
@Composable
fun Counter() {
// 使用 remember 來保留計數器的狀態
var count by remember { mutableStateOf(0) }
// 當按鈕被點擊時,增加計數器的值
Button(onClick = { count += 1 }) {
Text(text = "Count: $count")
}
}