当前位置: 首页>后端>正文

androidx.compose.runtime @Stable 注解,以及简单介绍如何使用 Jetpack Compose

官方文档对 @Stable 的注释:
稳定用于向组成编译器传达有关某种类型或函数的行为的某些保证。
当应用于类或接口时,[Stable]表示以下条件必须为真:
1)对于两个相同的实例,[equals]的结果将始终返回相同的结果。
2)当类型的公共财产发生变化时,将通知组成。
3)所有公共财产类型都是稳定的。
当应用于函数或属性时,@Stable 注解表示如果传入相同的参数,该函数将返回相同的结果。仅当参数和结果本身为[Stable],[Immutable], 或原始的。
该注解所隐含的不变量由组合编译器用于优化,如果不满足上述假设,则具有未定义的行为。 所以除非他们确定满足这些条件,否则不应该使用此注解。

举一个代码例子如下:

/**
 * 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 大会上发布的新的库。可以用更少更直观的代码创建 View,还有更强大的功能,以及还能提高开发速度。 说实话,View/Layout 的模式对安卓工程师来说太过于熟悉,对于学习曲线陡峭的 Jetpack Compose 能不能很好的普及还是有所担心。

如果使用 Jetpack Compose 呢,以下做些简单介绍:
在模块中的 build.gradle 文件根据自己的需要新增下列的库的依赖

    // 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 文件中新增下列的设置。

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

所有关于构建 View 的方法都必须添加 @Compose 的注解才可以。并且 @Compose 跟协程的 Suspend 的使用方法比较类似,被 @Compose 的注解的方法只能在同样被 @Comopse 注解的方法中才能被调用。

/**
 * 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 注解的方法可以在不运行 App 的情况下就可以确认布局的情况。
@Preview 的注解中比较常用的参数如下:

  • name: String: 为该 Preview 命名,该名字会在布局预览中显示。
  • showBackground: Boolean: 是否显示背景,true 为显示。
  • backgroundColor: Long: 设置背景的颜色。
  • showDecoration: Boolean: 是否显示 Statusbar 和 Toolbar,true 为显示。
  • group: String: 为该 Preview 设置 group 名字,可以在 UI 中以 group 为单位显示。
  • fontScale: Float: 可以在预览中对字体放大,范围是从0.01。
  • widthDp: Int: 在 Compose 中渲染的最大宽度,单位为dp。
  • heightDp: Int: 在 Compose 中渲染的最大高度,单位为dp。

上面的参数都是可选参数,还有像背景设置等的参数并不是对实际的 App 进行设置,只是对 Preview 中的背景进行设置,为了更容易看清布局。

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

在 IDE 的右上角有 Code,Split , Design 三个选项。分别是只显示代码,同时显示代码和布局和只显示布局。
当更改跟 UI 相关的代码时,会显示如下图的一个横条通知,点击 Build&Refresh 即可更新显示所更改代码的UI。

setContent

setContent 的作用是和 Layout/View 中的 setContentView 是一样的。
setContent 的方法也是有 @Compose 注解的方法。所以,在 setContent 中写入关于 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

在创建新的 Compose 项目时会自动创建一个项目名+Theme 的 @Compose 方法。 我们可以通过更改颜色来完成对主题颜色的设置。 生成的 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主题的颜色设置,里面还有关于MeterialTheme的设置。
关于Theme方法的用法如下。

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

在 DisneyComposeTheme 里面的所有 UI 方法都会应用上述主题中指定的颜色。

*Modifier

Modifier 是各个 Compose 的 UI 组件一定会用到的一个类。它是被用于设置 UI 的摆放位置,padding 等信息的类。关于 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))// 分别为上下左右设值
  • plus 可以把其他的Modifier加入到当前的Modifier中。
Modifier.plus(otherModifier) // 把otherModifier的信息加入到现有的modifier中

这里设置的值必须为 dp,Compose为我们在 Int 中扩展了一个方法 dp,帮我们转换成 dp。

  • fillMaxHeight, fillMaxWidth, fillMaxSize 类似于 match_parent ,填充整个父 layout 。
    例如:Modifier.fillMaxHeight() // 填充整个高度
@Composable
fun PosterDetails(
  viewModel: MainViewModel,
  pressOnBack: () -> Unit
) {
  val details: Posterby 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 设置 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.ltr  // 从左到右
  • Modifier的方法都返回Modifier的实例的链式调用,所以只要连续调用想要使用的方法即可。
@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name!", modifier = Modifier.padding(20.dp).fillMaxSize())
}
*Column,Row

正如其名字一样,Column 和 Row 可以理解为在 View/Layout 体系中的纵向和横向的 ViewGroup。
需要传入的参数一共有四个

  • Modifier 用上述的方法传入已经按需求设置好的 Modifier 即可
  • Arrangement.Horizontal, Arrangement.Vertical 需要给 Row 传入 Arrangement.Horizontal,为 Column 传入Arrangement.Vertical。
    这些值决定如何布置内部 UI 组件。
    可传入的值为 Center, Start, End, SpaceEvenly, SpaceBetween, SpaceAround。
    重点解释一下SpaceEvenly, SpaceBetween, SpaceAround。
    SpaceEvenly:各个元素间的空隙为等比例。
    SpaceBetween:第一元素前和最后一个元素之后没有空隙,所有空隙都按等比例放入各个元素之间。
    SpaceAround:把整体中一半的空隙平分的放入第一元素前和最后一个元素之后,剩余的一半等比例的放入各个元素之间。
  • Alignment.Vertical, Alignment.Horizontal
    需要给 Row 传入 Alignment.Vertical,为 Column 传入 Alignment.Horizontal。
    使用方法和 Modifier 的 gravity 中传入参数的用法是一样的,这里就略过了。
  • @Composable ColumnScope.() -> Unit 需要传入标有 @Compose 的 UI 方法。但是这里我们会有 lamda 函数的写法来实现。
@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) {
       // ..,...
    }

最后的话:

会 flutter 的这个上手就超级快,要是学会这个再去学 flutter 上手也很快


https://www.xamrdz.com/backend/38s1938282.html

相关文章: