Android Jetpack Compose快速上手

語言: CN / TW / HK

一、Jetpack Compose簡介

Jetpack Compose是Google推出的一個用於構建原生Android 界面的工具包,旨在幫助開發者更快、更輕鬆地在Android 平台上構建原生客户端應用。同時,作為全新的聲明式的UI框架,Jetpack Compose可以使用聲明式Kotlin API取代Android 傳統的xml佈局。

那什麼是聲明式呢?要搞清楚這個問題,我們需要佈局開發中的另外一個概念:命令式。事實上,傳統的使用xml佈局方式就是命令式。在傳統的命令式開發流程中,我們首先需要使用xml來創建佈局,然後再通過findViewById方法獲取控件,最後再綁定數據。而在聲明式開發中,我們可以直接調用compose的庫組件進行渲染,比如:

@Composable fun ShowText(content: String){ Text(text = content) }

事實上,除了Jetpack Compose,Flutter、React Native和Swift-UI 等框架都是聲明式的,可以説,前端的大部分的頁面渲染都可以使用聲明式來完成。

二、快速上手

2.1 環境搭建

工欲善其事,必先利其器。目前,Android Studio對Jetpack Compose 已經有了很好的支持,我們只需要下載最新版的Android Studio即可。

image.png

安裝完成之後,我們可以下載託管在github上的 Jetpack Compose 示例應用來體驗Jetpack Compose的魅力。

2.2 創建Jetpack Compose應用

為了幫助開發者快速地上手Jetpack Compose,Android Studio提供了支持Jetpack Compose 的新項目模板。我們只需要打開 Android Studio,然後在菜單欄中依次選擇 【File】->【New】->【New Project】 ->【Empty Compose Activity】。

image.png

然後,點擊【Next】按鈕,填寫 Name、Package name 和 Save location等參數即可完成Jetpack Compose項目的創建。工程創建完成之後,Jetpack Compose項目會默認添加如下一些依賴。

