Compose簡介
- Jetpack Compose:利用聲明式編程構建Android原生界面(UI)的 工具包
優勢
- 更少的代碼、代碼量銳減
- 強大的工具/組件支持
- 直觀的 Kotlin API
- 簡單易用
Compose 編程思想
聲明性編程範式:聲明性的函數構建一個簡單的界面組件,無需修改任何 XML 佈局,也不需要使用佈局編輯器,只需要調用 Jetpack Compose 函數來聲明想要的元素,Compose 編譯器即會完成後面的所有工作
-
舉個栗子:簡單的可組合函數
class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { Text("Hello world!") } } }
動態 :組合函數是用 Kotlin 而不是 XML 編寫,見上
$name
的傳入-
需要注意的事項:
-
可組合函數可以按任何順序執行
//可以按任何順序進行,不能讓 StartScreen() 設置某個全局變量(附帶效應)並讓 MiddleScreen() 利用這項更改。相反,其中每個函數都需要保持獨立。 @Composable fun ButtonRow() { MyFancyNavigation { StartScreen() MiddleScreen() EndScreen() } }
-
可組合函數可以並行執行
Compose 可以通過並行運行可組合函數來優化重組,這樣一來,Compose 就可以利用多個核心,並以較低的優先級運行可組合函數(不在屏幕上)
這種優化意味着,可組合函數可能會在後臺線程池中執行,如果某個可組合函數對 ViewModel 調用一個函數,則 Compose 可能會同時從多個線程調用該函數
調用某個可組合函數時,調用可能發生在與調用方不同的線程上,這意味着,應避免使用修改可組合 lambda 中的變量的代碼,既因爲此類代碼並非線程安全代碼,又因爲它是可組合 lambda 不允許的附帶效應
//此代碼沒有附帶效應 @Composable fun ListComposable(myList: List<String>) { Row(horizontalArrangement = Arrangement.SpaceBetween) { Column { for (item in myList) { Text("Item: $item") } } Text("Count: ${myList.size}") } }
//如果函數寫入局部變量,則這並非線程安全或正確的代碼: @Composable @Deprecated("Example with bug 有問題的代碼") fun ListWithBug(myList: List<String>) { var items = 0 Row(horizontalArrangement = Arrangement.SpaceBetween) { Column { for (item in myList) { Text("Item: $item") items++ // Avoid! Side-effect of the column recomposing. } } Text("Count: $items") } } //每次重組時,都會修改 items。這可以是動畫的每一幀,或是在列表更新時。但不管怎樣,界面都會顯示錯誤的項數。因此,Compose 不支持這樣的寫入操作;通過禁止此類寫入操作,我們允許框架更改線程以執行可組合 lambda。
重組會跳過儘可能多的 可組合函數和 lambda
重組是樂觀的操作,可能會被取消
可組合函數可能會像動畫的每一幀一樣非常頻繁地運行
-
環境準備
已瞭解的同學,可直接跳過
需要升級到Arctic Fox 2020-3-1 版本以上,此版本以下Android studio 無此支持-【下載最新Android studio】
- 我們注意到此項目只支持Kotlin 最低sdk 版本爲21,Android 5.0
- Gradle Compose相關依賴
implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.compose.material:material:$compose_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
implementation 'androidx.activity:activity-compose:1.3.0-alpha06'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
kotlinOptions {
jvmTarget = '1.8'
useIR = true//在 Gradle 構建腳本中指定額外編譯器選項即可啓用新的 JVM IR 後端
}
composeOptions {
kotlinCompilerExtensionVersion compose_version
kotlinCompilerVersion '1.5.10'
}
buildFeatures {
compose true
}
packagingOptions {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
- 由於新版本邀請java 11,安裝 java 8 環境的需要以下修復
Android Gradle plugin requires Java 11 to run. You are currently using Java 1.8.
You can try some of the following options:
- changing the IDE settings.
- changing the JAVA_HOME environment variable.
- changing `org.gradle.java.home` in `gradle.properties`.
org.gradle.java.home=/Library/Java/JavaVirtualMachines/jdk-11.0.11.jdk/Contents/Home
- @Preview起作用,環境正常
佈局
-
Android 傳統從xml-狀態的變更關係,程序員需要大量的代碼維護Ui 界面,以達到界面狀態的正確性,費時費力,即便是藉助MVVM架構,一樣需要維護狀態,因爲佈局只有一套
聲明式與傳統XML 實現區別,Compose 聲明式佈局,是直接重建了UI,所以不會有狀態問題
-
Text:Compose 提供了基礎的
BasicText
和BasicTextField
,它們是用於顯示文字以及處理用戶輸入的主要函數。Compose 還提供了更高級的Text
和TextField
Text("Hello World")
-
重組Text->Button
@Composable fun ClickCounter(clicks: Int, onClick: () -> Unit) { Button(onClick = onClick) { Text("I've been clicked $clicks times") } }
-
Modifier可以修改控件的位置、高度、邊距、對齊方式等等
//`padding` 設置各個UI的padding。padding的重載的方法一共有四個。 Modifier.padding(10.dp) // 給上下左右設置成同一個值 Modifier.padding(10.dp, 11.dp, 12.dp, 13.dp) // 分別爲上下左右設值 Modifier.padding(10.dp, 11.dp) // 分別爲上下和左右設值 Modifier.padding(InnerPadding(10.dp, 11.dp, 12.dp, 13.dp))// 分別爲上下左右設值 //這裏設置的值必須爲`Dp`,`Compose`爲我們在Int中擴展了一個方法`dp`,幫我們轉換成`Dp`。 //`plus` 可以把其他的Modifier加入到當前的Modifier中。 Modifier.plus(otherModifier) // 把otherModifier的信息加入到現有的modifier中 //`fillMaxHeight`,`fillMaxWidth`,`fillMaxSize` 類似於`match_parent`,填充整個父layout。 Modifier.fillMaxHeight() // 填充整個高度 //`width`,`heigh`,`size` 設置Content的寬度和高度。 Modifier.width(2.dp) // 設置寬度 Modifier.height(3.dp) // 設置高度 Modifier.size(4.dp, 5.dp) // 設置高度和寬度 //`widthIn`, `heightIn`, `sizeIn` 設置Content的寬度和高度的最大值和最小值。 Modifier.widthIn(2.dp) // 設置最大寬度 Modifier.heightIn(3.dp) // 設置最大高度 Modifier.sizeIn(4.dp, 5.dp, 6.dp, 7.dp) // 設置最大最小的寬度和高度 //`gravity` 在`Column`中元素的位置。 Modifier.gravity(Alignment.CenterHorizontally) // 橫向居中 Modifier.gravity(Alignment.Start) // 橫向居左 Modifier.gravity(Alignment.End) // 橫向居右 //`rtl`, `ltr` 開始佈局UI的方向。 Modifier.rtl // 從右到左 //更多Modifier學習:https://developer.android.com/jetpack/compose/modifiers-list
Column
線性佈局≈Android LinearLayout-VERTICAL
Row
水平佈局≈Android LinearLayout-HORIZONTAL
Box
幀佈局≈Android FrameLayout
,可將一個元素放在另一個元素上,如需在Row
中設置子項的位置,請設置horizontalArrangement
和verticalAlignment
參數。對於Column
,請設置verticalArrangement
和horizontalAlignment
參數-
相對佈局,需要引入
ConstraintLayout
- 引入
implementation "androidx.constraintlayout:constraintlayout-compose:1.0.0-beta02"
-
constraintlayout-compose
用法流程
- 完整用法示例
@Composable
fun testConstraintLayout() {
ConstraintLayout() {
//通過createRefs創建三個引用
val (imageRef, nameRef) = createRefs()
Image(painter = painterResource(id = R.mipmap.test),
contentDescription = "圖",
modifier = Modifier
.constrainAs(imageRef) {//通過constrainAs將Image與imageRef綁定,並增加約束
top.linkTo(parent.top)
start.linkTo(parent.start)
bottom.linkTo(parent.bottom)
}
.size(100.dp)
.clip(shape = RoundedCornerShape(5)),
contentScale = ContentScale.Crop)
Text(
text = "名稱",
modifier = Modifier
.constrainAs(nameRef) {
top.linkTo(imageRef.top, 2.dp)
start.linkTo(imageRef.end, 12.dp)
end.linkTo(parent.end)
width = Dimension.fillToConstraints
}
.fillMaxWidth(),
fontSize = 18.sp,
maxLines = 1,
textAlign = TextAlign.Left,
overflow = TextOverflow.Ellipsis,
)
}
}
列表
- 可以滾動的佈局
//我們可以使用 verticalScroll() 修飾符使 Column 可滾動
Column (
modifier = Modifier.verticalScroll(rememberScrollState())){
messages.forEach { message ->
MessageRow(message)
}
}
但以上佈局並無法實現重用,可能導致性能問題,下面介紹我們重點佈局,列表
-
LazyColumn/LazyRow==RecylerView/listView
列表佈局,解決了滾動時的性能問題,LazyColumn
和LazyRow
之間的區別就在於它們的列表項佈局和滾動方向不同-
內邊距
LazyColumn( contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), ) { // ... }
-
item間距
LazyColumn( verticalArrangement = Arrangement.spacedBy(4.dp), ) { // ... }
-
浮動列表的浮動標題,使用
LazyColumn
實現粘性標題,可以使用實驗性stickyHeader()
函數@OptIn(ExperimentalFoundationApi::class) @Composable fun ListWithHeader(items: List<Item>) { LazyColumn { stickyHeader { Header() } items(items) { item -> ItemRow(item) } } }
-
-
網格佈局
LazyVerticalGrid
@OptIn(ExperimentalFoundationApi::class) @Composable fun PhotoGrid(photos: List<Photo>) { LazyVerticalGrid( cells = GridCells.Adaptive(minSize = 128.dp) ) { items(photos) { photo -> PhotoItem(photo) } } }
自定義佈局
通過重組基礎佈局實現
-
Canvas
Canvas(modifier = Modifier.fillMaxSize()) { val canvasWidth = size.width val canvasHeight = size.height drawCircle( color = Color.Blue, center = Offset(x = canvasWidth / 2, y = canvasHeight / 2), radius = size.minDimension / 4 ) } //drawCircle 畫圓 //drawRectangle 畫矩形 //drawLine //畫線
動畫
- 動畫Api 選擇
其他庫支持
-
implementation("androidx.navigation:navigation-compose:2.4.0-alpha05")
總結
- Compose總體來說,對於Android-Native佈局實現上更加簡單高效,值得大家一學
- Compose 寫法與Flutter-Dart 有高度類似的情況,後面我們可以做一篇與Flutter-Dart 語音寫佈局的一些對比