什麼是宣告式 UI 的 Android Compose


Posted by 阿魯 on 2023-12-18

來聊聊我所認知的 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")
    }
}

#compose







Related Posts

1174. Immediate Food Delivery II

1174. Immediate Food Delivery II

作夢也在寫程式~讓 Siri 講睡前故事

作夢也在寫程式~讓 Siri 講睡前故事

【THM Walkthrough】Enumerating Active Directory

【THM Walkthrough】Enumerating Active Directory


Comments