https://github.com/tmdgh1592/Jetpack-Compose-Android-Examples
컴포즈 공부 내용은 위 깃허브 레퍼지토리에 정리합니다.
https://itstory1592.tistory.com/74
(이전 포스팅에 이어서 Compose의 Does and Don't에 대해 다룹니다.)
이번 포스팅에서는 Compose API 가이드라인을 살펴보면서 Do & Dont (두스앤돈스) 코드를 공부하려고 합니다.
Do 코드는 해당 라이브러리를 제공해주는 측에서 권장하는 방식의 코드 작성법입니다.
반대로 Don't 코드는 권장하지 않는 코드 작성법입니다.
■ Compose UI modifier
modifier는 immutable입니다. 즉, mutable이 아니기 때문에 값을 임의로 수정하는 것이 불가능합니다.
이를 위해 modifier는 factory 패턴으로 설계되어 있고, 화면의 속성을 지정할 수 있는 Modifier factory functions을 제공합니다.
Modifier.preferredSize(50.dp)
.backgroundColor(Color.Blue)
.padding(10.dp)
또한, Modifier APIs는 Modifier.Element 인터페이스 implementation type을 노출해서는 안됩니다.
fun Modifier.myModifier(
param1: ...,
paramN: ...
): Modifier = then(MyModifierImpl(param1, ... paramN))
■ Composed modifiers
Modifier.composed{ } 를 사용하면 Modifier에 다른 여러 Modifier를 함께 사용할 수 있습니다.
fun Modifier.myModifier(): Modifier = composed {
val color = LocalTheme.current.specialColor
backgroundColor(color)
}
fun Modifier.modifierWithState(): Modifier = composed {
val elementSpecificState = remember { MyModifierState() }
MyModifier(elementSpecificState)
}
// ...
val myModifier = someModifiers.modifierWithState()
Text("Hello", modifier = myModifier)
Text("World", modifier = myModifier)
■ Layout-scoped modifiers
Android의 View 시스템에는 LayoutParams라는 개념이 있습니다. 이는 ViewGroup의 자식 보기와 함께 불투명하게 저장된 객체 유형으로, 이를 측정하고 배치할 ViewGroup에 특정한 레이아웃 지침을 제공합니다.
Compose UI 수정자는 레이아웃 콘텐츠 기능에 대해 ParentDataModifier 및 수신기 범위 개체를 사용하여 관련 패턴을 제공합니다.
@Stable
interface WeightScope {
fun Modifier.weight(weight: Float): Modifier
}
@Composable
fun WeightedRow(
modifier: Modifier = Modifier,
content: @Composable WeightScope.() -> Unit
) {
// ...
// Usage:
WeightedRow {
Text("Hello", Modifier.weight(1f))
Text("World", Modifier.weight(2f))
}
■ Compose API design patterns
이 섹션에서는 Jetpack Compose API를 설계할 때 일반적인 사용 사례를 해결하기 위한 패턴을 간략하게 설명합니다.
즉, Compose로 UI를 구성할 때 자주 사용되는 패턴입니다.
□ Prefer stateless and controlled @Composable functions
예를 들어, Checkbox를 사용할 때, Checkbox Compose 스코프 내에서 isChecked와 같은 state를 가지고 있으면 안됩니다.
마치 Don't 예시 처럼, checkedState가 Checkbox 함수 내부에 선언되어 있는데, 이런 방식은 적절하지 않다고 합니다.
만약 구현을 하게 된다면 Do 예시처럼 Checkbox의 상태를 외부에서 파라미터로 전달하고 Controll 합니다.
Do
@Composable
fun Checkbox(
isChecked: Boolean,
onToggle: () -> Unit
) {
// ...
// Usage: (caller mutates optIn and owns the source of truth)
Checkbox(
myState.optIn,
onToggle = { myState.optIn = !myState.optIn }
)
Don't
@Composable
fun Checkbox(
initialValue: Boolean,
onChecked: (Boolean) -> Unit
) {
var checkedState by remember { mutableStateOf(initialValue) }
// ...
// Usage: (Checkbox owns the checked state, caller notified of changes)
// Caller cannot easily implement a validation policy.
Checkbox(false, onToggled = { callerCheckedState = it })
□ Hoisted state types
이전에는 Compose 파라미터로 변수와 해당 변수의 상태변화 콜백함수를 전달하였지만, 이후에는 Stable Interface로 상태를 가지고 있도록 하여 그 자체를 넘기도록 권장합니다.
또한 이러한 Interface의 네이밍을 할 때 suffix로 State가 붙도록 합니다.
Before
@Composable
fun VerticalScroller(
scrollPosition: Int,
scrollRange: Int,
onScrollPositionChange: (Int) -> Unit,
onScrollRangeChange: (Int) -> Unit
) {
After
@Stable
interface VerticalScrollerState {
var scrollPosition: Int
var scrollRange: Int
}
@Composable
fun VerticalScroller(
verticalScrollerState: VerticalScrollerState
) {
□ Default policies through hoisted state objects
Compose UI는 Compose 런타임에 빌드된 UI 도구 키트입니다. 이 섹션에서는 Compose UI 도구 키트를 사용하고 확장하는 API에 대한 지침을 간략하게 설명합니다.
예시) 아래 코드와 같이 UI를 만들면 됩니다.이러한 정책 개체의 사용자 정의 구현 또는 외부 소유권은 종종 필요하지 않습니다. Kotlin의 기본 arguments, Compose의 remember {} API / Kotlin "extension constructor" 패턴을 사용하여 API는 원할 때 더 정교한 사용을 허용하면서 간단한 사용을 위한 기본 상태 처리 정책을 제공할 수 있습니다.
예시)
fun VerticalScrollerState(): VerticalScrollerState =
VerticalScrollerStateImpl()
private class VerticalScrollerStateImpl(
scrollPosition: Int = 0,
scrollRange: Int = 0
) : VerticalScrollerState {
private var _scrollPosition by
mutableStateOf(scrollPosition, structuralEqualityPolicy())
override var scrollPosition: Int
get() = _scrollPosition
set(value) {
_scrollPosition = value.coerceIn(0, scrollRange)
}
private var _scrollRange by
mutableStateOf(scrollRange, structuralEqualityPolicy())
override var scrollRange: Int
get() = _scrollRange
set(value) {
require(value >= 0) { "$value must be > 0" }
_scrollRange = value
scrollPosition = scrollPosition
}
}
@Composable
fun VerticalScroller(
verticalScrollerState: VerticalScrollerState =
remember { VerticalScrollerState() }
) {
이렇게 구현한 ScrollerState을 사용할 때, Default 값을 null로 설정하는 경우, 예상치 못한 오류가 발생할 수 있습니다. 그렇기 때문에 애초에 Scroller의 파라미터로 remember {} 스코프를 사용하여 null이 될 수 없도록 전달합니다.
Do
@Composable
fun VerticalScroller(
verticalScrollerState: VerticalScrollerState =
remember { VerticalScrollerState() }
) {
Don't
// Null as a default can cause unexpected behavior if the input parameter
// changes between null and non-null.
@Composable
fun VerticalScroller(
verticalScrollerState: VerticalScrollerState? = null
) {
val realState = verticalScrollerState ?:
remember { VerticalScrollerState() }
□ Default hoisted state for modifiers
이 부분은 위의 내용과 비슷합니다. 기본적으로 상태를 null로 두고 사용하지 말아야 합니다. 위 내용의 State와 동일하게 null로 상태를 초기화하는 경우 예상치못한 오류가 발생할 수 있기 때문에 외부에서 파라미터로 받아 상태를 관리합니다.
Do
fun Modifier.foo() = composed {
FooModifierImpl(remember { FooState() }, LocalBar.current)
}
fun Modifier.foo(fooState: FooState) = composed {
FooModifierImpl(fooState, LocalBar.current)
}
Don't
// Null as a default can cause unexpected behavior if the input parameter
// changes between null and non-null.
fun Modifier.foo(
fooState: FooState? = null
) = composed {
FooModifierImpl(
fooState ?: remember { FooState() },
LocalBar.current
)
}
// @Composable modifier factory functions cannot be used
// outside of composition.
@Composable
fun Modifier.foo(
fooState: FooState = remember { FooState() }
) = composed {
FooModifierImpl(fooState, LocalBar.current)
}
□ Extensibility of hoisted state types
호이스팅된 상태 유형은 이를 수락하는 구성 가능한 기능의 동작에 영향을 미치는 정책 및 유효성 검사를 구현하는 경우가 많습니다. 구체적이고 특히 최종 호이스트 상태 유형은 상태 객체가 호소하는 진실 소스의 억제 및 소유권을 의미합니다.
극단적인 경우 여러 정보 소스를 생성하여 앱 코드에서 여러 개체 간에 데이터를 동기화해야 하므로 반응형 UI API 디자인의 이점을 무력화할 수 있습니다.
// Defined by another team or library
data class PersonData(val name: String, val avatarUrl: String)
class FooState {
val currentPersonData: PersonData
fun setPersonName(name: String)
fun setPersonAvatarUrl(url: String)
}
// Defined by the UI layer, by yet another team
class BarState {
var name: String
var avatarUrl: String
}
@Composable
fun Bar(barState: BarState) {
이러한 API는 FooState 및 BarState 클래스가 모두 나타내는 데이터의 출처가 되기를 원하기 때문에 함께 사용하기 어렵습니다. 여러 팀, 라이브러리 또는 모듈이 시스템 간에 공유해야 하는 데이터에 대해 단일 통합 유형에 동의하는 옵션이 없는 경우가 많습니다. 이러한 설계가 결합되어 앱 개발자 측에서 잠재적으로 오류가 발생하기 쉬운 데이터 동기화에 대한 요구 사항을 형성합니다.
보다 유연한 접근 방식은 이러한 호이스트된 상태 유형을 모두 인터페이스로 정의하여 통합 개발자가 시스템의 상태 관리에서 단일 정보 소스를 유지하면서 다른 하나 또는 세 번째 유형의 관점에서 둘 다를 정의할 수 있도록 합니다.
라고 공식 문서에 나와 있으나, 말이 너무 어려워서 코드상으로 봤을 때, State를 관리함에 있어서 @Stable Annotation이 붙은 interface로 상태를 관리하라고 "일단은" 이해하겠습니다.
@Stable
interface FooState {
val currentPersonData: PersonData
fun setPersonName(name: String)
fun setPersonAvatarUrl(url: String)
}
@Stable
interface BarState {
var name: String
var avatarUrl: String
}
class MyState(
name: String,
avatarUrl: String
) : FooState, BarState {
override var name by mutableStateOf(name)
override var avatarUrl by mutableStateOf(avatarUrl)
override val currentPersonData: PersonData =
PersonData(name, avatarUrl)
override fun setPersonName(name: String) {
this.name = name
}
override fun setPersonAvatarUrl(url: String) {
this.avatarUrl = url
}
}
Jetpack Compose framework and Library development 은 호이스트 상태 유형을 사용자 정의 구현을 허용하는 인터페이스로 선언해야 합니다(SHOULD). 추가적인 표준 정책 시행이 필요한 경우 추상 클래스를 고려하십시오.
Jetpack Compose framework and Library development 은 유형과 동일한 이름을 공유하는 호이스트 상태 유형의 기본 구현을 위한 팩토리 기능을 제공해야 합니다(SHOULD). 이것은 소비자를 위한 동일한 단순 API를 구체적인 유형으로 유지합니다.
예시)
@Stable
interface FooState {
// ...
}
fun FooState(): FooState = FooStateImpl(...)
private class FooStateImpl(...) : FooState {
// ...
}
// Usage
val state = remember { FooState() }
앱 개발은 인터페이스에서 제공하는 추상화가 필요하다고 입증될 때까지 더 단순한 구체적인 유형을 선호해야 합니다(SHOULD). 그럴 때 위에서 설명한 대로 기본 구현을 위한 팩토리 기능을 추가하는 것은 사용 사이트의 리팩토링이 필요하지 않은 소스 호환 변경입니다.
'Android > Compose' 카테고리의 다른 글
[Android] Jetpack Compose Part 1 - Does and Dont Code Style #1 (0) | 2022.05.15 |
---|---|
[Android] Jetpack Compose Part 0 - Compose란? (0) | 2022.05.04 |