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

Jetpack Compose. Introduction

Avatar for Max Max
April 23, 2021

Jetpack Compose. Introduction

Avatar for Max

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