androidx.compose.runtime @Stable 注解,以及簡(jiǎn)單介紹如何使用 Jetpack Compose

官方文檔對(duì) @Stable 的注釋:
穩(wěn)定用于向組成編譯器傳達(dá)有關(guān)某種類型或函數(shù)的行為的某些保證。
當(dāng)應(yīng)用于類或接口時(shí),[Stable]表示以下條件必須為真:
1)對(duì)于兩個(gè)相同的實(shí)例,[equals]的結(jié)果將始終返回相同的結(jié)果。
2)當(dāng)類型的公共財(cái)產(chǎn)發(fā)生變化時(shí),將通知組成。
3)所有公共財(cái)產(chǎn)類型都是穩(wěn)定的。
當(dāng)應(yīng)用于函數(shù)或?qū)傩詴r(shí),@Stable 注解表示如果傳入相同的參數(shù),該函數(shù)將返回相同的結(jié)果。僅當(dāng)參數(shù)和結(jié)果本身為[Stable],[Immutable], 或原始的。
該注解所隱含的不變量由組合編譯器用于優(yōu)化,如果不滿足上述假設(shè),則具有未定義的行為。 所以除非他們確定滿足這些條件,否則不應(yīng)該使用此注解。

舉一個(gè)代碼例子如下:

/**
 * Main holder of our inset values.
 */
@Stable
class DisplayInsets {
  /**
   * Inset values which match [WindowInsetsCompat.Type.systemBars]
   */
  val systemBars = Insets()

  /**
   * Inset values which match [WindowInsetsCompat.Type.systemGestures]
   */
  val systemGestures = Insets()

  /**
   * Inset values which match [WindowInsetsCompat.Type.navigationBars]
   */
  val navigationBars = Insets()

  /**
   * Inset values which match [WindowInsetsCompat.Type.statusBars]
   */
  val statusBars = Insets()

  /**
   * Inset values which match [WindowInsetsCompat.Type.ime]
   */
  val ime = Insets()
}

@Stable
class Insets {
  /**
   * The left dimension of these insets in pixels.
   */
  var left by mutableStateOf(0)
    internal set

  /**
   * The top dimension of these insets in pixels.
   */
  var top by mutableStateOf(0)
    internal set

  /**
   * The right dimension of these insets in pixels.
   */
  var right by mutableStateOf(0)
    internal set

  /**
   * The bottom dimension of these insets in pixels.
   */
  var bottom by mutableStateOf(0)
    internal set

  /**
   * Whether the insets are currently visible.
   */
  var isVisible by mutableStateOf(true)
    internal set
}

val InsetsAmbient = staticAmbientOf<DisplayInsets>()

/**
 * Applies any [WindowInsetsCompat] values to [InsetsAmbient], which are then available
 * within [content].
 *
 * @param consumeWindowInsets Whether to consume any [WindowInsetsCompat]s which are dispatched to
 * the host view. Defaults to `true`.
 */
@Composable
fun ProvideDisplayInsets(
  consumeWindowInsets: Boolean = true,
  content: @Composable () -> Unit
) {
  val view = ViewAmbient.current

  val displayInsets = remember { DisplayInsets() }

  onCommit(view) {
    ViewCompat.setOnApplyWindowInsetsListener(view) { _, windowInsets ->
      displayInsets.systemBars.updateFrom(windowInsets, WindowInsetsCompat.Type.systemBars())
      displayInsets.systemGestures.updateFrom(
        windowInsets,
        WindowInsetsCompat.Type.systemGestures()
      )
      displayInsets.statusBars.updateFrom(windowInsets, WindowInsetsCompat.Type.statusBars())
      displayInsets.navigationBars.updateFrom(
        windowInsets,
        WindowInsetsCompat.Type.navigationBars()
      )
      displayInsets.ime.updateFrom(windowInsets, WindowInsetsCompat.Type.ime())

      if (consumeWindowInsets) WindowInsetsCompat.CONSUMED else windowInsets
    }

    // Add an OnAttachStateChangeListener to request an inset pass each time we're attached
    // to the window
    val attachListener = object : View.OnAttachStateChangeListener {
      override fun onViewAttachedToWindow(v: View) = v.requestApplyInsets()
      override fun onViewDetachedFromWindow(v: View) = Unit
    }
    view.addOnAttachStateChangeListener(attachListener)

    if (view.isAttachedToWindow) {
      // If the view is already attached, we can request an inset pass now
      view.requestApplyInsets()
    }

    onDispose {
      view.removeOnAttachStateChangeListener(attachListener)
    }
  }

  Providers(InsetsAmbient provides displayInsets) {
    content()
  }
}

