$30 off During Our Annual Pro Sale. View Details »

Android Compose Component - mapping.

TaeHwan
June 27, 2022

Android Compose Component - mapping.

Base Material 2, Compose 1.2.x Compose Component mapping

TaeHwan

June 27, 2022
Tweet

More Decks by TaeHwan

Other Decks in Programming

Transcript

  1. 시작하기 전에 • Material 2 버전 기반의 Compose Component 구성

    • Scaffold 기반 UI 작업이 가능하도록 만든 Component 구성 Compose Component Mapping 이야기를 하기 전에 Material 2 버전 기반의 Compose Componet 구성에 대한 이야기이며, Scaffold 기반의 UI 작업을 하기 위한 Component 구성에 대한 이야기
  2. 현재 사용하는 버전 정보 • Android Compose 1.2.0 • Material

    2 • Kotlin 1.6.21 - Compose 강제 디펜던시 • Accompanist Compose 1.2.0 기반, Material 2버전 활용하고, Kotlin 버전은 Compose 강제 디펜던시로 1.6.21 활용
  3. • Activity/Fragment에서 가장 기본 틀로 활용 ◦ Background color -

    MaterialTheme.colors.background, ◦ Content color - contentColorFor(backgroundColor) Scaffold Scaffold는 Compose 뷰를 올리는 가장 기본적인 틀로 활용 기본 색상은 MaterialTheme의 background, onBackground 컬러 기준으로 기본 적용
  4. Scaffold @Composable fun Scaffold( modifier: Modifier = Modifier , scaffoldState:

    ScaffoldState = rememberScaffoldState(), topBar: @Composable () -> Unit = {}, bottomBar: @Composable () -> Unit = {}, snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) }, floatingActionButton: @Composable () -> Unit = {}, floatingActionButtonPosition: FabPosition = FabPosition. End, isFloatingActionButtonDocked: Boolean = false, drawerContent: @Composable (ColumnScope.() -> Unit)? = null, drawerGesturesEnabled: Boolean = true, drawerShape: Shape = MaterialTheme. shapes.large, drawerElevation: Dp = DrawerDefaults. Elevation, drawerBackgroundColor: Color = MaterialTheme. colors.surface, drawerContentColor: Color = contentColorFor(drawerBackgroundColor) , drawerScrimColor: Color = DrawerDefaults. scrimColor, backgroundColor: Color = MaterialTheme. colors.background, contentColor: Color = contentColorFor(backgroundColor) , content: @Composable (PaddingValues) -> Unit ) Scaffold에는 topBar - AppBar, bottomBar - Navigation bar, floating, drawer 메뉴 활용이 가능
  5. • TopAppBar를 활용하거나, 직접 Content를 채워넣을 수 있다. Scaffold -

    topBar TopBar는 TopAppBar 기준으로 적용이 가능하지만, 직접 Content를 채워 넣는 것도 가능
  6. Scaffold @Composable fun Scaffold( modifier: Modifier = Modifier , scaffoldState:

    ScaffoldState = rememberScaffoldState(), topBar: @Composable () -> Unit = {}, bottomBar: @Composable () -> Unit = {}, snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) }, floatingActionButton: @Composable () -> Unit = {}, floatingActionButtonPosition: FabPosition = FabPosition.End, isFloatingActionButtonDocked: Boolean = false, drawerContent: @Composable (ColumnScope.() -> Unit)? = null, drawerGesturesEnabled: Boolean = true, drawerShape: Shape = MaterialTheme.shapes.large, drawerElevation: Dp = DrawerDefaults.Elevation, drawerBackgroundColor: Color = MaterialTheme.colors.surface, drawerContentColor: Color = contentColorFor(drawerBackgroundColor), drawerScrimColor: Color = DrawerDefaults.scrimColor, backgroundColor: Color = MaterialTheme. colors.background, contentColor: Color = contentColorFor(backgroundColor) , content: @Composable (PaddingValues) -> Unit ) 필요한 예에 따라 매핑을 할 수 있는데, Scaffold에서 흰색 부분에 대한 매핑 적용하여 사용하고 있다.
  7. BottomSheetScaffold @Composable @ExperimentalMaterialApi fun BottomSheetScaffold( sheetContent: @Composable ColumnScope.() -> Unit,

    modifier: Modifier = Modifier, scaffoldState: BottomSheetScaffoldState = rememberBottomSheetScaffoldState(), topBar: (@Composable () -> Unit)? = null, snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) }, floatingActionButton: (@Composable () -> Unit)? = null, floatingActionButtonPosition: FabPosition = FabPosition.End, sheetGesturesEnabled: Boolean = true, sheetShape: Shape = MaterialTheme.shapes.large, sheetElevation: Dp = BottomSheetScaffoldDefaults.SheetElevation, sheetBackgroundColor: Color = MaterialTheme.colors.surface, sheetContentColor: Color = contentColorFor(sheetBackgroundColor), sheetPeekHeight: Dp = BottomSheetScaffoldDefaults.SheetPeekHeight, drawerContent: @Composable (ColumnScope.() -> Unit)? = null, drawerGesturesEnabled: Boolean = true, drawerShape: Shape = MaterialTheme.shapes.large, drawerElevation: Dp = DrawerDefaults.Elevation, drawerBackgroundColor: Color = MaterialTheme.colors.surface, drawerContentColor: Color = contentColorFor(drawerBackgroundColor), drawerScrimColor: Color = DrawerDefaults.scrimColor, backgroundColor: Color = MaterialTheme.colors.background, contentColor: Color = contentColorFor(backgroundColor), content: @Composable (PaddingValues) -> Unit ) 이번엔 BottomSheetScaffold로 BottomSheet가 필요한 경우에 이를 활용할 수 있다. 앞에 보았던 scaffold에 BottomSheet를 구현한데 필요한 데이터를 포함하고 있다.
  8. • Scaffold에 BottomSheet를 포함하는 경우 활용 ◦ Scaffold 대신 BottomDrawer를

    활용하여 커스텀 형태 활용 Scaffold - BottomSheet https://developer.android.com/reference/kotlin/androidx/compose/material/package-summary#bottomdrawer BottomSheet는 Model drawers(백그라운드 shadow), BottomDrawer로 일반적인 바텀 시트 적용이 가능한데, 커스텀이 많이 필요하다면 BottomDrawer를 활용하는 게 좋아 보인다. BottomSheetScaffold는 최소 높이를 강제한다.
  9. BottomSheetScaffold @Composable @ExperimentalMaterialApi fun BottomSheetScaffold( sheetContent: @Composable ColumnScope.() -> Unit,

    modifier: Modifier = Modifier, scaffoldState: BottomSheetScaffoldState = rememberBottomSheetScaffoldState(), topBar: (@Composable () -> Unit)? = null, snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) }, floatingActionButton: (@Composable () -> Unit)? = null, floatingActionButtonPosition: FabPosition = FabPosition.End, sheetGesturesEnabled: Boolean = true, sheetShape: Shape = MaterialTheme.shapes.large, sheetElevation: Dp = BottomSheetScaffoldDefaults.SheetElevation, sheetBackgroundColor: Color = MaterialTheme.colors.surface, sheetContentColor: Color = contentColorFor(sheetBackgroundColor), sheetPeekHeight: Dp = BottomSheetScaffoldDefaults.SheetPeekHeight, drawerContent: @Composable (ColumnScope.() -> Unit)? = null, drawerGesturesEnabled: Boolean = true, drawerShape: Shape = MaterialTheme.shapes.large, drawerElevation: Dp = DrawerDefaults.Elevation, drawerBackgroundColor: Color = MaterialTheme.colors.surface, drawerContentColor: Color = contentColorFor(drawerBackgroundColor), drawerScrimColor: Color = DrawerDefaults.scrimColor, backgroundColor: Color = MaterialTheme.colors.background, contentColor: Color = contentColorFor(backgroundColor), content: @Composable (PaddingValues) -> Unit ) 커스텀을 한다면 필요한 부분만 매핑해 활용한다.
  10. • 우리만의 틀을 만들기 위함 • Scaffold 터치 이벤트 발생

    시 키보드 및 Focus 날림 처리 • 컬러 셋 지정을 편하게 할 수 있음 Scaffold 매핑 이유 결국 Scaffold 매핑 한 이유는 접근 제한을 한다거나, 기본 색상 정보를 Material을 따르지 않고 싶다면 매핑을 해주는 편이 더 좋다. 포커스 처리를 공통화한다거나, 나만의 UI를 추가한다거나 등이 이유일 수 있다.
  11. 키보드 노출 1. TextField Focus on 2. 키보드 노출 화면

    터치 시 • 아무런 동작을 하지 않음 백 키 이벤트가 발생하면 • 1회 - 키보드 Hide • 2회 - 포커스가 사라짐 • 3회 - 이전 화면으로 돌아감 Scaffold - Touch focus clear Scaffold 터치 시 focus clear를 처리해두면 편한데, TextField는 아무런 코드 없어도 기본 focus on 시 키보드가 노출된다. 아무런 작업이 없을 경우 화면 터치 시 아무런 동작을 하지 않는다.
  12. • Scaffold 터치 시 키보드 focus 처리토록 변경 • Accompanist

    키보드 관련 라이브러리로 키보드 visible 상태를 알 수 있었지만 ◦ Compose 정식 버전에 포함되면서 이 코드 제거 • UI 높이 상태 체크하는 방식으로 키보드 노출 여부 확인하는 코드로 상태 확인 후 focus 변경 Scaffold - Touch focus clear 결국 focus 처리를 해주는 게 가장 쉬운데, 키보드가 떠있고, 포커싱 상태일 때 focus만 clear 하면 된다. 기존엔 Accompanist에서 키보드 관련 라이브러리를 활용해 visible 상태를 확인할 수 있었지만 아쉽게 Compose 정식에 포함되면서 일부 빠지고 포함되었다. 그래서 전통적인 방식의 view 높이 확인하는 코드를 활용할 수 있다.
  13. Scaffold - Touch focus clear internal fun View.isKeyboardOpen (): Boolean

    { val rect = Rect() getWindowVisibleDisplayFrame(rect) val screenHeight = rootView.height val keypadHeight = screenHeight - rect. bottom return keypadHeight > screenHeight * 0.15 } windowVisibleDisplayFrame을 활용하여 일부 높이 이상 가려졌는지 체크한다.
  14. Scaffold - Touch focus clear internal fun View.isKeyboardOpen(): Boolean {

    val rect = Rect() getWindowVisibleDisplayFrame(rect) val screenHeight = rootView.height val keypadHeight = screenHeight - rect.bottom return keypadHeight > screenHeight * 0.15 } @Composable internal fun rememberIsKeyboardOpen (): State<Boolean> { val view = LocalView.current return produceState(initialValue = view.isKeyboardOpen()) { val viewTreeObserver = view. viewTreeObserver val listener = ViewTreeObserver.OnGlobalLayoutListener { value = view.isKeyboardOpen() } viewTreeObserver.addOnGlobalLayoutListener(listener) awaitDispose { viewTreeObserver.removeOnGlobalLayoutListener(listener) } } } 이 이벤트는 State를 통해 관리하고, viewTree를 활용한다.
  15. Scaffold - Touch focus clear internal fun Modifier.clearFocusOnKeyboardDismissAndTouch( clearFocus: ()

    -> Unit = {}, ): Modifier = composed { val focusManager = LocalFocusManager.current val focusRequester = remember { FocusRequester() } var keyboardAppearedSinceLastFocused by remember { mutableStateOf(false) } fun clear() { clearFocus() focusManager.clearFocus() keyboardAppearedSinceLastFocused = false } var isFocused by remember { mutableStateOf(false) } val isKeyboardOpen by rememberIsKeyboardOpen() LaunchedEffect(key1 = isKeyboardOpen, key2 = isFocused) { if (isFocused) { if (isKeyboardOpen) { keyboardAppearedSinceLastFocused = true } else if (keyboardAppearedSinceLastFocused) { clear() } } } Compose Modifier를 확장해 적용할 수 있는데, keyboardOpen, focused 상태를 활용한다.
  16. Scaffold - Touch focus clear internal fun Modifier.clearFocusOnKeyboardDismissAndTouch( clearFocus: ()

    -> Unit = {}, ): Modifier = composed { // 생략 this .onFocusEvent { if (isFocused != it.isFocused) { isFocused = it.isFocused if (isFocused && isKeyboardOpen.not()) { keyboardAppearedSinceLastFocused = false } } } .pointerInput(Unit) { detectTapGestures { clear() } } .focusRequester(focusRequester) } Modifier에서는 FocusEvent에 정보를 등록하고, pointInput 상태를 활용해 clear 할 수 있도록 처리한다.
  17. • Status bar, Navigation bar 영역까지 차야 하는 UI 활용

    ◦ WindowCompat.setDecorFitsSystemWindows(window, false) • 이 경우 status bar, navigation bar 영역에 대한 padding을 제공한다. ◦ .statusBarsPadding() ◦ .navigationBarsPadding() • 가장 쉬운 방법은 Scaffold content에서 제공하는 paddingValue를 content 영역에 그대로.setPadding(it) 해주는 방법 Scaffold - padding Status bar, navigation bar 영역까지 백그라운드를 채워야 하는 경우라면 setDecorFitsSystemWindows을 활용, 이때 padding이 필요한 UI에 padding 적용
  18. • TopAppBar는 Scaffold의 topBar content로 활용 할 수 있다. ◦

    backgroundColor: Color = MaterialTheme.colors.primarySurface ◦ contentColor: Color = contentColorFor(backgroundColor) TopAppBar TopAppBar를 매핑해 활용하는 이유는 내부에서 컬러 정보를 isLight 값을 통해 바꿔버리기 때문이다. darkTheme라서 false로 보냈더니 색상 노출이 달라지는데, 저 isLight 키워드가 몇 가지 규칙을 변경해버린다.
  19. TopAppBar @Composable fun TopAppBar( modifier: Modifier = Modifier , backgroundColor:

    Color = MaterialTheme. colors.primarySurface, contentColor: Color = contentColorFor(backgroundColor) , elevation: Dp = AppBarDefaults. TopAppBarElevation , contentPadding: PaddingValues = AppBarDefaults. ContentPadding , content: @Composable RowScope.() -> Unit ) TopAppBar는 크게 2가지 함수를 제공한다. content를 커스텀 할 수 있는 TopAppBar와
  20. TopAppBar @Composable fun TopAppBar( modifier: Modifier = Modifier , backgroundColor:

    Color = MaterialTheme. colors.primarySurface, contentColor: Color = contentColorFor(backgroundColor) , elevation: Dp = AppBarDefaults. TopAppBarElevation , contentPadding: PaddingValues = AppBarDefaults. ContentPadding , content: @Composable RowScope.() -> Unit ) // OR @Composable fun TopAppBar( title: @Composable () -> Unit, modifier: Modifier = Modifier , navigationIcon: @Composable (() -> Unit)? = null, actions: @Composable RowScope.() -> Unit = {}, backgroundColor: Color = MaterialTheme. colors.primarySurface, contentColor: Color = contentColorFor(backgroundColor) , elevation: Dp = AppBarDefaults. TopAppBarElevation ) 기본 title, navigationIcon, actions에 대한 틀을 제공하는 컴포넌트이다.
  21. TopAppBar @Composable fun TopAppBar( modifier: Modifier = Modifier, backgroundColor: Color

    = MaterialTheme.colors. primarySurface, contentColor: Color = contentColorFor(backgroundColor), elevation: Dp = AppBarDefaults.TopAppBarElevation, contentPadding: PaddingValues = AppBarDefaults.ContentPadding, content: @Composable RowScope.() -> Unit ) // OR @Composable fun TopAppBar( title: @Composable () -> Unit, modifier: Modifier = Modifier, navigationIcon: @Composable (() -> Unit)? = null, actions: @Composable RowScope.() -> Unit = {}, backgroundColor: Color = MaterialTheme.colors. primarySurface, contentColor: Color = contentColorFor(backgroundColor), elevation: Dp = AppBarDefaults. TopAppBarElevation )
  22. TopAppBar - Mapping @Composable fun TopAppBar( title: @Composable () ->

    Unit, modifier: Modifier = Modifier, navigationIcon: @Composable (() -> Unit)? = null, actions: @Composable RowScope.() -> Unit = {}, backgroundColor: Color = MaterialTheme. colors.primarySurface, contentColor: Color = contentColorFor(backgroundColor), elevation: Dp = AppBarDefaults. TopAppBarElevation ) // 커스텀이 필요하여 수정할 경우라면 @Composable fun XXXTopAppBar( modifier: Modifier = Modifier, title: @Composable XXXAppBar.() -> Unit = {}, navigationIcon: @Composable (() -> Unit)? = null, actions: @Composable RowScope.() -> Unit = {}, backgroundColor: Color = XXXTheme. colors.primaryVariant , contentColor: Color = contentColorFor(backgroundColor), elevation: Dp = 0.dp ) 커스텀이 필요하여 수정한다면 기본 TopAppBar를 그대로 활용하고, 내부적으로 content() 영역을 채워주는 편이 더 좋다.
  23. • Kotlin Extension 활용하여 접근 제한자 사용 TopAppBar - 접근

    제한 Kotlin이니 extension 활용하여 접근 제한도 가능하다.
  24. TopAppBar - 접근 제한 object XXXAppBar @Suppress("unused") @Composable fun XXXAppBar.XXXTitle(

    text: String , ) { XXXText( text = text, style = XXXTheme.typography.appBarTitle, ) } TopAppBar에서만 활용할 수 있는 XXXTitle이라는 appBarTitle을 강제로 지정할 수 있다. Text를 직접 활용할 수도 있겠지만, 그것보다 공통화 시켜 활용할 수 있도록 매핑 할 수 있다.
  25. • Text/TextField - Font에 대한 기본 정의를 위해 매핑 ◦

    style = XXXTheme.typography.extraBoldText.merge(style.mergeTextLineHeight()) ◦ 외부가 가장 우선이 되도록 라이브러리에서 제공하여야 한다면 ▪ Merge 순서 중요! 외부 정보는 오른쪽에 추가 Text/TextField Text와 TextField를 매핑해 활용하는 것은 내부 폰트 적용, 컬러 정보 lineHeight 등을 내부에서 처리하기 위할 수 있다. 단 이런 라이브러리 제공 시에는 Merge 순서가 매우 중요한데 merge 대상이 () 안에 포함되어야 한다.
  26. Text @Composable fun Text( text: AnnotatedString , modifier: Modifier =

    Modifier , color: Color = Color. Unspecified, fontSize: TextUnit = TextUnit. Unspecified, fontStyle: FontStyle? = null, fontWeight: FontWeight? = null, fontFamily: FontFamily? = null, letterSpacing: TextUnit = TextUnit. Unspecified, textDecoration: TextDecoration? = null, textAlign: TextAlign? = null, lineHeight: TextUnit = TextUnit. Unspecified, overflow: TextOverflow = TextOverflow. Clip, softWrap: Boolean = true, maxLines: Int = Int. MAX_VALUE, inlineContent: Map<String , InlineTextContent> = mapOf(), onTextLayout: (TextLayoutResult) -> Unit = {}, style: TextStyle = LocalTextStyle.current ) Text를 노출하는 방식은 AnnotatedString을 활용하는 방법과 단순 String을 활용하는 방법이 있는데, 일반적으론 String 활용이 더 쉽다. 그러니 필요에 따라 매핑
  27. Text @Composable fun Text( text: AnnotatedString , modifier: Modifier =

    Modifier, color: Color = Color.Unspecified, fontSize: TextUnit = TextUnit.Unspecified, fontStyle: FontStyle? = null, fontWeight: FontWeight? = null, fontFamily: FontFamily? = null, letterSpacing: TextUnit = TextUnit.Unspecified, textDecoration: TextDecoration? = null, textAlign: TextAlign? = null, lineHeight: TextUnit = TextUnit.Unspecified, overflow: TextOverflow = TextOverflow.Clip, softWrap: Boolean = true, maxLines: Int = Int.MAX_VALUE, inlineContent: Map<String, InlineTextContent> = mapOf(), onTextLayout: (TextLayoutResult) -> Unit = {}, style: TextStyle = LocalTextStyle.current ) Text 요소에서 절반 이상은 TextStyle에 포함되는 정보인데, 그래서 외부에 어디까지 제공해 주는 게 좋을지도 정해주면 좋다. style만 쓰라고 할 수도 있어 보인다.
  28. val textColor = color. takeOrElse { style.color.takeOrElse { LocalContentColor.current.copy(alpha =

    LocalContentAlpha.current) } } Text - TextColor color에 Color.Unspecified가 있는데, 이 TextColor는 어디에서 결정할까? 추적해 보면 color 정보를 LocalContentColor provider에서 가져오도록 하고 있다.
  29. Text - TextColor val textColor = color. takeOrElse { style.color.takeOrElse

    { LocalContentColor.current.copy(alpha = LocalContentAlpha.current) } } val LocalContentColor = compositionLocalOf { Color.Black } LocalContentColor의 기본 컬러 값은 Black이다. 아무런 설정을 하지 않는다면 Black이 나오는데, 그렇다면 사용할 때마다 LocalContentColor를 지정해야 할까?
  30. Text - TextColor val textColor = color. takeOrElse { style.color.takeOrElse

    { LocalContentColor.current.copy(alpha = LocalContentAlpha.current) } } val LocalContentColor = compositionLocalOf { Color.Black } @Composable fun Surface( // 생략 ) { CompositionLocalProvider( LocalContentColor provides contentColor , ) } 다행히 그렇지는 않은데 대부분의 Compose Material 코드를 따라가 보면 Surface를 만나게 된다. Surface에서 CompositionLocalProvider를 통해 contentColor를 지정해 주고 있음을 알 수 있다. 결국 Surface 매핑 함수들을 활용하면 contentColor 정보는 자동으로 지정됨을 알 수 있다.
  31. • TextField ◦ BasicTextField ▪ Compose CoreTextField라는 내부 구현체를 바라본다.

    ▪ 만약 TextField 형태의 UI가 아니라면 BasicTextField를 활용해 커스텀 ◦ TextField ▪ TextField는 BasicTextField를 매핑하고 있다. ▪ BaiscTextField에 decorationBox를 포함 TextField TextField는 크게 BasicTextField와 TextField로 나눠지는데, TextField는 Material에서 Label과 Border를 가진 가장 기본 Input text를 제공한다. 자동으로 decorationBox를 포함하도록 구현해두었다. 만약 커스텀이 필요하다면 BasicTextField를 활용해 decorationBox를 포함하여 구현하도록 해야 한다.
  32. • TextField는 성능에따라 키보드 타이핑 이슈가 발생 ◦ Delay에 따라

    조합에 문제가 발생하는데, 대부분 아시아 국가의 단어 조합 문제 발생 • 해결방법 - EditText를 아직까지 활용할 수 밖에 없다. ◦ Chrisbanes - Twitter에서 활용하는 EditText 매핑을 참고해 작업 ◦ https://gist.github.com/chrisbanes/8c2f55f55b8dd2c64e436b704d65f266 TextField 문제 TextField는 조합 문제가 발생하는데, 보통 타이밍 이슈이다. 해결 방법은 키보드 제조사 쪽에서 수정 해주는 방법이 있지만 언제까지 기다려야 할지 모르니 다행히도 Chrisbanes의 EditText 매핑 코드를 참고하자.
  33. • TextField 숨겨두고, 텍스트 타이핑하는 경우 ◦ xml에서와 동일하게 size를

    0.dp로 적용하면 간단하게 사용 가능 ◦ textColor, cursor color Transparent로 처리 ◦ Focus 처리가 문제일 수 있는데, 이때는 TextFieldValue의 selection을 맨 끝으로 보내도록 작업 ◦ 단, selection이 맨 마지막이기에 조합언어에서는 불가능 Hide Type 키보드 형태를 띠지만 TextField가 아닌 단순 Text에 타이핑을 보여줘야 할 경우가 있다. 대표적인 예는 6자리 문자 SMS 인증 코드 입력 부일 것 같다. view의 사이즈를 0.dp로 해주고, 포커싱 처리를 해주면 된다. 꼭 포커싱을 TextField로 해줘야 문제가 없고, TextFieldValue에 selection을 직접 컨트롤해 줘야 한다.
  34. • Button 종류 ◦ Button - 일반적인 버튼 ◦ OutlinedButton

    - 일반적인 버튼에 Outlined theme 포함된 Button ◦ TextButton - Button에 Theme 없이 Text 버튼만 가짐 • 모든 버튼의 기본 padding ◦ top/bottom 8.dp ◦ start/end 16.dp • Button UI에 따라 매핑 Button Button은 매우 많이 매핑할 것 같다. 버튼의 종류도 백그라운드 컬러를 가진 Button, outlined만 가진 Button, 마지막으로 Text만을 가진 TextButton을 제공한다. 이 버튼은 모두 padding을 가지고 있는데, 다행히 이 코드의 모든 끝은 Button() 함수이다. paddingValues를 지정해 padding을 변경할 수 있다. minHeight(36.dp), minWidth(64.dp)도 수정이 필요하다면 커스텀을 함께 해야 한다. 내부적으로 이 정보를 강제화하고 있다.
  35. Button @OptIn(ExperimentalMaterialApi::class) @Composable fun Button( onClick: () -> Unit, modifier:

    Modifier = Modifier, enabled: Boolean = true, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, elevation: ButtonElevation? = ButtonDefaults.elevation(), shape: Shape = MaterialTheme.shapes.small, border: BorderStroke? = null, colors: ButtonColors = ButtonDefaults.buttonColors(), contentPadding: PaddingValues = ButtonDefaults.ContentPadding, content: @Composable RowScope.() -> Unit ) @Composable @NonRestartableComposable fun OutlinedButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, elevation: ButtonElevation? = null, shape: Shape = MaterialTheme.shapes.small, border: BorderStroke? = ButtonDefaults.outlinedBorder, colors: ButtonColors = ButtonDefaults.outlinedButtonColors(), contentPadding: PaddingValues = ButtonDefaults.ContentPadding, content: @Composable RowScope.() -> Unit )
  36. Button @OptIn(ExperimentalMaterialApi ::class) @Composable fun Button( onClick: () -> Unit,

    modifier: Modifier = Modifier, enabled: Boolean = true, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, elevation: ButtonElevation? = ButtonDefaults.elevation() , shape: Shape = MaterialTheme.shapes.small, border: BorderStroke? = null, colors: ButtonColors = ButtonDefaults.buttonColors() , contentPadding: PaddingValues = ButtonDefaults.ContentPadding, content: @Composable RowScope.() -> Unit ) @Composable @NonRestartableComposable fun OutlinedButton ( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, elevation: ButtonElevation? = null, shape: Shape = MaterialTheme.shapes.small, border: BorderStroke? = ButtonDefaults. outlinedBorder , colors: ButtonColors = ButtonDefaults.outlinedButtonColors() , contentPadding: PaddingValues = ButtonDefaults.ContentPadding, content: @Composable RowScope.() -> Unit ) 두 버튼의 차이점은 색상 정보에 있을 뿐 크게 다르지 않다. 내부적으론 두 버튼 모두 Button을 바라보고 있다.
  37. Button - Colors @Composable fun buttonColors( backgroundColor: Color = MaterialTheme.

    colors.primary, contentColor: Color = contentColorFor(backgroundColor) , disabledBackgroundColor: Color = MaterialTheme. colors.onSurface.copy(alpha = 0.12f) .compositeOver(MaterialTheme. colors.surface), disabledContentColor: Color = MaterialTheme. colors.onSurface .copy(alpha = ContentAlpha.disabled) ) @Composable fun outlinedButtonColors ( backgroundColor: Color = MaterialTheme. colors.surface, contentColor: Color = MaterialTheme. colors.primary, disabledContentColor: Color = MaterialTheme. colors.onSurface .copy(alpha = ContentAlpha.disabled) ) 버튼에 대한 colors는 ButtonColors에서 처리하고 있는데 일반 버튼과 outline 버튼의 색상 정보는 위와 같다. 만약 이를 커스텀 하겠다고 하면, Buton을 매핑하고, ButtonColors처럼 클래스를 직접 만들어 활용하거나, ButtonColors에 지정하는 defaultButtonColors를 만들어 사용할 수 있다.
  38. Button - Colors @Immutable data class XXXButtonColors( private val backgroundColor

    : Color, private val contentColor: Color, private val disabledBackgroundColor : Color, private val disabledContentColor : Color, ) @Composable fun buttonColors( backgroundColor: Color = MaterialTheme. colors.primary, contentColor: Color = contentColorFor(backgroundColor) , disabledBackgroundColor: Color = XXXColor. disabledColor, disabledContentColor: Color = XXXColor.White ) @Composable fun outlinedButtonColors ( backgroundColor: Color = Color. Transparent, contentColor: Color = contentColorFor(backgroundColor) , disabledContentColor: Color = XXXColor.White ) 직접 구현하면 이와 같고, @Immutable로 값의 변경이 없음을 알려주어야 한다.
  39. • Indicator 종류 ◦ CircularProgressIndicator - 원형 Indicator ◦ LinearProgressIndicator

    - 한줄 짜리 Indicator • 커스텀은? ◦ 단순 색상을 바꾸는 정도로 지원 ◦ Indicator의 끝에 라운드 필요 하다면 Canvas 활용해 직접 구현 필요 Indicator Indicator도 제공한다. 색상 정보를 바꾸는 정도의 커스텀이 가능한데, 만약 Round 형태의 indicator가 필요하다면 직접 커스텀 해줘야 한다.
  40. Indicator @Composable fun XXXLinearProgressIndicator ( /*@FloatRange(from = 0.0, to =

    1.0)*/ progress: Float , modifier: Modifier = Modifier , color: Color = MaterialTheme. colors.primary, backgroundColor: Color = color.copy( alpha = ProgressIndicatorDefaults. IndicatorBackgroundOpacity ), indicatorWidth: Dp = LinearIndicatorWidth, indicatorHeight: Dp = LinearIndicatorHeight, ) { Canvas( modifier .progressSemantics(progress) .size(indicatorWidth , indicatorHeight) .clip(RoundedCornerShape(10.dp)) ) { val strokeWidth = size.height drawLinearIndicatorBackground(backgroundColor , strokeWidth) drawLinearIndicator(0f, progress, color, strokeWidth) } } Canvas를 활용해 직접 처리할 수 있고, Round는 modifer에 지정해 주면 되겠다. 이 역시 커스텀 상황에 따라 다를 수 있으니 참고만
  41. Indicator private fun DrawScope.drawLinearIndicator( startFraction: Float, endFraction: Float, color: Color,

    strokeWidth: Float, ) { val width = size.width val height = size.height // Start drawing from the vertical center of the stroke val yOffset = height / 2 val isLtr = layoutDirection == LayoutDirection.Ltr val barStart = (if (isLtr) startFraction else 1f - endFraction) * width val barEnd = (if (isLtr) endFraction else 1f - startFraction) * width // Progress line drawLine(color, Offset(barStart, yOffset), Offset(barEnd, yOffset), strokeWidth) } private fun DrawScope.drawLinearIndicatorBackground( color: Color, strokeWidth: Float, ) = drawLinearIndicator(0f, 1f, color, strokeWidth) 나머지 코드는 이와 같다.
  42. • Accompanist 라이브러리 PlaceHolder 활용 • 커스텀은? ◦ Modifier를 활용한

    확장 함수 형태로 제작 PlaceHolder https://google.github.io/accompanist/placeholder/ PlaceHolder는 Accompanist 라이브러리의 PlaceHolder를 활용할 수 있다. Modifier를 확장하여 원하는 형태를 만들어두고, 사용하는 게 가장 편한다.
  43. Indicator @Composable internal fun Modifier.xxxPlaceHolder( showPlaceHolder: Boolean, shape: Shape =

    RoundedCornerShape(4.dp), ): Modifier = placeholder( visible = showPlaceHolder, color = XXXTheme.colors.surfacePlaceHolder.copy(alpha = 0.4f), // optional, defaults to RectangleShape shape = shape, highlight = PlaceholderHighlight.shimmer( highlightColor = XXXTheme.colors.surfacePlaceHolder ) ) // 필요한 위치에 Box( modifier = Modifier .padding(start = 20.dp, top = 20.dp) .fillMaxWidth(0.3f) .height(10.dp) .xxxPlaceHolder( showPlaceHolder = showPlaceHolder, ) ) 그래서 이와 같이 xxxPlaceHolder를 만들어두고 최소한의 커스텀을 제공하고 활용할 수 있도록 적용해두었다. 상황에 따라 modifier에 붙여주기만 하면 적용이 가능하다.
  44. Etc

  45. • LazyColumn 사용 시 ◦ 페이징 처리 ▪ Pager 라이브러리

    사용 ◦ 성능 최적화를 위해 고유한 key 값을 적용해야 한다. ◦ key는 고유해야 하며, 중복 시 즉시 오류 발생 LazyColumn LazColumn 사용 시 key 값을 잘 활용해야 최적화에 도움 되는데, 이 키는 고유해야 한다. 고유하지 않다면 즉시 오류가 발생하니 주의해 사용!
  46. • Preview 버전의 안드로이드 스튜디오에서는 현재 수정 사항 및 Recompose

    count 측정이 가능 • LiveEdit는 M1 이상을 추천 • Recompose count 역시 rootView를 찾지 못하는 케이스(타고 들어가는 뷰가 보통 찾지 못함)에서는 활용 치 못함 ◦ 현재 Activity를 바로 실행하고, 사용하는 걸 추천하는데, LiveEdit, Recompose Count 모두 동일 LiveEdit, Recompose Count LiveEdit, Recompose count는 아직 잘 동작하는 편은 아니다. 현재 작업 중인 Activity를 바로 띄어 테스트하는 게 가장 좋은 방법이다.
  47. • Debug와 Release Performance 차이가 발생 ◦ 출시에는 R8 사용

    • Compose는 Android 플랫폼의 일부가 아닌 라이브러리로 배포 ◦ Just-in-time 방식으로 실시간 해석되어야 하므로 앱의 속도가 느려질 수 있다. ◦ Baseline Profiles을 활용하도록 적용 ◦ Macrobenchmark Sample을 통해 성능 확인 Debug/Release Performance Compose performance - Jetpack Why should you always test Compose performance in release? | by Ben Trengrove | Android Developers | Jun, 2022 | Medium Debug/Release 성능 차가 발생하는데, 출시에서는 R8을 적용해야 한다. 그리고 플랫폼 일부가 아닌 라이브러리 형태라 Just-in-time 방식에서 실시간 해석되어야 하므로 앱의 속도가 느려질 수 있다. 이를 보완하기 위해 Baseline Profiles를 잘 활용하고, Macrobenchmark를 활용하는 것도 방법이다.