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 中寫(xiě)入關(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ù)的寫(xiě)法來(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)系作者
  • 序言:七十年代末暴氏,一起剝皮案震驚了整個(gè)濱河市茂蚓,隨后出現(xiàn)的幾起案子振定,更是在濱河造成了極大的恐慌,老刑警劉巖细溅,帶你破解...
    沈念sama閱讀 211,042評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件侥涵,死亡現(xiàn)場(chǎng)離奇詭異沼撕,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)芜飘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門(mén)务豺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人嗦明,你說(shuō)我怎么就攤上這事笼沥。” “怎么了娶牌?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,674評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵奔浅,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我诗良,道長(zhǎng)汹桦,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,340評(píng)論 1 283
  • 正文 為了忘掉前任鉴裹,我火速辦了婚禮舞骆,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘径荔。我一直安慰自己督禽,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布总处。 她就那樣靜靜地躺著狈惫,像睡著了一般。 火紅的嫁衣襯著肌膚如雪辨泳。 梳的紋絲不亂的頭發(fā)上虱岂,一...
    開(kāi)封第一講書(shū)人閱讀 49,749評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音菠红,去河邊找鬼第岖。 笑死,一個(gè)胖子當(dāng)著我的面吹牛试溯,可吹牛的內(nèi)容都是我干的蔑滓。 我是一名探鬼主播,決...
    沈念sama閱讀 38,902評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼键袱!你這毒婦竟也來(lái)了燎窘?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,662評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤蹄咖,失蹤者是張志新(化名)和其女友劉穎褐健,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體澜汤,經(jīng)...
    沈念sama閱讀 44,110評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蚜迅,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了俊抵。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片谁不。...
    茶點(diǎn)故事閱讀 38,577評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖徽诲,靈堂內(nèi)的尸體忽然破棺而出刹帕,到底是詐尸還是另有隱情,我是刑警寧澤谎替,帶...
    沈念sama閱讀 34,258評(píng)論 4 328
  • 正文 年R本政府宣布偷溺,位于F島的核電站,受9級(jí)特大地震影響院喜,放射性物質(zhì)發(fā)生泄漏亡蓉。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評(píng)論 3 312
  • 文/蒙蒙 一喷舀、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧淋肾,春花似錦硫麻、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,726評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至碌尔,卻和暖如春浇辜,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背唾戚。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,952評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工柳洋, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人叹坦。 一個(gè)月前我還...
    沈念sama閱讀 46,271評(píng)論 2 360
  • 正文 我出身青樓熊镣,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子绪囱,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評(píng)論 2 348