/**
 * Updates our mutable state backed [Insets] from an Android system insets.
 */
private fun Insets.updateFrom(windowInsets: WindowInsetsCompat, type: Int) {
  val insets = windowInsets.getInsets(type)
  left = insets.left
  top = insets.top
  right = insets.right
  bottom = insets.bottom

  isVisible = windowInsets.isVisible(type)
}

/**
 * Apply additional space which matches the height of the status bars height along the top edge
 * of the content.
 */
fun Modifier.statusBarsPadding() = composed {
  insetsPadding(insets = InsetsAmbient.current.statusBars, top = true)
}

/**
 * Apply additional space which matches the height of the navigation bars height
 * along the [bottom] edge of the content, and additional space which matches the width of
 * the navigation bars on the respective [left] and [right] edges.
 *
 * @param bottom Whether to apply padding to the bottom edge, which matches the navigation bars
 * height (if present) at the bottom edge of the screen. Defaults to `true`.
 * @param left Whether to apply padding to the left edge, which matches the navigation bars width
 * (if present) on the left edge of the screen. Defaults to `true`.
 * @param right Whether to apply padding to the right edge, which matches the navigation bars width
 * (if present) on the right edge of the screen. Defaults to `true`.
 */
fun Modifier.navigationBarsPadding(
  bottom: Boolean = true,
  left: Boolean = true,
  right: Boolean = true
) = composed {
  insetsPadding(
    insets = InsetsAmbient.current.navigationBars,
    left = left,
    right = right,
    bottom = bottom
  )
}

/**
 * Declare the height of the content to match the height of the navigation bars, plus some
 * additional height passed in via [additional]
 *
 * As an example, this could be used with `Spacer` to push content above the navigation bar
 * and bottom app bars:
 *
 * ```
 * Column {
 *     // Content to be drawn above navigation bars and bottom app bar (y-axis)
 *
 *     Spacer(Modifier.statusBarHeightPlus(48.dp))
 * }
 * ```
 *
 * Internally this matches the behavior of the [Modifier.height] modifier.
 *
 * @param additional Any additional height to add to the status bars size.
 */
fun Modifier.navigationBarsHeightPlus(additional: Dp) = composed {
  InsetsSizeModifier(
    insets = InsetsAmbient.current.navigationBars,
    heightSide = VerticalSide.Bottom,
    additionalHeight = additional
  )
}

enum class HorizontalSide {
  Left,
  Right
}

enum class VerticalSide {
  Top,
  Bottom
}

/**
 * Allows conditional setting of [insets] on each dimension.
 */
private fun Modifier.insetsPadding(
  insets: Insets,
  left: Boolean = false,
  top: Boolean = false,
  right: Boolean = false,
  bottom: Boolean = false
) = this then InsetsPaddingModifier(insets, left, top, right, bottom)

private data class InsetsPaddingModifier(
  private val insets: Insets,
  private val applyLeft: Boolean = false,
  private val applyTop: Boolean = false,
  private val applyRight: Boolean = false,
  private val applyBottom: Boolean = false
) : LayoutModifier {
  override fun MeasureScope.measure(
    measurable: Measurable,
    constraints: Constraints
  ): MeasureScope.MeasureResult {
    val left = if (applyLeft) insets.left else 0
    val top = if (applyTop) insets.top else 0
    val right = if (applyRight) insets.right else 0
    val bottom = if (applyBottom) insets.bottom else 0
    val horizontal = left + right
    val vertical = top + bottom

    val placeable = measurable.measure(constraints.offset(-horizontal, -vertical))

    val width = (placeable.width + horizontal)
      .coerceIn(constraints.minWidth, constraints.maxWidth)
    val height = (placeable.height + vertical)
      .coerceIn(constraints.minHeight, constraints.maxHeight)
    return layout(width, height) {
      placeable.place(left, top)
    }
  }
}

