Шпаргалка Ultimate Jetpack Compose

Шпаргалка Ultimate Jetpack Compose

16 февраля 2023 г.

Последние несколько недель я изучал компоновку и решил написать пост, чтобы поделиться своими заметками. Этот пост не предназначен для того, чтобы научить вас всему, что касается Compose, а скорее как дорожную карту, которую вы можете использовать, чтобы изучить Compose или узнать, чего вы еще не знаете о Compose.

Эта статья будет постоянно обновляться по мере того, как я узнаю что-то новое.

Поднятие состояния

Поднятие состояния — это шаблон перемещения состояния вверх, чтобы сделать компонент без состояния.

Применительно к составным объектам это часто означает добавление к составному объекту двух параметров.

  • value: T – текущее отображаемое значение
  • при изменении значения: (T) -> Единица – событие, запрашивающее изменение значения, где T – предлагаемое новое значение.

Местная композиция

Инструмент для неявной передачи данных через композицию.

CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
  Text(...)
}
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) {
  Icon(...)
  Text(...)
}

композицияLocalOf

Изменение значения, предоставленного во время перекомпоновки, делает недействительным только контент, который считывает его текущее значение.

data class Elevations(val card: Dp = 0.dp, val default: Dp = 0.dp)
val LocalElevations = compositionLocalOf { Elevations() } // Create composition with default value

val elevations = Elevations(card = 4.dp, default = 2.dp) // Provide different value here
CompositionLocalProvider(LocalElevations provides elevations) { ... }

staticCompositionLocalOf

Чтение staticCompositionLocalOf не отслеживается Compose. Изменение значения приводит к перекомпоновке всего лямбда-выражения контента, в котором предоставляется CompositionLocal.

Если маловероятно, что значение, предоставленное CompositionLocal, изменится или никогда не изменится, используйте staticCompositionLocalOf, чтобы повысить производительность.

Отслеживание изменений

помнить

Значение, вычисленное функцией запоминания, будет сохранено в дереве композиции и будет пересчитано только в случае изменения ключей для запоминания.

При добавлении памяти в компонуемый объект всегда спрашивайте себя: «Захочет ли какой-либо вызывающий объект управлять этим?»

  • Если ответ положительный, создайте параметр.
  • Если ответ отрицательный, сохраните его как локальную переменную.

Remember сохраняет значения в композиции и забывает о них, если компонуемый объект, вызывающий запоминание, удаляется. Это означает, что вы не должны полагаться на хранение важных вещей внутри составных объектов, которые добавляют и удаляют дочерние элементы, такие как LazyColumn.

помните, что можно сохранить

Он ведет себя аналогично запоминанию, но сохраненное значение сохранится после действия или восстановления процесса с использованием механизма сохраненного состояния экземпляра

запомнитьUpdatedState

В некоторых ситуациях вам может понадобиться зафиксировать в эффекте значение, при изменении которого вы не хотите, чтобы эффект перезапускался. Создайте ссылку на это значение, которое можно захватить и обновить.

запомнитьLauncherForActivityResult

val result = remember { mutableStateOf<Bitmap?>(null) }
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicturePreview()) {
  result.value = it
}

Button(onClick = { launcher.launch() }) { ... }

result.value?.let { image ->
  Image(image.asImageBitmap(), null, modifier = Modifier.fillMaxWidth())
}

запомнить CoroutineScope

Чтобы запустить сопрограмму за пределами составного объекта, но с такой областью действия, чтобы она автоматически отменялась после выхода из композиции

<цитата>

LaunchedEffect используется для определения объема заданий, инициированных композицией. rememberCoroutineScope предназначен для определения объема заданий, инициированных взаимодействием с пользователем.

Эффект запуска

Безопасный вызов функций приостановки из составного объекта.

Сопрограмма будет отменена, если LaunchedEffect покинет композицию.

Если LaunchedEffect перекомпонован с другими ключами, существующая сопрограмма будет отменена, а новая функция приостановки будет запущена в новой сопрограмме.

val currentOnTimeout by rememberUpdatedState(onTimeout)

LaunchedEffect(true) {
  delay(SplashWaitTimeMillis)
  currentOnTimeout()
}

снимок потока

Преобразовать объекты Compose State в поток.

Одноразовый эффект

DisposableEffect предназначен для побочных эффектов, которые необходимо устранить после изменения ключей или выхода компонуемого из композиции.

DisposableEffect(dispatcher) {
  dispatcher.addCallback(backCallback)
  onDispose {
    backCallback.remove()
  }
}

производное состояние

