개발/Compose

Jetpack Compose 테마 정리본

귀염둥이 팡무 2023. 9. 23. 16:58

GDG Songdo/Incheon에서 지난 4월부터 6월까지 2개월간 진행됐던 Compose 스터디에서 발표한 내용을 올려봅니다.

본 내용은 Codelab - Jetpack Compose theming 기반으로 작성되었습니다.


Jetpack Compose 테마 설정

학습 내용

  • Material Design 기본 지침서 및 브랜드에 맞게 맞춤설정하는 방법
  • Compose에서 Material Design 시스템을 구현하는 방법
  • 앱 전체에서 색상과 서체, 도형을 정의하고 사용하는 방법
  • 구성요소의 스타일을 지정하는 방법
  • 밝은 테마와 어두운 테마를 지원하는 방법

빌드 항목

잠깐! Material과 Material3는 요구되는 파라미터와 타입이 다른 등 일부 호환되지 않습니다.
  • Color
    • Material
      • lightColors
      • darkColors
    • Material3
      • lightColorScheme
      • darkColorScheme
  • Typography
    • Material
      • h1, h2, subtitle1, body …
    • Material3
      • bodyLarge, titleLarge, labelSmall …
  • And then more…

Material Theming

색상

Material Design은 앱 전체에서 사용할 수 있는, 의미론적으로 이름이 지정된 여러 색상을 정의

  • 기본 색상
    • 주요 브랜드 색상
  • 보조 색상
    • 강조 표시
  • 대비되는 영역에 더 어둡거나 밝은 변형을 제공
  • 배경, 표면 색상
    • 애플리케이션의 '표면'에 개념적으로 존재하는 구성요소를 보유한 컨테이너에 사용
  • on 색상
    • 이름이 지정된 색상 중 하나 위에 있는 콘텐츠에 사용
      • ex) ‘Surface’ 색상 컨테이너 위의 텍스트는 ‘On Surface’ 색상 사용
  • 예시 (기본적으로)
  • 이름이 지정된 색상을 정의
    • 밝은 테마 및 어두운 테마 둘 다와 같은 대체 색상 팔레트를 제공
  • Material 색상 도구
    • 쉽게 색상을 선택하여 색상 팔레트를 만들 수 있음

서체

지정된 여러 서체 스타일을 정의

  • 테마별로 서체 스타일을 변경은 불가능
  • 그렇지만 서체 스케일 사용 시 일관성 높아짐
  • 예시
    • 앱 바
      • 기본적으로 h6 스타일 사용
    • 버튼
      • 기본적으로 button 스타일 사용
  • 서체 스케일 생성기 도구

도형

도형을 체계적으로 사용하여 브랜드를 전달할 수 있도록 지원

기준

  • Material은 기본적으로 '기준' 테마로 설정
  • 색상, 폰트, 도형 등 테마 지정하지 않거나 맞춤설정하지 않으면 기준 테마 사용됨

테마 정의

테마 설정은 MaterialTheme 컴포저블을 통해 구현

Material Theme

@Composable
fun MaterialTheme(
    colors: Colors,
    typography: Typography,
    shapes: Shapes,
    content: @Composable () -> Unit
) {
    ...
}

테마 만들기

  • MaterialTheme를 Wrapping한 자체 테마를 만드는 것이 좋음
  • 관리를 한 곳에서 할 수 있으며, Preview 등 여러 곳에서 재사용 가능
@Composable
fun PangMooTheme(content: @Composable () -> Unit) {
  MaterialTheme(content = content)
}

색상

  • Compose의 색상은 Color 클래스를 사용하여 정의
  • 색상은 ULong을 사용하며, 간단하게 ARGB로 대입하여 사용
  • 색상 정의 시 ‘의미론적’이 아닌 ‘문자 그대로’ 이름 정의
    • 각 테마에 따라 ‘의미론적’ 색상이 변경될 수 있음
    • 예를 들어 primary를 0xFFFFFFFF(검정색)으로 지정하였지만 다크 테마로 변경하게 되면?
  1. Color.kt 내 색상 정의
val Red700 = Color(0xffdd0d3c)
val Red800 = Color(0xffd00036)
val Red900 = Color(0xffc20029)
  1. Theme.kt에서 정의한 색상들 사용하여 팔레트 생성
    • 브랜드에 별도의 기본 색상(Primary)과 보조 색상(Secondary)이 없다면 모두 동일 색상 사용 가능
    private val LightColors = lightColors(
        primary = Red700,
        primaryVariant = Red900,
        onPrimary = Color.White,
        secondary = Red700,
        secondaryVariant = Red900,
        onSecondary = Color.White,
        error = Red800
    )

