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) ) } }