private data class InsetsSizeModifier(
  private val insets: Insets,
  private val widthSide: HorizontalSide? = null,
  private val additionalWidth: Dp = 0.dp,
  private val heightSide: VerticalSide? = null,
  private val additionalHeight: Dp = 0.dp
) : LayoutModifier {
  private val Density.targetConstraints: Constraints
    get() {
      val additionalWidthPx = additionalWidth.toIntPx()
      val additionalHeightPx = additionalHeight.toIntPx()
      return Constraints(
        minWidth = additionalWidthPx + when (widthSide) {
          HorizontalSide.Left -> insets.left
          HorizontalSide.Right -> insets.right
          null -> 0
        },
        minHeight = additionalHeightPx + when (heightSide) {
          VerticalSide.Top -> insets.top
          VerticalSide.Bottom -> insets.bottom
          null -> 0
        },
        maxWidth = when (widthSide) {
          HorizontalSide.Left -> insets.left + additionalWidthPx
          HorizontalSide.Right -> insets.right + additionalWidthPx
          null -> Constraints.Infinity
        },
        maxHeight = when (heightSide) {
          VerticalSide.Top -> insets.top + additionalHeightPx
          VerticalSide.Bottom -> insets.bottom + additionalHeightPx
          null -> Constraints.Infinity
        }
      )
    }

  override fun MeasureScope.measure(
    measurable: Measurable,
    constraints: Constraints
  ): MeasureScope.MeasureResult {
    val wrappedConstraints = targetConstraints.let { targetConstraints ->
      val resolvedMinWidth = if (widthSide != null) {
        targetConstraints.minWidth
      } else {
        constraints.minWidth.coerceAtMost(targetConstraints.maxWidth)
      }
      val resolvedMaxWidth = if (widthSide != null) {
        targetConstraints.maxWidth
      } else {
        constraints.maxWidth.coerceAtLeast(targetConstraints.minWidth)
      }
      val resolvedMinHeight = if (heightSide != null) {
        targetConstraints.minHeight
      } else {
        constraints.minHeight.coerceAtMost(targetConstraints.maxHeight)
      }
      val resolvedMaxHeight = if (heightSide != null) {
        targetConstraints.maxHeight
      } else {
        constraints.maxHeight.coerceAtLeast(targetConstraints.minHeight)
      }
      Constraints(
        resolvedMinWidth,
        resolvedMaxWidth,
        resolvedMinHeight,
        resolvedMaxHeight
      )
    }
    val placeable = measurable.measure(wrappedConstraints)
    return layout(placeable.width, placeable.height) {
      placeable.place(0, 0)
    }
  }

  override fun IntrinsicMeasureScope.minIntrinsicWidth(
    measurable: IntrinsicMeasurable,
    height: Int
  ) = measurable.minIntrinsicWidth(height).let {
    val constraints = targetConstraints
    it.coerceIn(constraints.minWidth, constraints.maxWidth)
  }

  override fun IntrinsicMeasureScope.maxIntrinsicWidth(
    measurable: IntrinsicMeasurable,
    height: Int
  ) = measurable.maxIntrinsicWidth(height).let {
    val constraints = targetConstraints
    it.coerceIn(constraints.minWidth, constraints.maxWidth)
  }

  override fun IntrinsicMeasureScope.minIntrinsicHeight(
    measurable: IntrinsicMeasurable,
    width: Int
  ) = measurable.minIntrinsicHeight(width).let {
    val constraints = targetConstraints
    it.coerceIn(constraints.minHeight, constraints.maxHeight)
  }

  override fun IntrinsicMeasureScope.maxIntrinsicHeight(
    measurable: IntrinsicMeasurable,
    width: Int
  ) = measurable.maxIntrinsicHeight(width).let {
    val constraints = targetConstraints
    it.coerceIn(constraints.minHeight, constraints.maxHeight)
  }
}

Jetpack Compose 是谷歌在2019 Google i/o 大會(huì)上發(fā)布的新的庫(kù)??梢杂酶俑庇^的代碼創(chuàng)建 View,還有更強(qiáng)大的功能,以及還能提高開(kāi)發(fā)速度。 說(shuō)實(shí)話,View/Layout 的模式對(duì)安卓工程師來(lái)說(shuō)太過(guò)于熟悉,對(duì)于學(xué)習(xí)曲線陡峭的 Jetpack Compose 能不能很好的普及還是有所擔(dān)心。