서체

  1. Typography.kt 내 font 정의
private val Montserrat = FontFamily(
    Font(R.font.montserrat_regular),
    Font(R.font.montserrat_medium, FontWeight.W500),
    Font(R.font.montserrat_semibold, FontWeight.W600)
)

private val Domine = FontFamily(
    Font(R.font.domine_regular),
    Font(R.font.domine_bold, FontWeight.Bold)
)
  1. 폰트를 만들면 원하는 스타일에 맞춰 Typography 생성
val PangMooTypography = Typography(
    h4 = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W600,
        fontSize = 30.sp
    ),
    h5 = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W600,
        fontSize = 24.sp
    ),
    h6 = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W600,
        fontSize = 20.sp
    ),
    subtitle1 = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W600,
        fontSize = 16.sp
    ),
    subtitle2 = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W500,
        fontSize = 14.sp
    ),
    body1 = TextStyle(
        fontFamily = Domine,
        fontWeight = FontWeight.Normal,
        fontSize = 16.sp
    ),
    body2 = TextStyle(
        fontFamily = Montserrat,
        fontSize = 14.sp
    ),
    button = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W500,
        fontSize = 14.sp
    ),
    caption = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.Normal,
        fontSize = 12.sp
    ),
    overline = TextStyle(
        fontFamily = Montserrat,
        fontWeight = FontWeight.W500,
        fontSize = 12.sp
    )
)
  1. Theme 적용
@Composable
fun PangMooTheme(content: @Composable () -> Unit) {
  MaterialTheme(
    colors = LightColors,
        typography = PangMooTypography,
    content = content
  )
}

도형

  1. Shape.kt 내 도형 정의
val PangMooShapes = Shapes(
    small = CutCornerShape(topStart = 8.dp),
    medium = CutCornerShape(topStart = 24.dp),
    large = RoundedCornerShape(8.dp)
)
  1. Theme.kt 내 도형 적용
@Composable
fun PangMooTheme(content: @Composable () -> Unit) {
  MaterialTheme(
    colors = LightColors,
    typography = PangMooTypography,
        shapes = PangMooShapes,
    content = content
  )
}

어두운 테마

안드로이드 10부터는 전역 테마 전환을 지원합니다. (디자인 가이드를 보고 싶다면 디자인 안내 에서 확인 가능)

  1. Color.kt에 아래의 색상 추가
val Red200 = Color(0xfff297a2)
val Red300 = Color(0xffea6d7e)
  1. Theme.kt에 색상 추가
private val DarkColors = darkColors(
    primary = Red300,
    primaryVariant = Red700,
    onPrimary = Color.Black,
    secondary = Red300,
    onSecondary = Color.Black,
    error = Red200
)
  1. 테마 적용
@Composable
fun PangMooTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
  content: @Composable () -> Unit
) {
  MaterialTheme(
        colors = if (darkTheme) DarkColors else LightColors,
    typography = PangMooTypography,
    shapes = PangMooShapes,
    content = content
  )
}

결과물

@Preview(showBackground = true, group = "Buttons")
@Composable
fun SignInButton() {
    PangMooTheme {
        Button(
            onClick = { /*TODO*/ },
        ) {
            Text(text = "Sign In")
        }
    }
}

@Preview(showBackground = true, group = "Buttons")
@Composable
fun SignInButtonWithoutTheme() {
    Button(
        onClick = { /*TODO*/ },
    ) {
        Text(text = "Sign In")
    }
}

색상 사용

MaterialTheme.colors 를 사용하여 지정한 테마 내 다른 색상 사용 가능

적용한 surface 색상이 White 이기에 Preview Background를 LTGRAY로 바꿔 보이도록 하였습니다.

@Preview(showBackground = true, group = "Buttons", backgroundColor = Color.LTGRAY)
@Composable
fun SignInButtonOtherThemeColor() {
    PangMooTheme {
        Button(
            onClick = { /*TODO*/ },
            colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colors.surface)
        ) {
            Text(text = "Sign In")
        }
    }
}

원색

지정된 테마 색상이 아닌 직접 하드코딩으로 색상 지정 가능

다만 이 경우 다양한 테마 지원이 어려워지거나 불가능해질 수 있기에 중요 브랜드 색상과 같이 밝은/어두운 테마에서도 같은 색상을 사용해야 하는 등 제한된 경우에만 사용하는 것을 권장

@Preview(showBackground = true, group = "Buttons")
@Composable
fun SignInButtonCustomColor() {
    PangMooTheme {
        Button(
            onClick = { /*TODO*/ },
            colors = ButtonDefaults.buttonColors(backgroundColor = Color(0xFF25AAFF))
        ) {
            Text(text = "Sign In")
        }
    }
}

색상 파생

위처럼 하드 코딩하는 것 보다 copy를 통해 사본을 만들어 사용하면 다양한 테마에서 유연하게 사용 가능

 

@Preview(showBackground = true, group = "Buttons")
@Composable
fun SignInButtonCopy() {
    PangMooTheme {
        val derivedColor = MaterialTheme.colors.onPrimary.copy(alpha = 0.3f)

        Button(
            onClick = { /*TODO*/ },
        ) {
            Text(text = "Sign In", color = derivedColor)
        }
    }
}

표현 및 컨텐츠 색상

많은 컴포저블은 한 쌍의 색상 및 ‘콘텐츠 색상’을 허용

main color를 지정하면 자동으로 sub color(content)를 지정해 준다는 의미 같음
Surface(
  color: Color = MaterialTheme.colors.surface,
  contentColor: Color = contentColorFor(color),
  ...

TopAppBar(
  backgroundColor: Color = MaterialTheme.colors.primarySurface,
  contentColor: Color = contentColorFor(backgroundColor),
  ...

이를 통해 contentColorFor 메서드로 테마 색상에서 적절한 on 색상을 가질 수 있음

예를 들어 primary 배경을 지정하면 onPrimary 가 컨텐츠 색상으로 사용됨

@Preview(showBackground = true, group = "ContentColor")
@Composable
fun ContentColor() {
    PangMooTheme {
        Column {
            Surface {
                                // default color 'onSurface'
                Row {
                    Text(text = "Primary")
                }
            }

            Surface(color = MaterialTheme.colors.error) {
                                // default color 'onError'
                Row {
                    Icon(imageVector = Icons.Default.Warning, contentDescription = "Error")
                    Text(text = "ERORR")
                }
            }
        }
    }
}

LocalContentColorCompositionLocal을 사용하여 현재 배경과 대비되는 색상을 가져올 수 있음

BottomNavigationItem(
  unselectedContentColor = LocalContentColor.current ...

적절한 컨텐츠 색상을 CompositionLocal을 통해 값을 설정할 수 있기 때문에 요소 색상 설정 시 Surface를 사용하는 것이 좋음

Modifier.background를 직접 호출하는 것은 적절한 컨텐츠 색상을 설정하지 않기에 주의해야 함

@Preview(showBackground = true, group = "ContentColor")
@Composable
fun ContentColorWithSurface() {
    PangMooTheme(darkTheme = false) {
        DefaultBody {
            Surface(
                color = MaterialTheme.colors.onSurface.copy(alpha = 0.1f),
                contentColor = MaterialTheme.colors.primary
            ) {
                Text(
                    text = "PangMoo",
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(horizontal = 16.dp, vertical = 8.dp)
                )
            }
        }
    }
}

@Preview(showBackground = true, group = "ContentColor")
@Composable
fun ContentColorWithoutSurface() {
    PangMooTheme(darkTheme = false) {
        DefaultBody {
            Row(modifier = Modifier.background(MaterialTheme.colors.primary)) {
                Text(
                    text = "PangMoo",
                    modifier = Modifier
                        .fillMaxWidth()
                        .background(Color.LightGray)
                        .padding(horizontal = 16.dp, vertical = 8.dp)
                )
            }
        }
    }
}

@Composable
fun DefaultBody(content: @Composable ColumnScope.() -> Unit) {
    Surface {
        Column {
            Spacer(modifier = Modifier.height(16.dp))
            content()
        }
    }
}
  • 코드 요약
    • ContentColorWithSurface
Surface(
    color = MaterialTheme.colors.onSurface.copy(alpha = 0.1f),
    contentColor = MaterialTheme.colors.primary
) {
    Text(
        text = "PangMoo",
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = 16.dp, vertical = 8.dp)
    )
}
    • ContentColorWithoutSurface
Row(modifier = Modifier.background(MaterialTheme.colors.primary)) {
    Text(
        text = "PangMoo",
        modifier = Modifier
            .fillMaxWidth()
            .background(Color.LightGray)
            .padding(horizontal = 16.dp, vertical = 8.dp)
    )
}

 

텍스트 색상을 제외하면 위와 아래의 컴포저블은 크게 달라 보이는 것이 없지만 만약 어두운 테마로 변경하면 어떻게 될까?

밝은 테마에서는 문제가 없지만 어두운 테마에서는 배경이 고대비를 이루며 특히 텍스트의 경우 배경과 대비되지 않는 컨텐츠 색상을 상속받을 위험 존재

 

그렇기에 ContentColorWithSurface처럼 직접 background를 지정 등 하드 코딩을 하는 행위보다 colorcontentColor를 활용하여 유연하게 처리하는 것이 좋음

컨텐츠 알파

중요도를 전달하고 시각적 계층 구조 제공하기 위해 컨텐츠를 강조 혹은 덜 강조하는 경우가 많은데 Material Design에서는 다양한 수준의 불투명도를 사용하여 다양한 중요도 수준을 전달하는 것을 권장

  • Jetpack Compose에서는 LocalContentAlpha를 사용해 이를 구현
  • CompositionLocal 값을 제공하여 계층 구조의 콘텐츠 알파를 지정
  • 이때 Material에서는 ContentAlpha 객체에 의해 모델링 된 일부 표준 알파 값
    (high, medium, disabled)을 지정하여 기본값을 ContentAlpha.high로 설정되어 있음
    • 예를 들어 TextIcon은 기본적으로 LocalContentAlpha를 사용하도록 조정된 LocalContentColor 조합을 사용

@Preview(showBackground = true, group = "ContentAlpha")
@Composable
fun ContentAlphaLight() {
    PangMooTheme {
        ContentAlpha()
    }
}

@Preview(showBackground = true, group = "ContentAlpha")
@Composable
fun ContentAlphaDark() {
    PangMooTheme(darkTheme = true) {
        ContentAlpha()
    }
}

@Composable
fun ContentAlpha() {
    Surface {
        Column {
            CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
                Text(text = "PangMoo")
            }
            CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) {
                Row {
                    Text(text = "2023.04.23")
                }
            }
        }
    }
}