derivedStateOf используется, когда вы хотите, чтобы состояние создания было производным от другого состояния.

Государственный держатель

О держателях состояний всегда нужно помнить, чтобы удерживать их в Композиции, а не создавать каждый раз новую. Хорошей практикой является создание метода в том же файле, который делает это, чтобы удалить шаблон и избежать возможных ошибок.

Макеты

LazyColumn и LazyRow

Не перерабатывайте их дочерние элементы, как RecyclerView. Он генерирует новые Composables по мере того, как вы прокручиваете его, и по-прежнему работает эффективно, поскольку создание Composables относительно дешево по сравнению с созданием экземпляров Android Views.

val listState = rememberLazyListState()

LazyColumn(
  contentPadding,
  verticalArrangement / horizontalArrangement = Arrangement.spacedBy
) {
  stickyHeader { Header()  } // Experimental
  items(5) { index -> Text(text = "Item: $index") } // key = ...
  item { Text(text = "Last item") }
}

LaunchedEffect(listState) {
  snapshotFlow { listState.firstVisibleItemIndex }
}

firstVisibleItemIndex
firstVisibleItemScrollOffset

scrollToItem()
animateScrollToItem()

Макет ограничения

ConstraintLayout {
  val (button, text) = createRefs()

  Button(
    modifier = Modifier.constrainAs(button) {
      top.linkTo(parent.top, margin = 16.dp)
    }
  )

  Text("Text", Modifier.constrainAs(text) {
    top.linkTo(button.bottom, margin = 16.dp)
  })
}

ConstraintSet {
  val button = createRefFor("button")
  val text = createRefFor("text")

  constrain(button) { ... }
  constrain(text) { ... }
}

Макет

Вместо того, чтобы контролировать, как один составной элемент измеряется и размещается на экране, вам может понадобиться группа составных элементов. Для этого вы можете использовать компоновку макета, чтобы вручную контролировать, как измерять и размещать дочерние элементы макета.

Layout(
  modifier = modifier,
  content = content
) { measurables, constraints ->
  // Don't constrain child views further, measure them with given constraints
  val placeables = measurables.map { measurable -> measurable.measure(constraints) }

  // Set the size of the layout as big as it can
  layout(constraints.maxWidth, constraints.maxHeight) {
    var yPosition = 0

    placeables.forEach { placeable ->
      placeable.placeRelative(x = 0, y = yPosition)
      yPosition += placeable.height
    }
  }
}

Внутренние измерения

Строка(modifier = modifier.height(IntrinsicSize.Min))

Навигация

val navController = rememberNavController()
val backstackEntry = navController.currentBackStackEntryAsState()

NavHost(navController = navController, startDestination = "profile") {
  composable(
    "profile/{userId}/?mode={mode}",
    arguments = listOf(
      navArgument("userId") { type = NavType.StringType },
      navArgument("mode") { defaultValue = "lite" },
    ),
    deepLinks = listOf(navDeepLink {
      uriPattern = "rally://$accountsName/{name}"
    })
  ) { 
    Profile(/*...*/) 
  }

  loginGraph()
}

fun NavGraphBuilder.loginGraph(navController: NavController) {
  navigation(startDestination = "username", route = "login") {
    composable("username") { ... }
  }
}

navController.navigate("friends") {
  popUpTo("home") { inclusive = true }
}

navController.navigate(screen.route) {
  // Pop up to the start destination of the graph to avoid building up a large stack 
  // of destinations on the back stack as users select items
  popUpTo(navController.graph.findStartDestination().id) {
    saveState = true
  }
  // Avoid multiple copies of the same destination when reselecting the same item
  launchSingleTop = true
  // Restore state when reselecting a previously selected item
  restoreState = true
}

Модель представления

@HiltViewModel
class ExampleViewModel @Inject constructor(
  ...
) : ViewModel() { /* ... */ }

@AndroidEntryPoint
class ExampleActivity : AppCompatActivity()

@Composable
fun ExampleScreen(
  exampleViewModel: ExampleViewModel = viewModel()
) { /* ... */ }

Анимация

Вы можете создать значение анимации, просто заключив изменяющееся значение в соответствующий вариант составных объектов animate*AsState.

  • Float, Color, Dp, Size, Bounds, Offset, Rect, Int, IntOffset и IntSize

Вы можете комбинировать несколько объектов перехода с помощью оператора +.

MutableTransitionState(false).apply {
  targetState = true  // Start the animation immediately
  // isIdle, currentState
}

Анимированная видимость

  • Содержимое в пределах AnimatedVisibility (прямые или косвенные дочерние элементы) может использовать модификатор animateEnterExit, чтобы указать различное поведение анимации для каждого из них.