dependencies { implementation("androidx.compose.ui:ui:1.2.1") // Tooling support (Previews, etc.) implementation("androidx.compose.ui:ui-tooling:1.2.1") // Foundation (Border, Background, Box, Image, Scroll, shapes, animations, etc.) implementation("androidx.compose.foundation:foundation:1.2.1") // Material Design implementation("androidx.compose.material:material:1.2.1") // Material design icons implementation("androidx.compose.material:material-icons-core:1.2.1") implementation("androidx.compose.material:material-icons-extended:1.2.1") // Integration with observables implementation("androidx.compose.runtime:runtime-livedata:1.2.1") implementation("androidx.compose.runtime:runtime-rxjava2:1.2.1") // UI Tests androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.2.1") }

2.3 原有項目添加Jetpack Compose

當然,使用Android Studio提供的Jetpack Compose 模板來創建項目是最簡單的,如果對於老的Android項目,我們需要怎麼處理呢?

首先,打開項目的build.gralde文件,確保Kotlin的版本是1.4.30以上的版本。

plugins { ... id 'org.jetbrains.kotlin.android' version '1.5.30' apply false }

然後,打開app/build.gralde文件,添加或修改如下一些配置:

``` android {

// 1、確保最低sdk版本為21或者更高 
defaultConfig {
    ...
    minSdkVersion 21
}

buildFeatures {
    // 2、開啟 jetpack compose 支持        
    compose true
}
...


// 3、設置java 和 kotlin 編譯器版本為java8或者更高的版本
compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}


// 4、添加kotlin編譯器擴展版本
composeOptions {
    kotlinCompilerExtensionVersion '1.3.0'
}

} ```

然後,添加Jetpack Compose 開發需要的一些依賴庫。

dependencies { // Integration with activities implementation 'androidx.activity:activity-compose:1.5.1' // Compose Material Design implementation 'androidx.compose.material:material:1.2.1' // Animations implementation 'androidx.compose.animation:animation:1.2.1' // Tooling support (Previews, etc.) implementation 'androidx.compose.ui:ui-tooling:1.2.1' // Integration with ViewModels implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1' // UI Tests androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.2.1' }

2.4 遷移到 Compose

對於傳統的xml佈局,我們如何將其遷移到 Jetpack Compose。加入,我們在 XML 佈局中有如下一段代碼。

``` <...>

```

為了將其遷移到 Compose,我們可以將 TextView 替換為保留了相同佈局參數和 id 的 ComposeView,如下所示。

``` <...>

```

然後,在使用了該XML佈局的Activity或Fragment中獲取ComposeView,然後調用setContent方法並向其中添加Compose內容。

``` class MyActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // ... val greeting = findViewById(R.id.greeting) greeting.setContent { MdcTheme { Greeting() } } } }

@Composable private fun Greeting() { Text( text = stringResource(R.string.greeting), style = MaterialTheme.typography.h5, modifier = Modifier .fillMaxWidth() .padding(horizontal = dimensionResource(R.dimen.margin_small)) .wrapContentWidth(Alignment.CenterHorizontally) ) } ```

三、Compose工具

事實上,為了支持Jetpack Compose 的快速開發,Android Studio引入了許多專用於Jetpack Compose 的新功能。它支持使用代碼優先方法,同時提高了開發者的工作效率,因為開發者不必在設計界面或代碼編輯器之間二選一。

而基於View的界面與Jetpack Compose之間的一個基本區別在於,Compose不依賴View來呈現其可組合項。同時,Android Studio 為Jetpack Compose提供了擴展功能,使其不必像 Android View 一樣打開模擬器或連接到設備即可預覽,從而加快了開發者實現其界面設計的迭代過程。

同時,為了實現如需為 Jetpack Compose 的預覽功能,需要我們在應用 build.gradle 文件中添加以下依賴項:

debugImplementation "androidx.compose.ui:ui-tooling:1.2.1" implementation "androidx.compose.ui:ui-tooling-preview:1.2.1"

3.1 預覽模式

3.1.1 可組合項預覽

首先,我們打開新建的Jetpack Compose項目,MainActivity.kt的代碼如下:

class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { ComposeDemosTheme { Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background ) { Greeting("Android") } } } } } @Composable fun Greeting(name: String) { Text(text = "Hello $name!") } @Preview(showBackground = true) @Composable fun DefaultPreview() { ComposeDemosTheme { Greeting("Android") } }

在上面的代碼中,setContent()不再是以前的傳遞一個View或者是layout,而是一個組合函數,也就是一個Compose組件。而帶有@Composable的Kotlin函數就是就是一個Compose組件,一般為了跟普通函數區分,因此上面的Greeting和DefaultPreview 都是一個Compose組件。  

最後,點擊拆分(設計/代碼)視圖,打開顯示預覽的右側面板就可以看到效果。

image.png @Preview接受參數來支持Android Studio呈現的方式,我們可以在代碼中手動添加這些參數,也可以點擊 @Preview 旁邊的邊線圖標來顯示配置選擇器,以便選擇和更改這些配置參數。

除此之外,Android Studio 提供了一些功能來擴展可組合項預覽。我們可以通過讀取 LocalInspectionMode CompositionLocal來確認可組合項是否正在預覽中呈現。如果可組合項在預覽中呈現,LocalInspectionMode.current 的結果為 true。

if (LocalInspectionMode.current) { //呈現在預覽界面 Text("Hello preview user!") } else { //在App中呈現 Text("Hello $name!") }

3.1.2 互動模式

互動模式,可以實現在設備上互動的方式和預覽互動,互動模式被隔離在沙盒環境中,在該模式下,我們可以在預覽中點擊元素並輸入用户輸入;預覽甚至播放動畫。預覽互動模式可以直接在Android Studio中運行,由於並未運行模擬器,所以存在一些使用限制:

  • 無法訪問網絡
  • 無法訪問文件
  • 有些 Context API 不一定完全可用

使用互動模式時,只需要編寫好代碼,然後點擊【Start interactive mode】開啟,如下圖。

image.png

tooling-interactive-preview-demo.gif  

3.1.3 部署預覽

我們可以使用 @Preview 來部署應用到模擬器或實體設備上。點擊 @Preview 註解旁邊或預覽頂部的 Deploy to Device 圖標 ,Android Studio 會將該 @Preview 部署到連接的設備或模擬器上。

image.png

tooling-deploy-preview-demo.gif

3.1.4 Multipreview

使用 Multipreview註解時,我們可以定義一個註解類,該類本身可配置多個採用不同配置的 Preview註解。將此註解添加到一個可組合函數後,系統會自動同時呈現所有不同的預覽。例如,我們可以定義一個同時支持預覽多個設備、字體大小或主題的註解類。

比如,我們首先定義一個可以改變字體的FontScalePreviews類。

@Preview( name = "small font", group = "font scales", fontScale = 0.5f ) @Preview( name = "large font", group = "font scales", fontScale = 1.5f ) annotation class FontScalePreviews

然後,在字體中使用這個預覽可組合項使用此自定義註解。

@FontScalePreviews @Composable fun HelloWorldPreview() { Text("Hello World") }

最終的效果如下圖。

image.png

當然,我們也可以將多個 MultiPreview 註解和普通 preview 註解結合進行使用,從而創建一個更完整的預覽集。結合使用 MultiPreview 註解並不意味着所有不同的組合都會得以呈現。實際上,每個 MultiPreview 註解會獨立運行,並且僅會呈現自己的變體。

@Preview( name = "dark theme", group = "themes", uiMode = UI_MODE_NIGHT_YES ) @FontScalePreviews @DevicePreviews annotation class CombinedPreviews @CombinedPreviews @Composable fun HelloWorldPreview() { MyTheme { Surface { Text("Hello world") } } }

image.png

通過右鍵點擊呈現的每個預覽,即可將其作為圖像來複制。

image.png

默認情況下,我們的可組合項是以透明背景來顯示的。如果需要添加背景,那麼需要用到showBackground 和 backgroundColor 兩個參數,比如:

@Preview(showBackground = true, backgroundColor = 0xFF00FF00) @Composable fun WithGreenBackground() { Text("Hello World") }

如需手動設置尺寸,可以添加 heightDp 和 widthDp 參數。

@Preview(widthDp = 50, heightDp = 50) @Composable fun SquareComposablePreview() { Box(Modifier.background(Color.Yellow)) { Text("Hello World") } }

有時候,我們需要對狀態欄和操作欄進行一些修改,那麼可以使用showSystemUi參數。

@Preview(showSystemUi = true) @Composable fun DecoratedComposablePreview() { Text("Hello World") }

當然,我們也可以使用@PreviewParameter註解來添加參數,用來將示例數據傳遞給某個可組合項預覽函數。

@Preview @Composable fun UserProfilePreview( @PreviewParameter(UserPreviewParameterProvider::class) user: User ) { UserProfile(user) }

然後,創建一個可實現 PreviewParameterProvider 並以序列形式返回示例數據的類。

class UserPreviewParameterProvider : PreviewParameterProvider<User> { override val values = sequenceOf( User("Elise"), User("Frank"), User("Julia") ) }

接着,序列中的每個數據元素都會呈現一個預覽。

image.png

當需要為多個預覽使用相同的提供程序類時。如有必要,可通過設置 limit 參數來限制呈現的預覽數量,比如。

@Preview @Composable fun UserProfilePreview( @PreviewParameter(UserPreviewParameterProvider::class, limit = 2) user: User ) { UserProfile(user) }

3.2 編輯器

為了提高使用 Jetpack Compose 時的工作效率,Android Studio在編輯器區域提供了一些功能,如實時模板、邊線圖標、顏色選擇器等。

3.2.1 實時模板

Android Studio 提供了Compose 相關的實時模板,開發者可以通過輸入相應的模板縮寫來輸入代碼段,以實現快速插入。

  • comp:用於設置 @Composable 函數
  • prev:用於創建 @Preview 可組合函數
  • paddp:用於以 dp 為單位添加 padding 修飾符
  • weight:用於添加 weight 修飾符
  • W、WR、WC:用於通過 Box、Row 或 Column 容器設置當前可組合項的呈現效果

3.2.2 邊線圖標

邊線圖標是邊欄中可見的上下文操作,位於行號旁邊。Android Studio 引入了多個 Jetpack Compose 專用邊線圖標,以便開發者更輕鬆地使用。比如,可以直接通過邊線圖標將 @Preview 部署到模擬器或實體設備上。

tooling-preview-deploy-gutter-icon.gif  

而對於顏色選擇器來説,我們可以點擊顏色,然後更改選中的眼神,如下所示。

image.png

為了方便對圖像進行選擇,Android Studio也支持圖形選擇器,可以通過圖像資源選擇器更改選擇圖片,如下所示:

tooling-resource-picker.gif

3.3 迭代開發

作為移動開發者,移動應用界面開發並不是一次性開發完所有的內容的。Android Studio 通過提供不需要完整 build 即可檢查、修改值和驗證最終結果的工具,支持使用 Jetpack Compose 進行逐步開發。

3.3.1 實時修改字面量

Android Studio 可以實時更新在預覽、模擬器和實體設備中的可組合項中使用的一些常量字面量,如Int、String、Color、Dp、Boolean等。

tooling-live-literals-demo.gif

通過“Live Edit of Literals”界面指示器啟用字面量修飾功能,無需進行編譯即可查看觸發實時更新的常量字面量。

live-editing-of-literals.gif

3.3.2 實時編輯

我們可以打開 Android Studio Electric Eel 的 Canary 版本,然後使用實時編輯功能加快 Compose 開發過程。相較於實時編輯字面量功能,“實時編輯”是具備更強大功能的版本。開發者可以自動將代碼更改部署到模擬器或設備上,從而實時查看可組合項更新後的效果。

live-edit-only-device.gif

3.3.3 Apply Changes

Apply Changes 支持更新代碼和資源,並且不需要在模擬器或實體設備上重新運行代碼。每當開發者添加、修改或刪除可組合項時,只需要點擊一下此按鈕,即可更新應用,而不必重新部署。

image.png

3.4 佈局檢查器

通過佈局檢查器,開發者可以在模擬器或實體設備上檢查正在運行的應用中的 Compose 佈局。

image.png

如需跟蹤重組,需要在視圖選項中啟用【Show Recomposition Counts】。

image.png 啟用後,佈局檢查器會在左側顯示重組次數,在右側顯示跳過重組的次數。

image.png

3.5 動畫

Android Studio 允許開發者從動畫預覽中檢查動畫,我們可以在組合項預覽中描述了動畫效果,檢查每個動畫值在給定時間點的確切值,並且可以暫停、循環播放、快進或放慢動畫,以便在動畫過渡過程中調試動畫。

animation-preview.gif

當然,我們也可以使用動畫預覽以圖形方式呈現動畫曲線,這對於確保正確編排動畫值非常有用。

image.png

四、Kotlin與Jetpack Compose配合使用

Jetpack Compose使用Kotlin構建而成,在某些情況下,Kotlin 提供了一些特殊的慣用語,可以幫助開發者編寫良好的 Compose 代碼。如果使用另一種編程語言,那麼很可能會錯失 Compose 的一些優勢。

4.1 默認參數

編寫 Kotlin 函數時,我們可以指定函數參數的默認值;如果調用方未明確傳遞相應的值,系統就會使用這些默認值。在 Kotlin 中,編寫一個函數並指定參數的默認值的方式和Java是類似的。

fun drawSquare( sideLength: Int, thickness: Int = 2, edgeColor: Color = Color.Black ) { ... }

當然,上面的代碼也可以簡寫成下面的方式,Kotlin的編譯器也是可以識別的。

drawSquare(sideLength = 30, thickness = 5, edgeColor = Color.Red)

4.2 高階函數和 lambda 表達式

所謂高階函數,指的是接收其他函數作為參數的函數,Kotlin也是支持高階函數的。例如,Button 可組合函數提供了一個 onClick lambda 參數。

Button( // ... onClick = myClickFunction )

高階函數與 lambda 表達式是自然配對的。如果您只需要使用該函數一次,則不必在其他位置進行定義以將其傳遞給高階函數,而只需使用 lambda 表達式在該位置定義該函數即可。

Button( // ... onClick = { // do something // do something else } ) { /* ... */ }

4.3 委託屬性

Kotlin支持委託屬性,這些屬性可以像字段一樣被調用,但它們的值是通過對錶達式動態確定的。

``` class DelegatingClass { var name: String by nameGetterFunction() }

val myDC = DelegatingClass() println("The name property is: " + myDC.name) ```

當執行 println() 函數時,系統會調用 nameGetterFunction() 以返回字符串的值。同時,使用狀態支持的屬性時,這些委託屬性特別有用。

var showDialog by remember { mutableStateOf(false) } // Updating the var automatically triggers a state change showDialog = true

4.4 協程

在 Kotlin 中,協程在語言級別提供了異步編程的支持。協程可以掛起執行,而不會阻塞線程。自適應界面本質上是異步的,而 Jetpack Compose 會在 API 級別引入協程而非使用回調來解決此問題。

Jetpack Compose 提供了可在界面層中安全使用協程的 API,rememberCoroutineScope 函數會返回一個 CoroutineScope,可以用它在事件處理腳本中創建協程並調用 Compose Suspend API。

val composableScope = rememberCoroutineScope() Button( // ... onClick = { composableScope.launch { scrollState.animateScrollTo(0) // This is a suspend function viewModel.loadData() } } ) { /* ... */ }

默認情況下,協程會依序執行代碼塊。正在運行且調用掛起函數的協程會掛起其執行,直到掛起函數返回,即使掛起函數將執行移至其他 CoroutineDispatcher,也是如此。若要同時執行代碼,則需要創建新的協程。在上述示例中,如需在滾動到屏幕頂部的同時從 viewModel 加載數據,則需要兩個協程。

val composableScope = rememberCoroutineScope() Button( onClick = { composableScope.launch { scrollState.animateScrollTo(0) } composableScope.launch { viewModel.loadData() } } ) { /* ... */ }

協程可幫助您更輕鬆地合併異步 API。在以下示例中,我們會將 pointerInput 修飾符與動畫 API 結合,以便在用户點按屏幕時在元素的位置呈現動畫效果。

@Composable fun MoveBoxWhereTapped() { // Creates an `Animatable` to animate Offset and `remember` it. val animatedOffset = remember { Animatable(Offset(0f, 0f), Offset.VectorConverter) } Box( // The pointerInput modifier takes a suspend block of code Modifier.fillMaxSize().pointerInput(Unit) { // Create a new CoroutineScope to be able to create new // coroutines inside a suspend function coroutineScope { while (true) { // Wait for the user to tap on the screen val offset = awaitPointerEventScope { awaitFirstDown().position } // Launch a new coroutine to asynchronously animate to where // the user tapped on the screen launch { // Animate to the pressed position animatedOffset.animateTo(offset) } } } } ) { Text("Tap anywhere", Modifier.align(Alignment.Center)) Box( Modifier .offset { // Use the animated offset as the offset of this Box IntOffset( animatedOffset.value.x.roundToInt(), animatedOffset.value.y.roundToInt() ) } .size(40.dp) .background(Color(0xff3c1361), CircleShape) ) } }