如果使用 Jetpack Compose 呢,以下做些簡(jiǎn)單介紹:
在模塊中的 build.gradle 文件根據(jù)自己的需要新增下列的庫(kù)的依賴

    // compose
    composeVersion        : '1.0.0-alpha03',

    // compose
    implementation "androidx.compose.ui:ui:$versions.composeVersion"
    implementation "androidx.compose.material:material:$versions.composeVersion"
    implementation "androidx.compose.material:material-icons-extended:$versions.composeVersion"
    implementation "androidx.compose.foundation:foundation:$versions.composeVersion"
    implementation "androidx.compose.foundation:foundation-layout:$versions.composeVersion"
    implementation "androidx.compose.animation:animation:$versions.composeVersion"
    implementation "androidx.compose.runtime:runtime:$versions.composeVersion"
    implementation "androidx.compose.runtime:runtime-livedata:$versions.composeVersion"
    implementation "androidx.ui:ui-tooling:$versions.composeVersion"
    androidTestImplementation "androidx.ui:ui-test:$versions.composeVersion"

還有在模塊的 build.gradle 文件中新增下列的設(shè)置。

android {
    ...
    
    buildFeatures {
        compose true
    }
    
    composeOptions {
        kotlinCompilerExtensionVersion "${compose_version}"
        kotlinCompilerVersion versions.kotlin
    }
}
@Compose

所有關(guān)于構(gòu)建 View 的方法都必須添加 @Compose 的注解才可以。并且 @Compose 跟協(xié)程的 Suspend 的使用方法比較類似,被 @Compose 的注解的方法只能在同樣被 @Comopse 注解的方法中才能被調(diào)用。

/**
 * A wrapper around [CoilImage] setting a default [contentScale] and loading indicator for loading disney poster images.
 */
@Composable
fun NetworkImage(
  url: String,
  modifier: Modifier = Modifier,
  contentScale: ContentScale = ContentScale.Crop
) {
  CoilImageWithCrossfade(
    data = url,
    modifier = modifier,
    contentScale = contentScale,
    loading = {
      ConstraintLayout(
        modifier = Modifier.fillMaxSize()
      ) {
        val indicator = createRef()
        CircularProgressIndicator(
          modifier = Modifier.constrainAs(indicator) {
            top.linkTo(parent.top)
            bottom.linkTo(parent.bottom)
            start.linkTo(parent.start)
            end.linkTo(parent.end)
          }
        )
      }
    }
  )
@Preview

加上 @Preview 注解的方法可以在不運(yùn)行 App 的情況下就可以確認(rèn)布局的情況。
@Preview 的注解中比較常用的參數(shù)如下:

  • name: String: 為該 Preview 命名,該名字會(huì)在布局預(yù)覽中顯示。
  • showBackground: Boolean: 是否顯示背景,true 為顯示。
  • backgroundColor: Long: 設(shè)置背景的顏色。
  • showDecoration: Boolean: 是否顯示 Statusbar 和 Toolbar,true 為顯示。
  • group: String: 為該 Preview 設(shè)置 group 名字,可以在 UI 中以 group 為單位顯示。
  • fontScale: Float: 可以在預(yù)覽中對(duì)字體放大,范圍是從0.01。
  • widthDp: Int: 在 Compose 中渲染的最大寬度,單位為dp。
  • heightDp: Int: 在 Compose 中渲染的最大高度,單位為dp。

上面的參數(shù)都是可選參數(shù),還有像背景設(shè)置等的參數(shù)并不是對(duì)實(shí)際的 App 進(jìn)行設(shè)置,只是對(duì) Preview 中的背景進(jìn)行設(shè)置,為了更容易看清布局。

@Preview(showBackground = true, name = "Home UI", showDecoration = true)
@Composable
fun DefaultPreview() {
    MyApplicationTheme(darkTheme = false)  {
        Greeting("Android")
    }
}

在 IDE 的右上角有 Code,Split , Design 三個(gè)選項(xiàng)。分別是只顯示代碼,同時(shí)顯示代碼和布局和只顯示布局。
當(dāng)更改跟 UI 相關(guān)的代碼時(shí),會(huì)顯示如下圖的一個(gè)橫條通知,點(diǎn)擊 Build&Refresh 即可更新顯示所更改代碼的UI。

setContent

setContent 的作用是和 Layout/View 中的 setContentView 是一樣的。
setContent 的方法也是有 @Compose 注解的方法。所以,在 setContent 中寫入關(guān)于 UI 的 @Compopse 方法,即可在 Activity 中顯示。

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

  @VisibleForTesting val viewModel: MainViewModel by viewModels()

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    // observe toast.
    viewModel.toast.observe(this) {
      Toast.makeText(this, it, Toast.LENGTH_SHORT).show()
    }

    // fetch disney posters.
    viewModel.fetchDisneyPosterList()

    // set disney contents.
    setContent {
      DisneyComposeTheme {
        DisneyMain(
          viewModel = viewModel,
          backDispatcher = onBackPressedDispatcher
        )
      }
    }
  }
}
*Theme