Анимированный контент

  • SizeTransform определяет, как размер должен анимироваться между исходным и целевым содержимым.

animateContentSize

  • Анимирует изменение размера

Кроссфейд

  • Анимация между двумя макетами с плавной анимацией.

Анимация

  • Animatable — это держатель значения, который может анимировать значение по мере его изменения с помощью animateTo.
  • snapTo немедленно устанавливает текущее значение в целевое значение.
  • animateDecay запускает анимацию, которая замедляется с заданной скоростью.
val color = remember { Animatable(Color.Gray) }
LaunchedEffect(ok) { color.animateTo(if (ok) Color.Green else Color.Red) }InfiniteTransition

Бесконечный переход

  • Содержит одну или несколько дочерних анимаций, таких как переход, но анимация запускается, как только входит в композицию, и не останавливается, пока не будет удалена.
  • Используйте функцию rememberInfiniteTransition.

Переход

Transition управляет одной или несколькими анимациями как дочерними и запускает их одновременно между несколькими состояниями.

var currentState by remember { mutableStateOf(BoxState.Collapsed) }
val transition = updateTransition(currentState)

// Start in collapsed state and immediately animate to expanded
var currentState = remember { MutableTransitionState(BoxState.Collapsed) }
currentState.targetState = BoxState.Expanded
val transition = updateTransition(currentState)

transition.AnimatedVisibility
transition.AnimatedContent

Спецификация анимации

spring(
  dampingRatio = Spring.DampingRatioHighBouncy,
  stiffness = Spring.StiffnessMedium
)

tween(
  durationMillis = 300,
  delayMillis = 50,
  easing = LinearOutSlowInEasing
)

// animates based on the snapshot values specified at different timestamps
keyframes {
  durationMillis = 375
  0.0f at 0 with LinearOutSlowInEasing // for 0-15 ms
  0.2f at 15 with FastOutLinearInEasing // for 15-75 ms
  0.4f at 75 // ms
  0.4f at 225 // ms
}

// runs a animation repeatedly until it reaches the specified iteration count
repeatable(
  iterations = 3,
  animation = tween(durationMillis = 300),
  repeatMode = RepeatMode.Reverse
)

// like repeatable, but it repeats for an infinite amount of iterations
infiniteRepeatable(
  animation = tween(durationMillis = 300),
  repeatMode = RepeatMode.Reverse
)

// immediately switches the value to the end value
snap(delayMillis = 50)

Тематика

Определяя цвета, мы называем их «буквально», исходя из значения цвета, а не «семантически», например. Red500 не основной. Это позволяет нам определять несколько тем, например. другой цвет может считаться основным в темной теме или на экране с другим стилем.

isSystemInDarkTheme() / MaterialTheme.colors.isLight

color: Color = MaterialTheme.colors.surface, contentColor: Color = contentColorFor(color)

При установке цвета любых элементов предпочтительнее использовать для этого поверхность, так как она устанавливает соответствующее значение цвета содержимого CompositionLocal. Будьте осторожны с прямыми вызовами Modifier.background, которые не устанавливают соответствующее содержимое. цвет.

Текстовое поле по умолчанию

textFieldColors

outlinedTextFieldColors

Предоставить стиль текста

Как же компоненты задают стиль оформления темы? Под капотом они используют компонуемый ProvideTextStyle (который сам использует CompositionLocal) для установки «текущего» TextStyle. Составляемый текст по умолчанию запрашивает этот «текущий» стиль, если вы не укажете конкретный параметр textStyle.

Ресурсы

stringResource(R.string.congratulate, "New Year", 2021)
dimensionResource(R.dimen.padding_small)

painterResource(id = R.drawable.ic_logo)
animatedVectorResource(id = R.drawable.animated_vector)

Icon(Icons.Rounded.Menu)

Шрифты

private val light = Font(R.font.raleway_light, FontWeight.W300)
private val regular = Font(R.font.raleway_regular, FontWeight.W400)
private val medium = Font(R.font.raleway_medium, FontWeight.W500)
private val semibold = Font(R.font.raleway_semibold, FontWeight.W600)

// Create a font family to use in TextStyles
private val craneFontFamily = FontFamily(light, regular, medium, semibold)

сборка AnnotatedString

buildAnnotatedString {
  append("This is some unstyled textn")
  withStyle(SpanStyle(color = Color.Red)) {
    append("Red textn")
  }
  pushStringAnnotation(tag = "URL", annotation = "<https://developer.android.com>")
  withStyle(SpanStyle(fontSize = 24.sp)) {
    append("Large text")
  }
  pop()
}