어두운 테마

Compose에서 어두운 테마를 적용 시 다양한 색상 세트와 구현한 테마를 조합하여 사용하면 됨

밝은 테마에서 실행 중인 여부를 확인하고 싶다면 아래의 코드를 통해 간단하게 가져올 수 있음
val isLightTheme = MaterialTheme.colors.isLight

 

어두운 테마에서는 고도가 높은 표면이 고도 오버레이를 수신(배경이 밝아짐)하는데 어두운 색상 팔레트를 사용하면 자동으로 구현됨

색상을 직접 따봤을 때, 밝은 테마에서는 색상 변화가 전혀 없었음

@Preview(showBackground = true, group = "DarkTheme")
@Composable
fun DarkTheme() {
    PangMooTheme(darkTheme = true) {
        Scaffold(topBar = {
            TopAppBar(backgroundColor = MaterialTheme.colors.primarySurface) {
                Text(
                    text = "Notices",
                    fontSize = 20.sp
                )
            }
        }) {
            Column {
                Surface(elevation = 0.dp) {
                    Column {
                        Text(text = "PangMoo")
                        Text(text = "2023.04.23")
                    }
                }
                Surface(elevation = 32.dp) {
                    Column {
                        Text(text = "PangMoo")
                        Text(text = "2023.04.23")
                    }
                }
            }
            it
        }
    }
}

TopAppBar에는 기본적으로 elevation에 AppBarDefaults.TopAppBarElevation 프로퍼티가 적용되어 있고 이는 4.dp 이다. 그럼 만약 자식 노드의 elevation도 똑같이 적용을 하면 어떻게 될까?

컬러를 따보았을 때 TopAppBar와 자식 노드 모두 #282828 색상으로 동일하게 사용되고 있다.

외적으로 Material Design은 어두운 테마에서 넓은 밝은 색상 영역을 피하는 것을 권장하고 있기에 일반적으로 사용하는 패턴은 컨테이너를 밝은 테마에서는 primary 를 지정하고 어두운 테마에서는 surface 를 지정하는 것이다.

 

앱 바하단 탐색 메뉴 와 같은 많은 구성요소에서 기본적으로 이 전략을 사용 중인데, 이를 직접 구현하면 다음과 같이 작성해야 한다.