在創(chuàng)建新的 Compose 項(xiàng)目時(shí)會(huì)自動(dòng)創(chuàng)建一個(gè)項(xiàng)目名+Theme 的 @Compose 方法。 我們可以通過(guò)更改顏色來(lái)完成對(duì)主題顏色的設(shè)置。 生成的 Theme 方法的代碼如下。

private val DarkColorPalette = darkColors(
  background = background,
  onBackground = background800,
  primary = purple200,
  primaryVariant = purple500,
  secondary = purple500,
  onPrimary = Color.White,
  onSecondary = Color.White
)

private val LightColorPalette = lightColors(
  background = Color.White,
  onBackground = Color.White,
  surface = Color.White,
  primary = purple200,
  primaryVariant = purple500,
  secondary = purple500,
  onPrimary = Color.White,
  onSecondary = Color.White
)

@Composable
fun DisneyComposeTheme(
  darkTheme: Boolean = isSystemInDarkTheme(),
  content: @Composable () -> Unit
) {
  val colors = if (darkTheme) {
    DarkColorPalette
  } else {
    LightColorPalette
  }

  val typography = if (darkTheme) {
    DarkTypography
  } else {
    LightTypography
  }

  MaterialTheme(
    colors = colors,
    typography = typography,
    shapes = shapes,
    content = content
  )
}

Theme方法中有正常主題和Dark主題的顏色設(shè)置,里面還有關(guān)于MeterialTheme的設(shè)置。
關(guān)于Theme方法的用法如下。

    // set disney contents.
    setContent {
      DisneyComposeTheme {
        DisneyMain(
          viewModel = viewModel,
          backDispatcher = onBackPressedDispatcher
        )
      }
    }

在 DisneyComposeTheme 里面的所有 UI 方法都會(huì)應(yīng)用上述主題中指定的顏色。

*Modifier

Modifier 是各個(gè) Compose 的 UI 組件一定會(huì)用到的一個(gè)類。它是被用于設(shè)置 UI 的擺放位置,padding 等信息的類。關(guān)于 Modifier 相關(guān)的設(shè)置實(shí)在是太多,在這里只介紹會(huì)經(jīng)常用到的。

  • padding 設(shè)置各個(gè) UI 的 padding。padding 的重載的方法一共有四個(gè)
Modifier.padding(10.dp) // 給上下左右設(shè)置成同一個(gè)值
Modifier.padding(10.dp, 11.dp, 12.dp, 13.dp) // 分別為上下左右設(shè)值
Modifier.padding(10.dp, 11.dp) // 分別為上下和左右設(shè)值
Modifier.padding(InnerPadding(10.dp, 11.dp, 12.dp, 13.dp))// 分別為上下左右設(shè)值
  • plus 可以把其他的Modifier加入到當(dāng)前的Modifier中。
Modifier.plus(otherModifier) // 把otherModifier的信息加入到現(xiàn)有的modifier中

這里設(shè)置的值必須為 dp,Compose為我們?cè)?Int 中擴(kuò)展了一個(gè)方法 dp,幫我們轉(zhuǎn)換成 dp。

  • fillMaxHeight, fillMaxWidth, fillMaxSize 類似于 match_parent ,填充整個(gè)父 layout 。
    例如:Modifier.fillMaxHeight() // 填充整個(gè)高度
@Composable
fun PosterDetails(
  viewModel: MainViewModel,
  pressOnBack: () -> Unit
) {
  val details: Poster? by viewModel.posterDetails.observeAsState()
  details?.let { poster ->
    ScrollableColumn(
      modifier = Modifier
        .background(MaterialTheme.colors.background)
        .fillMaxHeight()
    ) {
      ConstraintLayout {
        val (arrow, image, title, content) = createRefs()
        NetworkImage(
          url = poster.poster,
          modifier = Modifier.constrainAs(image) {
            top.linkTo(parent.top)
          }.fillMaxWidth()
            .aspectRatio(0.85f)
        )
        Text(
          text = poster.name,
          style = MaterialTheme.typography.h1,
          overflow = TextOverflow.Ellipsis,
          maxLines = 1,
          modifier = Modifier.constrainAs(title) {
            top.linkTo(image.bottom)
          }.padding(start = 16.dp, top = 16.dp)
        )
        Text(
          text = poster.description,
          style = MaterialTheme.typography.body2,
          modifier = Modifier.constrainAs(content) {
            top.linkTo(title.bottom)
          }.padding(16.dp)
        )
        Icon(
          asset = Icons.Filled.ArrowBack,
          tint = Color.White,
          modifier = Modifier.constrainAs(arrow) {
            top.linkTo(parent.top)
          }.padding(12.dp)
            .clickable(onClick = { pressOnBack() })
        )
      }
    }
  }
}
  • width, heigh, size 設(shè)置 Content 的寬度和高度。
Modifier.widthIn(2.dp) // 設(shè)置最大寬度
Modifier.heightIn(3.dp) // 設(shè)置最大高度
Modifier.sizeIn(4.dp, 5.dp, 6.dp, 7.dp) // 設(shè)置最大最小的寬度和高度
  • gravity 在 Column 中元素的位置
Modifier.gravity(Alignment.CenterHorizontally) // 橫向居中
Modifier.gravity(Alignment.Start) // 橫向居左
Modifier.gravity(Alignment.End) // 橫向居右
  • rtl, ltr 開(kāi)始布局UI的方向。
Modifier.rtl  // 從右到左
Modifier.ltr  // 從左到右
  • Modifier的方法都返回Modifier的實(shí)例的鏈?zhǔn)秸{(diào)用,所以只要連續(xù)調(diào)用想要使用的方法即可。
@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name!", modifier = Modifier.padding(20.dp).fillMaxSize())
}
*Column,Row

正如其名字一樣,Column 和 Row 可以理解為在 View/Layout 體系中的縱向和橫向的 ViewGroup。
需要傳入的參數(shù)一共有四個(gè)

  • Modifier 用上述的方法傳入已經(jīng)按需求設(shè)置好的 Modifier 即可
  • Arrangement.Horizontal, Arrangement.Vertical 需要給 Row 傳入 Arrangement.Horizontal,為 Column 傳入Arrangement.Vertical。
    這些值決定如何布置內(nèi)部 UI 組件。
    可傳入的值為 Center, Start, End, SpaceEvenly, SpaceBetween, SpaceAround。
    重點(diǎn)解釋一下SpaceEvenly, SpaceBetween, SpaceAround。
    SpaceEvenly:各個(gè)元素間的空隙為等比例。
    SpaceBetween:第一元素前和最后一個(gè)元素之后沒(méi)有空隙,所有空隙都按等比例放入各個(gè)元素之間。
    SpaceAround:把整體中一半的空隙平分的放入第一元素前和最后一個(gè)元素之后,剩余的一半等比例的放入各個(gè)元素之間。
  • Alignment.Vertical, Alignment.Horizontal
    需要給 Row 傳入 Alignment.Vertical,為 Column 傳入 Alignment.Horizontal。
    使用方法和 Modifier 的 gravity 中傳入?yún)?shù)的用法是一樣的,這里就略過(guò)了。
  • @Composable ColumnScope.() -> Unit 需要傳入標(biāo)有 @Compose 的 UI 方法。但是這里我們會(huì)有 lamda 函數(shù)的寫法來(lái)實(shí)現(xiàn)。
@Composable
fun RadioPosters(
  posters: List<Poster>,
  selectPoster: (Long) -> Unit,
  modifier: Modifier = Modifier
) {
  Column(
    modifier = modifier
      .statusBarsPadding()
      .background(MaterialTheme.colors.background)
  ) {
    LazyColumnFor(
      items = posters,
      contentPadding = PaddingValues(4.dp),
    ) { poster ->
      RadioPoster(
        poster = poster,
        selectPoster = selectPoster
      )
    }
  }
}
Column {
        Row(modifier = Modifier.ltr.fillMaxWidth(),horizontalArrangement = Arrangement.SpaceAround, verticalGravity = Alignment.Top) {
       // ..,...
    }

最后的話:

會(huì) flutter 的這個(gè)上手就超級(jí)快,要是學(xué)會(huì)這個(gè)再去學(xué) flutter 上手也很快

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容