스타일을 사용하여 앱을 빌드하는 방법은 여러 가지가 있습니다. 선택하는 방법은 앱이 Material Design 채택과 관련하여 어디에 있는지에 따라 다릅니다.
- 완전한 맞춤 디자인 시스템, Material Design을 사용하지 않음
- 권장사항: 테마에서 값을 사용하는 구성요소 스타일을 정의하고 디자인 시스템 구성요소에 스타일 매개변수를 노출합니다.
- 머티리얼 디자인 사용
- 권장사항: 스타일과 통합하기 위해 머티리얼 채택을 기다립니다. 가능한 경우 자체 구성요소에 스타일을 사용합니다.
스타일 레이어
기존 Compose 모델에서 맞춤설정은 MaterialTheme에서 제공하는 전역 토큰 (색상 및 서체)을 재정의하거나 가능한 경우 디자인 시스템 컴포저블의 속성을 래핑하고 재정의하는 데 크게 의존합니다.
경우에 따라 하위 시스템 또는 매개변수를 통해 노출되지 않지만 구성요소 자체에 하드 코딩된 기본값이 있는 머티리얼 레이어 내에 속성이 있습니다.
스타일 API에는 하위 시스템과 구성요소 간의 브리지인 새로운 추상화 레이어인 스타일 이 있습니다.
| 레이어 | 책임 | 예 |
|---|---|---|
| 하위 시스템 값 | 명명된 값 | val Primary = Color(0xFF34A85E) |
| 원자 스타일 | 속성 변경을 정확히 한 번 실행하는 스타일 | val buttonStyle = paddingAtomic then roundedCornerShapeAtomic then primaryBackgroundAtomic then largeSize then interactiveShadowAtomic |
| 구성요소 스타일 | 구성요소별 구성 | 기본 배경과 16dp 패딩이 있는 버튼 val buttonStyle = Style { contentPadding(16.dp) shape(RoundedCornerShape(8.dp)) background(Color.Blue) } |
| 구성요소 | 스타일을 사용하는 기능적 UI 요소입니다. | Button(style = buttonStyle) { ... } |
원자 스타일과 모놀리식 스타일
스타일 API를 사용하면 스타일을 별도의 원자 스타일로 분류할 수 있습니다.
baseButtonStyle과 같은 복잡한 구성요소별 스타일을 정의하는 대신 소규모의 단일 용도 유틸리티 스타일을 만들 수도 있습니다. 이러한 스타일은 '원자' 역할을 합니다.
// Define single-purpose "atomic" styles val paddingAtomic = Style { contentPadding(16.dp) } val roundedCornerShapeAtomic = Style { shape(RoundedCornerShape(8.dp)) } val primaryBackgroundAtomic = Style { background(Color.Blue) } val largeSizeAtomic = Style { size(100.dp, 40.dp) } val interactiveShadowAtomic = Style { hovered { animate { dropShadow( Shadow( offset = DpOffset( 0.dp, 0.dp ), radius = 2.dp, spread = 0.dp, color = Color.Blue, ) ) } } }
'then'을 사용한 구성
새로운 스타일 API의 강력한 기능 중 하나는 여러 Style 객체를 병합할 수 있는 then 연산자입니다. 이를 통해 원자 유틸리티 클래스를 사용하여 구성요소를 빌드할 수 있습니다.
기존 (원자 아님):
// One large monolithic style val buttonStyle = Style { contentPadding(16.dp) shape(RoundedCornerShape(8.dp)) background(Color.Blue) }
원자 리팩터링:
// Combine atoms to create the final appearance val buttonStyle = paddingAtomic then roundedCornerShapeAtomic then primaryBackgroundAtomic then interactiveShadowAtomic
디자인 시스템에서 스타일 채택
디자인 시스템이 스펙트럼의 어느 위치에 있는지에 따라 디자인 시스템 내에서 스타일을 채택할 때 다음 옵션을 고려하세요.
스타일이 있는 맞춤 디자인 시스템
고려사항: 머티리얼 디자인을 기반으로 하지 않는 광범위한 브랜드 가이드가 제공되었으며 머티리얼 디자인을 사용할 계획이 없습니다.
**전략**: 완전한 맞춤 디자인 시스템을 구현하고 스타일을 테마의 일부 로 노출합니다.
이 옵션은 머티리얼을 기본 디자인 시스템 언어로 사용하지 않는 경우 맞춤 경로입니다. 시각적 정의를 위해 MaterialTheme을 완전히 우회하고
이미 자체 맞춤 테마를 만들었습니다. 스타일의 컨테이너 역할을 하는 CompanyTheme을 빌드합니다.
- 작동 방식: 시스템의 모든 구성요소에
Style객체 를 보유하는CompanyTheme객체를 만듭니다. 구성요소 (머티리얼 로직 또는 맞춤Box또는Layout구현을 래핑)는 이러한 스타일을 직접 사용하고 디자인 시스템의 소비자를 위한Style매개변수를 노출합니다. - 스타일 레이어: 스타일은 디자인 시스템의 기본 정의입니다. 토큰은 이러한 스타일에 제공되는 명명된 변수입니다. 이를 통해 상태 변경에 고유한 애니메이션 정의(예: 누를 때 크기 및 색상 애니메이션)와 같은 심층 맞춤설정이 가능합니다.
머티리얼을 사용하지 않고 자체 맞춤 테마를 빌드하고 스타일을 채택하려면 스타일 목록을 테마에 추가하세요. 이렇게 하면 프로젝트의 어느 곳에서나 기본 스타일에 액세스할 수 있습니다.
애플리케이션의 다양한 스타일을 저장하고 기본값을 만드는
Styles클래스를 만듭니다. 예를 들어 Jetsnack 앱에서 클래스 이름은JetsnackStyles입니다.object JetsnackStyles{ val buttonStyle: Style = Style { shape(shapes.medium) background(colors.brand) contentColor(colors.textPrimary) contentPaddingVertical(8.dp) contentPaddingHorizontal(24.dp) textStyle(typography.labelLarge) disabled { animate { background(colors.brandSecondary) } } } val cardStyle: Style = Style { shape(shapes.medium) background(colors.uiBackground) contentColor(colors.textPrimary) } }
전체 테마의 일부로
Styles를 제공하고StyleScope에 도우미 확장 함수를 노출하여 하위 시스템에 액세스합니다.@Immutable class JetsnackTheme( val colors: JetsnackColors = LightJetsnackColors, val typography: androidx.compose.material3.Typography = androidx.compose.material3.Typography(), val shapes: Shapes = Shapes() ) { companion object { val colors: JetsnackColors @Composable @ReadOnlyComposable get() = LocalJetsnackTheme.current.colors val typography: androidx.compose.material3.Typography @Composable @ReadOnlyComposable get() = LocalJetsnackTheme.current.typography val shapes: Shapes @Composable @ReadOnlyComposable get() = LocalJetsnackTheme.current.shapes val styles: JetsnackStyles = JetsnackStyles val LocalJetsnackTheme: ProvidableCompositionLocal<JetsnackTheme> get() = LocalJetsnackThemeInstance } } val StyleScope.colors: JetsnackColors get() = LocalJetsnackTheme.currentValue.colors val StyleScope.typography: androidx.compose.material3.Typography get() = LocalJetsnackTheme.currentValue.typography val StyleScope.shapes: Shapes get() = LocalJetsnackTheme.currentValue.shapes internal val LocalJetsnackThemeInstance = staticCompositionLocalOf { JetsnackTheme() } @Composable fun JetsnackTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { val colors = if (darkTheme) DarkJetsnackColors else LightJetsnackColors val theme = JetsnackTheme(colors = colors) CompositionLocalProvider( LocalJetsnackTheme provides theme, ) { MaterialTheme( typography = LocalJetsnackTheme.current.typography, shapes = LocalJetsnackTheme.current.shapes, content = content, ) } }
컴포저블 내에서
JetsnackStyles에 액세스합니다.@Composable fun CustomButton(modifier: Modifier, style: Style = Style, text: String) { val interactionSource = remember { MutableInteractionSource() } val styleState = remember(interactionSource) { MutableStyleState(interactionSource) } // Apply style to top level container in combination with incoming style from parameter. Box(modifier = modifier .clickable( interactionSource = interactionSource, indication = null, enabled = true, role = Role.Button, onClick = { }, ) .styleable(styleState, JetsnackTheme.styles.buttonStyle, style)) { Text(text) } }
전역 테마 채택 외에도 앱에 Styles를 통합하는 대체 전략이 있습니다. 특정 호출 사이트에 Styles를 인라인으로 활용하거나 전체 테마 기능이 필요하지 않은 경우 정적 정의를 사용할 수 있습니다.
전체 스타일이 근본적으로 다른 경우가 아니면 Styles를 조건부로 전환해서는 안 됩니다. 서로 다른 스타일 객체 간에 전환하는 대신 시각적 정의 내에서 동적 토큰에 액세스하는 것이 좋습니다.