Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Jetpack Compose. Introduction

Max
April 23, 2021

Jetpack Compose. Introduction

Max

April 23, 2021
Tweet

More Decks by Max

Other Decks in Programming

Transcript

  1. Зачем это понадобилось? • Жизненный цикл Activity и Fragment •

    Темы и Material-компоненты • Изменение конфигурации • Сложное управление состоянием • Простые вещи требуют много кода • Окончательно убить Databinding
  2. В чём профит? • Независимость от версии Android • Kotlin-only

    • Композитный подход • Unidirectional Data Flow • Обратная совместимость • Меньше кода • Потенциально выше скорость интерфейса
  3. Плюсы для бизнеса • Можно интегрировать в уже существующее приложение

    • Уменьшение вероятности ошибиться разработчику в состояниях приложения • Ускорение разработки за счёт новых API
  4. @Composable fun ClickCounter(clicks: Int, onClick: () -> Unit) { Button(onClick

    = onClick) { Text("I've been clicked $clicks times") } }
  5. @Composable fun DataStoreToggle( text: String, value: Boolean, onValueChanged: (Boolean) ->

    Unit ) { Row { Text(text) Checkbox(checked = value, onCheckedChange = onValueChanged) } }
  6. Composable-функции могут выполняться параллельно @Composable fun ListComposable(myList: List<String>) { Row(horizontalArrangement

    = Arrangement.SpaceBetween) { Column { for (item in myList) { Text("Item: $item") } } Text("Count: ${myList.size}") } }
  7. @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") } }
  8. Рекомпозиция пропускает как можно больше @Composable fun NamePicker( header: String,

    names: List<String>, onNameClicked: (String) -> Unit ) { Column { Text(header, style = MaterialTheme.typography.h5) Divider() LazyColumnFor(names) { name -> NamePickerItem(name, onNameClicked) } } } @Composable private fun NamePickerItem(name: String, onClicked: (String) -> Unit) { Text(name, Modifier.clickable(onClick = { onClicked(name) })) }
  9. Рекомпозиция может быть отменена • Рекомпозиция начинается, когда Compose считает,

    что параметры составного объекта могли измениться. • Рекомпозиция оптимистична это означает, что Compose ожидает завершить рекомпозицию до того, как параметры снова изменятся. • Когда рекомпозиция отменена, Compose исключает дерево пользовательского интерфейса из рекомпозиции.
  10. Composable-функции могут часто запускаться • В некоторых случаях composable-функция может

    запускаться для каждого кадра анимации пользовательского интерфейса. • Если функция выполняет дорогостоящие операции, такие как чтение из памяти устройства, то функция может вызвать искажение пользовательского интерфейса.
  11. LaunchedEffect @Composable fun MyScreen( state: UiState<List<Movie>>, scaffoldState: ScaffoldState = rememberScaffoldState()

    ) { if (state.hasError) { LaunchedEffect(scaffoldState.snackbarHostState) { scaffoldState.snackbarHostState.showSnackbar( message = "Error message", actionLabel = "Retry message" ) } } Scaffold(scaffoldState = scaffoldState) { /* ... */ } }
  12. rememberCoroutineScope @Composable fun MoviesScreen(scaffoldState: ScaffoldState = rememberScaffoldState()) { val scope

    = rememberCoroutineScope() Scaffold(scaffoldState = scaffoldState) { Column { Button( onClick = { scope.launch { scaffoldState.snackbarHostState .showSnackbar("Something happened!") } } ) { Text("Press me") } } } }
  13. rememberUpdatedState @Composable fun LandingScreen(onTimeout: () -> Unit) { val currentOnTimeout

    by rememberUpdatedState(onTimeout) LaunchedEffect(true) { delay(SplashWaitTimeMillis) currentOnTimeout() } /* Landing screen content */ }
  14. DisposableEffect @Composable fun BackHandler(backDispatcher: OnBackPressedDispatcher, onBack: () -> Unit) val

    currentOnBack by rememberUpdatedState(onBack) val backCallback = remember { object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { currentOnBack() } } } DisposableEffect(backDispatcher) { backDispatcher.addCallback(backCallback) onDispose { backCallback.remove() } } }
  15. SideEffect @Composable fun BackHandler( backDispatcher: OnBackPressedDispatcher, enabled: Boolean = true,

    onBack: () -> Unit ) { /* ... */ val backCallback = remember { /* ... */ } SideEffect { backCallback.isEnabled = enabled } /* Rest of the code */ }
  16. produceState @Composable fun loadNetworkImage( url: String, imageRepository: ImageRepository ): State<Result<Image>>

    { return produceState(initialValue = Result.Loading, url, imageRepository) { val image = imageRepository.load(url) value = if (image == null) { Result.Error } else { Result.Success(image) } } }
  17. derivedStateOf @Composable fun TodoList( highPriorityKeywords: List<String> = listOf("Review", "Unblock", "Compose")

    ) { val todoTasks = remember { mutableStateListOf<String>() } val highPriorityTasks by remember(todoTasks, highPriorityKeywords) { derivedStateOf { todoTasks.filter { it.containsWord(highPriorityKeywords) } } } Box(Modifier.fillMaxSize()) { LazyColumn { items(highPriorityTasks) { /* ... */ } items(todoTasks) { /* ... */ } } } }
  18. @Composable fun HelloContent() { Column(modifier = Modifier.padding(16.dp)) { Text( text

    = "Hello!", modifier = Modifier.padding(bottom = 8.dp), style = MaterialTheme.typography.h5 ) OutlinedTextField( value = "", onValueChange = { }, label = { Text("Name") } ) } }
  19. @Composable fun HelloContent() { Column(modifier = Modifier.padding(16.dp)) { var name

    by remember { mutableStateOf("") } Text( text = "Hello", modifier = Modifier.padding(bottom = 8.dp), style = MaterialTheme.typography.h5 ) OutlinedTextField( value = name, onValueChange = { name = it }, label = { Text("Name") } ) } }
  20. CompositionLocal val ActiveUser = compositionLocalOf<User> { error("No active user found!")

    } @Composable fun App(user: User) { CompositionLocalProvider(ActiveUser provides user) { SomeScreen() } } @Composable fun SomeScreen() { UserPhoto() } @Composable fun UserPhoto() { val user = ActiveUser.current ProfileIcon(src = user.profilePhotoUrl) }
  21. NavController val navController = rememberNavController() NavHost(navController, startDestination = "profile") {

    composable("profile") { Profile(...) } composable("friendslist") { FriendsList(...) } ... }
  22. Navigate to composable fun Profile(navController: NavController) { ... Button(onClick =

    { navController.navigate("friends") }) { Text(text = "Navigate next") } ... }
  23. Навигация с аргументами NavHost(startDestination = "profile/{userId}") { ... composable( "profile/{userId}",

    arguments = listOf(navArgument("userId") { type = NavType.StringType }) ) {...} } composable("profile/{userId}") { backStackEntry -> Profile(navController, backStackEntry.arguments?.getString("userId")) } navController.navigate("profile/user1234")
  24. Опциональные аргументы composable( "profile?userId={userId}", arguments = listOf(navArgument("userId") { defaultValue =

    "me" }) ) { backStackEntry -> Profile(navController, backStackEntry.arguments?.getString("userId")) }
  25. Deep Links val uri = "https://example.com" composable( "profile?id={id}", deepLinks =

    listOf(navDeepLink { uriPattern = "$uri/{id}" }) ) { backStackEntry -> Profile(navController, backStackEntry.arguments?.getString("id")) } <activity …> <intent-filter> ... <data android:scheme="https" android:host="www.example.com" /> </intent-filter> </activity>
  26. Nested Navigation NavHost(navController, startDestination = startRoute) { ... navigation(startDestination =

    nestedStartRoute, route = nested) { composable(nestedStartRoute) { ... } } ... }
  27. Bottom nav bar sealed class Screen(val route: String, @StringRes val

    resourceId: Int) { object Profile : Screen("profile", R.string.profile) object FriendsList : Screen("friendslist", R.string.friends_list) } val items = listOf(Screen.Profile, Screen.FriendsList)
  28. val navController = rememberNavController() Scaffold( bottomBar = { BottomNavigation {

    val navBackStackEntry by navController.currentBackStackEntryAsState() val currentRoute = navBackStackEntry?.arguments?.getString(KEY_ROUTE) items.forEach { screen -> BottomNavigationItem( icon = { Icon(Icons.Filled.Favorite) }, label = { Text(stringResource(screen.resourceId)) }, selected = currentRoute == screen.route, onClick = { navController.navigate(screen.route) { popUpTo = navController.graph.startDestination launchSingleTop = true } } ) } } } ) { NavHost(navController, startDestination = Screen.Profile.route) { composable(Screen.Profile.route) { Profile(navController) } composable(Screen.FriendsList.route) { FriendsList(navController) } } }
  29. Constraint @Composable fun ConstraintLayoutContent() { ConstraintLayout { val (button, text)

    = createRefs() Button( onClick = { /* Do something */ }, modifier = Modifier.constrainAs(button) { top.linkTo(parent.top, margin = 16.dp) } ) { Text("Button") } Text("Text", Modifier.constrainAs(text) { top.linkTo(button.bottom, margin = 16.dp) }) } }
  30. Custom layouts fun Modifier.customLayoutModifier(...) = this.layout { measurable, constraints ->

    ... }) @Composable fun MyBasicColumn( modifier: Modifier = Modifier, content: @Composable() () -> Unit ) { Layout( modifier = modifier, children = content ) { measurables, constraints -> // measure and position children given constraints logic here } }
  31. LazyColumn { // Add a single item item { Text(text

    = "First item") } // Add 5 items items(5) { index -> Text(text = "Item: $index") } // Add another single item item { Text(text = "Last item") } }
  32. val grouped = contacts.groupBy { it.firstName[0] } @OptIn(ExperimentalFoundationApi::class) @Composable fun

    ContactsList(grouped: Map<Char, List<Contact>>) { LazyColumn { grouped.forEach { (initial, contactsForInitial) -> stickyHeader { CharacterHeader(initial) } items(contactsForInitial) { contact -> ContactListItem(contact) } } } }
  33. var visible by remember { mutableStateOf(true) } AnimatedVisibility( visible =

    visible, enter = slideInVertically( initialOffsetY = { -40 } ) + expandVertically( expandFrom = Alignment.Top ) + fadeIn(initialAlpha = 0.3f), exit = slideOutVertically() + shrinkVertically() + fadeOut() ) { Text("Hello", Modifier.fillMaxWidth().height(200.dp)) }
  34. animateContentSize var message by remember { mutableStateOf("Hello") } Box(modifier =

    Modifier.background(Color.Blue).animateContentSize()) { Text(text = message) }
  35. Crossfade var currentPage by remember { mutableStateOf("A") } Crossfade(targetState =

    currentPage) { screen -> when (screen) { "A" -> Text("Page A") "B" -> Text("Page B") } }
  36. animate*AsState val alpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f)

    Box( Modifier.fillMaxSize() .graphicsLayer(alpha = alpha) .background(Color.Red) )
  37. rememberInfiniteTransition val infiniteTransition = rememberInfiniteTransition() val color by infiniteTransition.animateColor( initialValue

    = Color.Red, targetValue = Color.Green, animationSpec = infiniteRepeatable( animation = tween(1000, easing = LinearEasing), repeatMode = RepeatMode.Reverse ) ) Box(Modifier.fillMaxSize().background(color))
  38. Нажатие @Composable fun ClickableSample() { val count = remember {

    mutableStateOf(0) } // content that you want to make clickable Text( text = count.value.toString(), modifier = Modifier .clickable { count.value += 1 } ) }
  39. Больше гибкости! Modifier.pointerInput(Unit) { detectTapGestures( onPress = { /* Called

    when the gesture starts */ }, onDoubleTap = { /* Called on Double Tap */ }, onLongPress = { /* Called on Long Press */ }, onTap = { /* Called on Tap */ } ) }
  40. Скроллинг @Composable fun ScrollBoxes() { Column( modifier = Modifier .background(Color.LightGray)

    .size(100.dp) .verticalScroll(rememberScrollState()) ) { repeat(10) { Text("Item $it", modifier = Modifier.padding(2.dp)) } } }
  41. rememberScrollState @Composable private fun ScrollBoxesSmooth() { val state = rememberScrollState()

    LaunchedEffect(Unit) { state.animateScrollTo(100) } Column( modifier = Modifier .background(Color.LightGray) .size(100.dp) .padding(horizontal = 8.dp) .verticalScroll(state) ) { repeat(10) { Text("Item $it", modifier = Modifier.padding(2.dp)) } } }
  42. Scrollable @Composable fun ScrollableSample() { var offset by remember {

    mutableStateOf(0f) } Box( Modifier .size(150.dp) .scrollable( orientation = Orientation.Vertical, state = rememberScrollableState { delta -> offset += delta delta } ) .background(Color.LightGray), contentAlignment = Alignment.Center ) { Text(offset.toString()) } }
  43. Nested Scrolling val gradient = Brush.verticalGradient(0f to Color.Gray, 1000f to

    Color.White) Box( modifier = Modifier .background(Color.LightGray) .verticalScroll(rememberScrollState()) .padding(32.dp) ) { Column { repeat(6) { Box( modifier = Modifier .height(128.dp) .verticalScroll(rememberScrollState()) ) { Text( "Scroll here", modifier = Modifier .border(12.dp, Color.DarkGray) .background(brush = gradient) .padding(24.dp) .height(150.dp) ) } } } }
  44. Dragging Box(modifier = Modifier.fillMaxSize()) { var offsetX by remember {

    mutableStateOf(0f) } var offsetY by remember { mutableStateOf(0f) } Box( Modifier .offset { IntOffset( offsetX.roundToInt(), offsetY.roundToInt()) } .background(Color.Blue) .size(50.dp) .pointerInput(Unit) { detectDragGestures { change, dragAmount -> change.consumeAllChanges() offsetX += dragAmount.x offsetY += dragAmount.y } } ) }
  45. Swiping fun SwipeableSample() { val width = 96.dp val squareSize

    = 48.dp val swipeableState = rememberSwipeableState(0) val sizePx = with(LocalDensity.current) { squareSize.toPx() } val anchors = mapOf(0f to 0, sizePx to 1) Box( modifier = Modifier .width(width) .swipeable( state = swipeableState, anchors = anchors, thresholds = { _, _ -> FractionalThreshold(0.3f) }, orientation = Orientation.Horizontal ) .background(Color.LightGray) ) { Box( Modifier .offset { IntOffset(swipeableState.offset.value.roundToInt(), 0) } .size(squareSize) .background(Color.DarkGray) ) } }
  46. Multitouch: Panning, zooming, rotating @Composable fun TransformableSample() { var scale

    by remember { mutableStateOf(1f) } var rotation by remember { mutableStateOf(0f) } var offset by remember { mutableStateOf(Offset.Zero) } val state = rememberTransformableState { zoomChange, offsetChange, rotationChange -> scale *= zoomChange rotation += rotationChange offset += offsetChange } Box( Modifier .graphicsLayer( scaleX = scale, scaleY = scale, rotationZ = rotation, translationX = offset.x, translationY = offset.y ) .transformable(state = state) .background(Color.Blue) .fillMaxSize() ) }
  47. Минусы • Android Studio Canary • Не хватает некоторых возможностей

    View • Лучшие практики только начинают вырабатываться • Мелкие недоработки
  48. Что дальше? • Догнать по функционалу View • Kotlin Multiplatform

    • Поддержка в Intellij IDEA • Compose for Web • Compose for Native