Jetpack Compose(2) —— 入門實踐

一、項目中使用 Jetpack Compose

從此節開始,爲方便起見,如無特殊說明,Compose 均指代 Jetpack Compose。
開發工具: Android Studio

1.1 創建支持 Compose 新應用

新版 Android Studio 默認創建新項目即爲 Compose 項目。

注意:在 Language 下拉菜單中,Kotlin 是唯一可用的選項,因爲 Jetpack Compose 僅適用於使用 Kotlin 編寫的類。
在 Minimum API level dropdown 菜單中,選擇 API 級別 21 或更高級別。

1.2 爲現有應用設置 Compose

如果要在現有項目中使用 Compose,只需要將一下定義添加到應用的 build.gradle 文件中:

android {
    buildFeatures {
        compose = true
    }

    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.9"
    }
}
  • 在 Android BuildFeatures 代碼塊內將 compose 標誌設置爲 true 會啓用 Compose 功能。
  • ComposeOptions 代碼塊中定義的 Kotlin 編譯器擴展版本控制與 Kotlin 版本控制相關聯。請參閱兼容性對應圖,並選擇與項目的 Kotlin 版本匹配的庫版本。

1.3 添加依賴

dependencies {

    val composeBom = platform("androidx.compose:compose-bom:2024.02.01")
    implementation(composeBom)
    androidTestImplementation(composeBom)

    // Choose one of the following:
    // Material Design 3
    implementation("androidx.compose.material3:material3")
    // or Material Design 2
    implementation("androidx.compose.material:material")
    // or skip Material Design and build directly on top of foundational components
    implementation("androidx.compose.foundation:foundation")
    // or only import the main APIs for the underlying toolkit systems,
    // such as input and measurement/layout
    implementation("androidx.compose.ui:ui")

    // Android Studio Preview support
    implementation("androidx.compose.ui:ui-tooling-preview")
    debugImplementation("androidx.compose.ui:ui-tooling")

    // UI Tests
    androidTestImplementation("androidx.compose.ui:ui-test-junit4")
    debugImplementation("androidx.compose.ui:ui-test-manifest")

    // Optional - Included automatically by material, only add when you need
    // the icons but not the material library (e.g. when using Material3 or a
    // custom design system based on Foundation)
    implementation("androidx.compose.material:material-icons-core")
    // Optional - Add full set of material icons
    implementation("androidx.compose.material:material-icons-extended")
    // Optional - Add window size utils
    implementation("androidx.compose.material3:material3-window-size-class")

    // Optional - Integration with activities
    implementation("androidx.activity:activity-compose:1.8.2")
    // Optional - Integration with ViewModels
    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
    // Optional - Integration with LiveData
    implementation("androidx.compose.runtime:runtime-livedata")
    // Optional - Integration with RxJava
    implementation("androidx.compose.runtime:runtime-rxjava2")
}

我們看一下新建的項目中,自動生成的 Activity 的代碼如下:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            FirstComposeDemoTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    Greeting("Android")
                }
            }
        }
    }
}

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

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    FirstComposeDemoTheme {
        Greeting("Android")
    }
}

二、 Comppose API 設計原則

2.1 一切皆爲函數

Compose 聲明式 UI 的基礎是 Composable 函數,使用 Compose, 需要通過定義一組接收數據而渲染界面元素的可組合函數來構建界面。
看上面的最簡單的示例:Greeting widget, 它接收一個 String 並渲染出一個顯示問候消息的 Text widget。

@Composable
fun Greeting(name: String) {
    Text("Hello, $name")
}

運行效果如下:

對於需要渲染成界面的函數,稱之爲可組合函數,有一下特點:

  • 此函數帶有 @Composable 註釋,表明它是一個可組合函數,所有可組合函數都必須帶有此註釋。
  • 可組合函數需要在其它可組合函數的作用域內被調用。
  • 爲了與普通函數區分,約定可組合函數首字母大寫。

代碼中還有一個,帶有 @Preview 註解的 Composable 函數,顧名思義,該函數用來實時預覽效果的。點擊 design 選項,可看到預覽的樣式。Compose 強大的預覽功能,大家可以自行探索。

我們自定義 Greeting 組件,裏面實際上包含了一個 Text 組件, 點擊跳轉到 Text:

@Composable
fun Text(
    text: String,
    modifier: Modifier = Modifier,
    color: Color = Color.Unspecified,
    fontSize: TextUnit = TextUnit.Unspecified,
    fontStyle: FontStyle? = null,
    fontWeight: FontWeight? = null,
    fontFamily: FontFamily? = null,
    letterSpacing: TextUnit = TextUnit.Unspecified,
    textDecoration: TextDecoration? = null,
    textAlign: TextAlign? = null,
    lineHeight: TextUnit = TextUnit.Unspecified,
    overflow: TextOverflow = TextOverflow.Clip,
    softWrap: Boolean = true,
    maxLines: Int = Int.MAX_VALUE,
    onTextLayout: (TextLayoutResult) -> Unit = {},
    style: TextStyle = LocalTextStyle.current
) {
    // ...
}

可見框架提供的 Text 組件也是一個 Composable 函數。

Composable 函數通過多級嵌套形成結構化的函數調用鏈,函數調用鏈經過運行後生成 UI 一棵視圖樹。視圖樹一旦生成便不可隨意改變,視圖樹的刷新依靠 Composable 函數的反覆執行來實現,當需要顯示的數據發生變化時,Composable 基於新的參數再次執行,更新底層的視圖樹。最終完成視圖的刷新。
這個通過反覆執更新視圖樹的過程稱之爲重組。後面的文章再詳細介紹重組。

在 Compose 中,一切組件都是頂層函數,沒有類的概念,自然也不會有任何的繼承結構。

2.2 組合優於繼承

看一個常用控件,按鈕。

...
Surface(
    modifier = Modifier.fillMaxSize(),
    color = MaterialTheme.colorScheme.background
) {
    Column {
        Greeting("Android")
        Button(onClick = { /*TODO*/ }) {

        }
    }
}
...

爲了方便演示,我這裏增加了一個 Column 組件,相當於傳統 View 視圖中的垂直方向的線性佈局。然後在裏面增加了一個 Button 組件。

效果如上圖,界面上多了一個按鈕,我們沒有設置 button 的顏色,它卻默認與當前系統主題顏色適應了。點擊,還能看到水波紋效果。這是因爲我們使用的 Button 組件來自 Google material3 包裏面,自動適配了這些。由於我們沒有給按鈕設置文本,所以按鈕上並沒有文字顯示。那如 何給按鈕添加文本呢?

@Composable
fun Button(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    shape: Shape = ButtonDefaults.shape,
    colors: ButtonColors = ButtonDefaults.buttonColors(),
    elevation: ButtonElevation? = ButtonDefaults.buttonElevation(),
    border: BorderStroke? = null,
    contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    content: @Composable RowScope.() -> Unit
) {
    // ...
}

我們跳轉到 Button 的源碼,卻並沒有發現類似 Text 組件一樣的 text 參數。也就是說,我們並不能通過設置參數的方式,給 Button 組件設置文本,那要怎麼做?
從源碼我們看到,有兩個參數是沒有默認值的,需要我們調用時傳入。一個是 onClick, 這裏我們傳入了一個空的 lambda 表達式,另一個沒有默認值的參數 content,類型是 @Composable RowScope.() -> Unit,其實是要求傳入一個 Composable ,並且,它提供的作用域是 RowScope。看代碼就明白了:

...
Column {
    Greeting("Android")
    Button(onClick = { /*TODO*/ }) {

    }
}
...

我們成功給 Button 組件添加上了文字,不是通過參數的形式設置的,二是將一個 Button 組件和一個 Text 組件組合起來,形成了一個帶有文本的按鈕。仔細想一下,這樣的設計是否更合理,Button 本身的作用就是提供點擊時間,Text 提供文本作用的。從設計模式的角度來講,各個組件職責更單一。也變面出現了上文中提到的 “帶有剪貼板功能的按鈕” 這種問題。

這也是爲什麼說組合優於繼承。

2.3 單一數據源

單一數據源是包括 Compose 在內的所有聲明式 UI 框架的一個重要原則。
回想傳統 View 視圖中的 EditText 控件。它的文本變化可能來自用用戶的輸入,也可能來自代碼某處的 setText。這種多數據源在狀態變化的情況下不容易跟蹤,且狀態源過度分散,會增加狀態同步的工作量,比如 EditText 內部持有一個 mText 狀態,其它組件需要監聽它的狀態變化,同時,它還有可能需要監聽其它組件的狀態變化。
我們再看看在 Compose 中,是如何實現 EditText 的效果的。

Compose 提供了 TextField 作爲常用的文本輸入框。它也遵循 Meterial Design 設計準則。看看它最簡單的使用方式:

Column {
    Greeting("Android")
    Button(onClick = { /*TODO*/ }) {
        Text("I’m a button")
    }

    var text by remember { mutableStateOf("文本框初始值") }
    TextField(value = text, onValueChange = {
        text = it
    })
}

效果如下:

這裏出現了關於 State 的使用,關於狀態,將在下一篇文章中講解,這裏只需要知道,TextField 的參數 value 是唯一能決定其顯示文本的數據源。我們定義了一個狀態變量 text, 並設置給了 value 參數。如果給 value 傳入一個固定的字符串,則無論在鍵盤上輸入什麼,TextField 的顯示都不會改變。onValueChange 參數這個回到中,可以獲取到當前來自軟鍵盤的最新輸入。我們利用這個信息來更新可變狀態 text, 驅動界面刷新來顯示最新的輸入文本。

三、Compose 與 View 互操作

Compose 生成的 UI 樹節點是 LayoutNode, View 生成的 UI 樹節點是 View 和 ViewGroup, 兩者之間可以共存與一棵樹中,就像 DOM 節點可以依靠 Webview 掛載到 View 樹一樣, Compose 與 View 之間也存在這樣的橋樑,使得兩者可以共同存在。

3.1 Compose 中使用 View

什麼時候會在 Compose 中使用 View 呢?

  • 極少數 View 暫時還沒有 Compose 版本,比如 MapView, WebView
  • 有一塊之前寫好的 UI, (暫時或者永遠)不想動,想直接拿過來用
  • 初學者用 Compose 實現不了想要的效果,先用 View

3.1.1 Compose 中使用 AndroidView

看例子:

@Composable
fun MyTextView(text: String) {
    AndroidView(
        fatory = { context ->
            TextView(context).apply {
                setText(text)
            }
        },
        update = { view -> 
            view.setText(text)
        }
    )
}

這個橋樑是 AndroidView, 它是一個 Composable 函數。

@Composable
fun <T: View> AndroidView(
    fatory: (context) -> T,
    modifier: Modifier = Modifier,
    update: (T) -> Unit = NoOpUpdate
)

fatory 接收一個 Context 參數,用來構建一個 View, update 方法是一個 callback, inflate 之後會執行,讀取的狀態 state 值變化後,也會被執行。

3.1.2 Compose 中使用 xml 佈局

上面使用 AndroidView 適用於少量的 UI, 如果需要複用一個已經存在的 xml 佈局,怎麼辦?

  • 首先開啓 viewBinding
android {
    buildFeatures {
        compose = true
        viewbinding = true
    }
}
  • 添加 Compose viewbinding 依賴
implementation("androidx.compose.ui:ui-viewbinding:1.5.4")

使用過 ViewBinding 的同學應該清楚,build 之後,會根據 xml 文件生成對應的 Binding 類,例如 TestLayoutBinding

@Composable
fun TestComposableLayout() {
    AndroidViewBinding(TestLayoutBinding::inflate) {
        testButton.setOnClickListener {
            //...
        }
    }
}

其實 AndroidViewBinding 內部還是調用了 AndroidView 這個 Composable 函數。

3.2 View 中使用 Compose

使用 ComposeView 作爲橋樑。
普通 xml 文件中加入 ComposeView

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com.apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <TextView id="@+id/tv_test"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="test" />

    <androidx.compose.ui.platform.ComposeView
        android:id="@+id/compose_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

代碼中,先根據 id 查找出來,再 setContent 即可:

findViewById<ComposeView>(R.id.compose_view).setContent {
    Text("I'm Composable")
}

動態添加也可:

addView(ComposeView(this@MainActivity).apply {
    setContent {
        Text("I'm Composable")
    }
})

這裏起到橋樑作用的 ComposeView, 本質上是一個 ViewGroup, 它的 setContent() 方法開啓了 Compose 世界的大門,在這裏可以傳入 Composable 函數。

小結:

  • Compose 中調用 View, 藉助 AndroidView
  • View 中調用 Compose,藉助 ComposeView
    Compose 和 View 的互操作性也保證了項目可以逐步遷移。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章