Общие модификаторы

align
alignBy

// animateEnterExit modifier can be used for any direct or indirect children 
// of AnimatedVisibility to create a different enter/exit animation than 
// what's specified in AnimatedVisibility.
animateEnterExit(enter, exit)

border
background
clip
clipToBounds

drawBehind
drawWithCache
drawWithContent

// The draw layer can be invalidated separately from parents. 
// scaleX, scaleY, rotationXYZ, alpha, shadowElevation, shape, clip, shape
// Use with state values such as ScrollState or LazyListState
graphicsLayer

shadow
zIndex

onKeyEvent

// Creates a LayoutModifier that allows changing how the wrapped element is 
// measured and laid out.
layout

absoluteOffset
offset

fillMaxHeight, fillMaxWidth, fillMaxSize
// matches the size of the Box after all other children have been measured 
// to obtain the Box's size.
matchParentSize

heighIn(min, max), widthIn(min, max)

Модификаторы жестов

combinedClickable(onLongClick, onDoubleClick, onClick)

draggable(
  orientation = Orientation.Horizontal,
  state = rememberDraggableState { delta ->
    val newValue = offsetPosition.value + delta
    offsetPosition.value = newValue.coerceIn(minPx, maxPx)
  }
)

swipeable(
  state = swipeableState,
  anchors = anchors,
  thresholds = { _, _ -> FractionalThreshold(0.3f) },
  orientation = Orientation.Horizontal
)

pointerInput(Unit) {
  detectTapGestures(onPress, onDoubleTap, onLongPress, onTap)
  detectDragGestures { change, dragAmount ->
    change.consumeAllChanges()
    offsetX += dragAmount.x
    offsetY += dragAmount.y
  }
}

horizontalScroll, verticalScroll

// Detects the scroll gestures, but does not offset its contents.
// Has nested scroll built in
scrollable

nestedScroll(nestedScrollConnection, nestedScrollDispatcher)

Разное

Локальный программный контроллер клавиатуры

val keyboardController = LocalSoftwareKeyboardController.current
// Calling this function is considered a side-effect and should not be called directly from recomposition
keyboardController.hide() 

LocalOnBackPressedDispatcherOwner и BackHandler

var backHandlingEnabled by remember { mutableStateOf(true) }
var backPressedCount by remember { mutableStateOf(0) }
BackHandler(backHandlingEnabled) { backPressedCount++ }

val dispatcher = LocalOnBackPressedDispatcherOwner.current!!.onBackPressedDispatcher

Button(onClick = { dispatcher.onBackPressed() }) {
  Text("Press Back count $backPressedCount")
}

Локальная плотность

val sizeInPx = with(LocalDensity.current) { 16.dp.toPx() }
val (minPx, maxPx) = with(LocalDensity.current) { min.toPx() to max.toPx() }

Фокусреквестер

val focusRequester = remember { FocusRequester() }
var color by remember { mutableStateOf(Black) }
Box(
  Modifier
    .clickable { focusRequester.requestFocus() }
    .border(2.dp, color)
    // The focusRequester should be added BEFORE the focusable.
    .focusRequester(focusRequester)
    // The onFocusChanged should be added BEFORE the focusable that is being observed.
    .onFocusChanged { color = if (it.isFocused) Green else Black }
    .focusable()
)

Спасибо, что прочитали. Если у вас есть какие-либо предложения, не стесняйтесь обращаться ко мне.

Ресурсы

https://developer.android.com/jetpack/compose/architecture

https://developer.android.com/jetpack/compose/resources

https://developer.android.com/jetpack/compose/animation

https://developer.android.com/jetpack/compose/gestures

https://developer.android.com/jetpack/compose/navigation

https://developer.android.com/jetpack/compose/modifiers-list

https://developer.android.com/jetpack/compose/mental-model

https://developer.android.com/jetpack/compose/compositionlocal

https://developer.android.com/codelabs/jetpack-compose-basics

https://developer.android.com/codelabs/jetpack-compose-layouts

https://developer.android.com/codelabs/jetpack-compose-state

https://developer.android.com/codelabs/jetpack-compose-theming

https://developer.android.com/codelabs/jetpack-compose-animation

https://developer.android.com/codelabs/jetpack-compose-navigation

https://developer.android.com/codelabs/jetpack-compose-advanced -состояние-побочные-эффекты

https://developer.android.com/reference/kotlin/androidx /compose/runtime/saveable/package-summary

:::информация Также опубликовано здесь.

:::


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE