$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. Compose
    Component Mapping
    레몬트리 - Taehwan

    View Slide

  2. 시작하기 전에
    ● Material 2 버전 기반의 Compose Component 구성
    ● Scaffold 기반 UI 작업이 가능하도록 만든 Component 구성
    Compose Component Mapping 이야기를 하기 전에 Material 2 버전 기반의 Compose Componet
    구성에 대한 이야기이며, Scaffold 기반의 UI 작업을 하기 위한 Component 구성에 대한 이야기

    View Slide

  3. 현재 사용하는 버전 정보
    ● Android Compose 1.2.0
    ● Material 2
    ● Kotlin 1.6.21 - Compose 강제 디펜던시
    ● Accompanist
    Compose 1.2.0 기반, Material 2버전 활용하고, Kotlin 버전은 Compose 강제 디펜던시로 1.6.21
    활용

    View Slide

  4. 공통 Component
    scaffold

    View Slide

  5. ● Activity/Fragment에서 가장 기본 틀로 활용
    ○ Background color - MaterialTheme.colors.background,
    ○ Content color - contentColorFor(backgroundColor)
    Scaffold
    Scaffold는 Compose 뷰를 올리는 가장 기본적인 틀로 활용 기본 색상은 MaterialTheme의
    background, onBackground 컬러 기준으로 기본 적용

    View Slide

  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에는 topBar - AppBar, bottomBar - Navigation bar, floating, drawer 메뉴 활용이 가능

    View Slide

  7. ● TopAppBar를 활용하거나, 직접 Content를 채워넣을 수 있다.
    Scaffold - topBar
    TopBar는 TopAppBar 기준으로 적용이 가능하지만, 직접 Content를 채워 넣는 것도 가능

    View Slide

  8. ● Bottom Navigation과 함께 사용
    Scaffold - bottomBar
    BottomBar는 BottomNavigation을 사용할 수 있고, BottomAppBar 등 활용이 가능

    View Slide

  9. 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에서 흰색 부분에 대한 매핑 적용하여 사용하고
    있다.

    View Slide

  10. 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를 구현한데 필요한 데이터를 포함하고 있다.

    View Slide

  11. ● 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는 최소 높이를 강제한다.

    View Slide

  12. 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
    )
    커스텀을 한다면 필요한 부분만 매핑해 활용한다.

    View Slide

  13. ● 우리만의 틀을 만들기 위함
    ● Scaffold 터치 이벤트 발생 시 키보드 및 Focus 날림 처리
    ● 컬러 셋 지정을 편하게 할 수 있음
    Scaffold 매핑 이유
    결국 Scaffold 매핑 한 이유는 접근 제한을 한다거나, 기본 색상 정보를 Material을 따르지 않고
    싶다면 매핑을 해주는 편이 더 좋다. 포커스 처리를 공통화한다거나, 나만의 UI를 추가한다거나 등이
    이유일 수 있다.

    View Slide

  14. 공통 Component
    Scaffold - Keyboard

    View Slide

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

    View Slide

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

    View Slide

  17. 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을 활용하여 일부 높이 이상 가려졌는지 체크한다.

    View Slide

  18. 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 {
    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를 활용한다.

    View Slide

  19. 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 상태를 활용한다.

    View Slide

  20. 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 할 수 있도록
    처리한다.

    View Slide

  21. Scaffold - focus clear 성능은?
    이에 대한 처리는 기존 방식의 focus 처리 clear와 차이가 없다.

    View Slide

  22. 공통 Component
    Scaffold - padding

    View Slide

  23. ● 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 적용

    View Slide

  24. 공통 Component
    TopAppBar

    View Slide

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

    View Slide

  26. 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와

    View Slide

  27. 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에 대한 틀을 제공하는 컴포넌트이다.

    View Slide

  28. 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
    )

    View Slide

  29. 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() 영역을
    채워주는 편이 더 좋다.

    View Slide

  30. ● Kotlin Extension 활용하여 접근 제한자 사용
    TopAppBar - 접근 제한
    Kotlin이니 extension 활용하여 접근 제한도 가능하다.

    View Slide

  31. TopAppBar - 접근 제한
    object XXXAppBar
    XXXAppBar라는 이름으로 object를 하나 만들고

    View Slide

  32. TopAppBar - 접근 제한
    object XXXAppBar
    @Suppress("unused")
    @Composable
    fun XXXAppBar.XXXTitle(
    text: String
    ,
    ) {
    XXXText(
    text = text,
    style = XXXTheme.typography.appBarTitle,
    )
    }
    TopAppBar에서만 활용할 수 있는 XXXTitle이라는 appBarTitle을 강제로 지정할 수 있다. Text를
    직접 활용할 수도 있겠지만, 그것보다 공통화 시켜 활용할 수 있도록 매핑 할 수 있다.

    View Slide

  33. 공통 Component
    Text/TextField

    View Slide

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

    View Slide

  35. 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, InlineTextContent> = mapOf(),
    onTextLayout: (TextLayoutResult) -> Unit = {},
    style: TextStyle = LocalTextStyle.current
    )
    Text를 노출하는 방식은 AnnotatedString을 활용하는 방법과 단순 String을 활용하는 방법이
    있는데, 일반적으론 String 활용이 더 쉽다. 그러니 필요에 따라 매핑

    View Slide

  36. 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 = mapOf(),
    onTextLayout: (TextLayoutResult) -> Unit = {},
    style: TextStyle = LocalTextStyle.current
    )
    Text 요소에서 절반 이상은 TextStyle에 포함되는 정보인데, 그래서 외부에 어디까지 제공해 주는 게
    좋을지도 정해주면 좋다. style만 쓰라고 할 수도 있어 보인다.

    View Slide

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

    View Slide

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

    View Slide

  39. 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 정보는 자동으로 지정됨을 알 수 있다.

    View Slide

  40. ● 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를 포함하여 구현하도록 해야 한다.

    View Slide

  41. ● TextField는 성능에따라 키보드 타이핑 이슈가 발생
    ○ Delay에 따라 조합에 문제가 발생하는데, 대부분 아시아 국가의 단어 조합 문제 발생
    ● 해결방법 - EditText를 아직까지 활용할 수 밖에 없다.
    ○ Chrisbanes - Twitter에서 활용하는 EditText 매핑을 참고해 작업
    ○ https://gist.github.com/chrisbanes/8c2f55f55b8dd2c64e436b704d65f266
    TextField 문제
    TextField는 조합 문제가 발생하는데, 보통 타이밍 이슈이다. 해결 방법은 키보드 제조사 쪽에서
    수정 해주는 방법이 있지만 언제까지 기다려야 할지 모르니 다행히도 Chrisbanes의 EditText 매핑
    코드를 참고하자.

    View Slide

  42. 공통 Component
    Hide Type

    View Slide

  43. ● 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을 직접
    컨트롤해 줘야 한다.

    View Slide

  44. 공통 Component
    Button

    View Slide

  45. ● 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)도 수정이 필요하다면 커스텀을 함께 해야 한다. 내부적으로
    이 정보를 강제화하고 있다.

    View Slide

  46. 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
    )

    View Slide

  47. 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을
    바라보고 있다.

    View Slide

  48. 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를 만들어 사용할 수 있다.

    View Slide

  49. 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로 값의 변경이 없음을 알려주어야 한다.

    View Slide

  50. 공통 Component
    Indicator

    View Slide

  51. ● Indicator 종류
    ○ CircularProgressIndicator - 원형 Indicator
    ○ LinearProgressIndicator - 한줄 짜리 Indicator
    ● 커스텀은?
    ○ 단순 색상을 바꾸는 정도로 지원
    ○ Indicator의 끝에 라운드 필요 하다면 Canvas 활용해 직접 구현 필요
    Indicator
    Indicator도 제공한다. 색상 정보를 바꾸는 정도의 커스텀이 가능한데, 만약 Round 형태의
    indicator가 필요하다면 직접 커스텀 해줘야 한다.

    View Slide

  52. 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에 지정해 주면 되겠다. 이 역시 커스텀
    상황에 따라 다를 수 있으니 참고만

    View Slide

  53. 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)
    나머지 코드는 이와 같다.

    View Slide

  54. 공통 Component
    PlaceHolder

    View Slide

  55. ● Accompanist 라이브러리 PlaceHolder 활용
    ● 커스텀은?
    ○ Modifier를 활용한 확장 함수 형태로 제작
    PlaceHolder
    https://google.github.io/accompanist/placeholder/
    PlaceHolder는 Accompanist 라이브러리의 PlaceHolder를 활용할 수 있다.
    Modifier를 확장하여 원하는 형태를 만들어두고, 사용하는 게 가장 편한다.

    View Slide

  56. 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에 붙여주기만 하면 적용이 가능하다.

    View Slide

  57. Etc

    View Slide

  58. ● LazyColumn 사용 시
    ○ 페이징 처리
    ■ Pager 라이브러리 사용
    ○ 성능 최적화를 위해 고유한 key 값을 적용해야 한다.
    ○ key는 고유해야 하며, 중복 시 즉시 오류 발생
    LazyColumn
    LazColumn 사용 시 key 값을 잘 활용해야 최적화에 도움 되는데, 이 키는 고유해야 한다. 고유하지
    않다면 즉시 오류가 발생하니 주의해 사용!

    View Slide

  59. ● Preview 버전의 안드로이드 스튜디오에서는 현재 수정 사항 및 Recompose
    count 측정이 가능
    ● LiveEdit는 M1 이상을 추천
    ● Recompose count 역시 rootView를 찾지 못하는 케이스(타고 들어가는 뷰가
    보통 찾지 못함)에서는 활용 치 못함
    ○ 현재 Activity를 바로 실행하고, 사용하는 걸 추천하는데, LiveEdit, Recompose Count 모두 동일
    LiveEdit, Recompose Count
    LiveEdit, Recompose count는 아직 잘 동작하는 편은 아니다. 현재 작업 중인 Activity를 바로 띄어
    테스트하는 게 가장 좋은 방법이다.

    View Slide

  60. ● 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를 활용하는 것도 방법이다.

    View Slide

  61. End.

    View Slide