Jetpack Compose入門篇-簡約而不簡單

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 提供了基礎的 BasicTextBasicTextField,它們是用於顯示文字以及處理用戶輸入的主要函數。Compose 還提供了更高級的 TextTextField

    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 中設置子項的位置,請設置 horizontalArrangementverticalAlignment 參數。對於 Column,請設置 verticalArrangementhorizontalAlignment 參數

  • 相對佈局,需要引入 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 列表佈局,解決了滾動時的性能問題,LazyColumnLazyRow 之間的區別就在於它們的列表項佈局和滾動方向不同

    • 內邊距

      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 語音寫佈局的一些對比

引用

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章