@Preview(showBackground = true, group = "DarkTheme")
@Composable
fun Primary() {
    PangMooTheme(darkTheme = false) {
        Card(
            shape = MaterialTheme.shapes.large,
            backgroundColor = if (isSystemInDarkTheme()) MaterialTheme.colors.surface else MaterialTheme.colors.primary
        ) {
            Column(modifier = Modifier.padding(16.dp)) {
                Text(text = "PangMoo")
                Text(text = "2023.04.23")
            }
        }
    }
}

그렇지만 이는 번거로운 행위고 이미 Compose에서 제공을 해주고 있다. 이미 TopAppBar 컴포저블에는 기본 backgroundColor로 사용 중인데, primarySurface를 사용하면 색상을 자동으로 지정해 준다.

val Colors.primarySurface: Color get() = if (isLight) primary else surface

결과를 비교하면 다음과 같으며 backgroundColor를 primary / primarySurface를 지정한 각 행의 프리뷰를 보면 다른 색상이 노출됨을 볼 수 있다.

텍스트 사용

텍스트 사용 시 Text 컴포저블을 사용하여 표시하고 TextField 및 OutlinedTextField 를 통해 입력을 받으며 TextStyle 을 사용하여 텍스트에 단일 스타일을 적용할 수 있다. 또한 AnnotatedString 을 사용하여 텍스트에 여러 스타일 적용도 가능하다.

 

색상처럼 텍스트를 표시하는 Material 구성요소는 테마 맞춤설정을 선택한다.

Button(...) {
  Text("This text will use MaterialTheme.typography.button style by default")
}

사실 구성요소에서 텍스트를 직접 표시하지 않는 경향이 있기 때문에 Text 컴포저블을 전달할 수 있는 '슬롯 API'를 제공한다.

 

슬롯 API란?

androidx.compose.material:material 종속 항목을 기반으로 한 다양한 컴포저블 (DrawerFloatingActionButton 및 TopAppBar) 와 같은 요소가 모두 제공되는데

 

이때 Material 구성요소는 맞춤설정 레이어를 배치하기 위해 도입한 패턴인 슬롯 API를 많이 사용한다. 슬롯은 개발자가 원하는 대로 채울 수 있도록 UI에 빈 공간을 두며 아래는 TopAppBar에서 맞춤설정할 수 있는 슬롯이다.

내부적으로 ProvideTextStyle 컴포저블(자체적으로 CompositionLocal 사용)을 사용하여 '현재' TextStyle을 설정하며, textStyle을 매개변수로 제공하지 않으면 ‘현재’ 스타일을 가져오도록 기본 설정된다.

 

아래는 Compose의 Button 및 Text 클래스의 예이다.

@Composable
fun Button(
    // many other parameters
    content: @Composable RowScope.() -> Unit
) {
  ...
  ProvideTextStyle(MaterialTheme.typography.button) { //set the "current" text style
    ...
    content()
  }
}

@Composable
fun Text(
    // many, many parameters
    style: TextStyle = LocalTextStyle.current // get the value set by ProvideTextStyle
) { ...

테마 텍스트 스타일

색상과 마찬가지로 하드코딩보다 현재 테마에서 TextStyle을 사용하는 것이 좋다.

MaterialTheme.typographyMaterialTheme 에 설정된 Typography 를 검색하므로 개발자가 정의한 스타일을 사용할 수 있다.

Text(
  style = MaterialTheme.typography.subtitle2
)

TextStyle을 맞춤설정 하는 경우 copy하여 속성을 재정의하거나(data classText 컴포저블이 TextSTyle 위에 오버레이드될 여러 스타일 지정 매개변수를 허용하면 된다.

@Preview(showBackground = true, group = "Text")
@Composable
fun TextFontSizeAndCopyBackground() {
    PangMooTheme(darkTheme = false) {
        Text(
            text = "Hello World",
            style = MaterialTheme.typography.body1.copy(
                background = MaterialTheme.colors.secondary
            ),
            fontSize = 20.sp
        )
    }
}

여러 스타일

일반 텍스트에 여러 스타일을 적용하는 경우 마크업을 적용하는 AnnotatedString 클래스를 사용하면 SpanStyle을 텍스트 범위에 추가할 수 있으며 동적 추가 및 DSL 문법을 사용하여 콘텐츠를 만들 수 있다.
(Spnnable처럼 다양하게 커스텀 가능하며, InlineTextContent와 연계하면 이미지도 넣을 수 있다)

@Preview(showBackground = true, group = "Text")
@Composable
fun TextAnnotatedString() {
    PangMooTheme {
        val text = buildAnnotatedString {
            append("Kawai PangMoo\n")
            withStyle(SpanStyle(color = Color.Red)) {
                append("Red text\n")
            }
            withStyle(SpanStyle(fontSize = 24.sp)) {
                append("Large text")
            }
        }

        Text(text = text)
    }
}

또한 이런 식으로 간단하게 태그 컴포저블을 만들 수 도 있다.

@Preview(showBackground = true, group = "Text")
@Composable
fun TextTags() {
    val post = remember {
        listOf("pangmoo", "kotlin", "compose")
    }

    PangMooTheme {
        Surface {
            Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
                val tagStyle = MaterialTheme.typography.overline.toSpanStyle().copy(
                    background = MaterialTheme.colors.primary.copy(alpha = 0.1f)
                )
                post.forEachIndexed { index, tag ->
                    Text(text = buildAnnotatedString {
                        withStyle(tagStyle) {
                            append(" ${tag.toUpperCase()} ")
                        }
                    })
                }
            }
        }
    }
}

도형 사용

색상 및 서체와 마찬가지로 도형 테마도 사용할 수 있다. 다음은 Button의 shape 파라미터다.

@Composable
fun Button( ...
  shape: Shape = MaterialTheme.shapes.small
) {

다른 속성들처럼 copy를 통해 테마를 유지하면서 해당 부분만 유연하게 변경할 수 있다.

@Preview(showBackground = true, group = "Shape")
@Composable
fun TextFieldUsernameDefault() {
    TextField(
        value = "",
        onValueChange = {},
        modifier = Modifier.padding(24.dp),
        label = { Text(text = "Username") },
        shape = MaterialTheme.shapes.small
    )
}

@Preview(showBackground = true, group = "Shape")
@Composable
fun TextFieldUsernameOverride() {
    TextField(
        value = "",
        onValueChange = {},
        modifier = Modifier.padding(24.dp),
        label = { Text(text = "Username") },
        shape = MaterialTheme.shapes.small.copy(
            topStart = CornerSize(24.dp),
            topEnd = CornerSize(24.dp)
        )
    )
}

테마 도형

물론 테마를 사용하지 않고 직접 다음의 수정자를 통해 변경할 수 있다.

@Preview(showBackground = true, group = "Shape")
@Composable
fun CircleImage() {
    PangMooTheme {
        Surface {
            Image(
                painter = painterResource(id = R.drawable.ic_launcher_foreground),
                contentDescription = "profile",
                modifier = Modifier.clip(CircleShape).background(MaterialTheme.colors.primary)
            )
        }
    }
}

 

스타일  구성 요소

Android 뷰 스타일이나 CSS 스타일과 같이 구성요소 스타일을 추출하는 방식을 제공하지 않기에 이미 우리가 하고 있는 것처럼 구성요소 자체를 라이브러리로 만들어 앱 전체에 사용하면 된다.

@Preview(showBackground = true, group = "Style")
@Composable
fun StyleHeader() {
    PangMooTheme {
        Header(
            text = "Header",
            modifier = Modifier.fillMaxWidth(),
            onRequestBack = {},
            onRequestFavorite = {})
    }
}

@Composable
fun Header(
    text: String,
    modifier: Modifier = Modifier,
    isFavorite: Boolean = false,
    onRequestBack: () -> Unit,
    onRequestFavorite: () -> Unit,
) {
    Surface(
        color = MaterialTheme.colors.onSurface.copy(alpha = 0.1f),
        contentColor = MaterialTheme.colors.primary,
        modifier = modifier.semantics { heading() }
    ) {
        Row(
            horizontalArrangement = Arrangement.SpaceBetween,
            verticalAlignment = Alignment.CenterVertically
        ) {
            IconButton(onClick = onRequestBack) {
                Icon(
                    imageVector = Icons.Default.ArrowBack,
                    contentDescription = "Back",
                    modifier = Modifier.padding(16.dp)
                )
            }
            Text(
                text = text,
                style = MaterialTheme.typography.subtitle2,
                modifier = Modifier
            )
            IconButton(onClick = onRequestFavorite) {
                Icon(
                    imageVector = if(isFavorite) Icons.Default.Favorite else Icons.Default.FavoriteBorder,
                    contentDescription = "Delete",
                    modifier = Modifier.padding(16.dp)
                )
            }
        }
    }
}