1. Modifier = composed {}
- 현재는 공식적으로 사용하지 않는 기능
- Modifier에서 remember, LaunchedEffect, derivedStateOf 등의 기능을 사용 가능
- 단, 리컴포지션할때마다 {} 람다가 계속 재실행되고, 이로 인해 매번 Modifier 객체를 새로 생성하는 오버헤드 발생
- 최근에는 위 오버헤드를 해결하기 위해 Modifier.Node 및 ModifierNodeElement 방식을 사용
2. Modifier.Node
- 상태를 가질 수 있음
- 리컴포지션 시 유지되어 재사용 가능
- 실제 기능을 구현하는 부분
- 기능 구현 시 Node 클래스에 Modifier.Node와 추가로 필요한 기능들을 붙일 수 있음
- 예를들어 그리기 기능이 필요한 경우 DrawModifierNode를, 레이아웃을 만드는 경우 LayoutModiferNode를 상속받아 구현
3. ModifierNodeElement
- 상태를 갖지 않음
- 리컴포지션 할때마다 객체가 새로 생성됨
- create(), update() 함수를 구현하는 불변 클래스
1) create()
- Modifier가 처음 적용되는 시점에 노드를 생성하기 위해 호출됨
2) update(node)
- 기존 위치에 동일한 Modifier가 사용되었으나, 속성이 변경되는 경우 호출됨
- 클래스의 equals() 메서드로 속성이 변경되었는지 비교하므로 ModifierNodeElement는 일반적으로 data class로 구현
- data class는 equals() 메서드를 기본적으로 구현하므로
- equals() 판단 조건을 세부적으로 구현해야 하는 경우 일반 class로 만들고 equals() 메서드 직접 구현
- equals() 판단 조건을 제대로 구현하지 않으면 불필요한 update가 계속 발생함
- update 내에서 node를 새로 만들면 안되고, 기존의 노드를 갱신하는 방향으로 구현해야 함
- 이를 위해 기존 노드객체가 매개변수로 전달됨
- 만약 Node에 매개변수가 하나도 없다면 update할 필요 없으므로 data class 대신 단일 인스턴스인 data object로 구현
4. inspectableProperties
- Android Studio의 Layout Inspector 디버깅 도구를 열어 특정 컴포저블을 클릭했을 때, 우측의 속성창에 이 Modifier의 이름과 매개변수 값을 시각적으로 보여주는 역할
5. Modifier Factory
- Modifier.Node와 ModifierNodeElement를 사용해 구현한 Modifier를 부여하기 위한 API
6. 코드 구현 예시
// 2. Modifier.Node를 상속받아 구현한 클래스
// - 상태를 가질 수 있음
// - 실제 기능을 구현하는 부분
class CustomTagNode(
var tag: String // tag가 State 역할
) : Modifier.Node()
// 3. 노드의 생성과 업데이트를 관리하는 ModifierNodeElement
private data class CustomTagElement(
val tag: String
) : ModifierNodeElement<CustomTagNode>() {
// 최초 생성 시 호출됨
override fun create(): CustomTagNode = CustomTagNode(tag)
// 매개변수(상태) 변경 시 기존 노드 업데이트
override fun update(node: CustomTagNode) {
node.tag = tag
}
// 4. Layout Inspector 디버깅용 속성 정보 설정
override fun InspectorInfo.inspectableProperties() {
name = "customTag" // Layout Inspector에 표시될 Modifier 이름
properties["tag"] = tag // Layout Inspector에 표시될 매개변수 및 값
}
}
// 5. Modifier Factory
fun Modifier.customTag(tag: String): Modifier = this.then(CustomTagElement(tag))
- 특정 컴포저블의 Modifier에 .customTag를 붙이게 되면 해당 컴포저블이 처음 Attach될 때 Compose 프레임워크가 CustomTagElement의 create() 함수를 호출해서 노드 트리에 배치할 CustomTagNode 객체를 생성함
- 해당 컴포저블이 살아있는 동안 CustomTagNode도 함께 살아서 유지되며 기능을 수행함
- CustomTagNode의 매개변수는 State 역할을 하며, State 값이 바뀔때마다 CustomTagElement의 update 함수가 호출되어 Node 재생성 없이 기존의 Node를 갱신함
- 여러 컴포저블에 .customTag를 추가하면 추가한 갯수만큼 CustomTagNode 객체가 생성됨
7. 상속 가능한 Node의 종류
- Modifier.Node()와 함께 아래의 Node 클래스를 상속받아 추가적인 기능 구현 가능
- 대응되는 기능들은 간단하게 사용 가능한 고수준 람다 API이며, 더 세부적인 커스텀이나 최적화가 필요한 경우 아래 Node들을 상속받아 직접 구현
가. Layout 관련
1) LayoutModifierNode
- 레이아웃 측정(Measure) 및 배치(Layout) 제어
- Modifier.layout에 대응
2) ParentDataModifierNode
- 부모 레이아웃에 데이터 전달
- Modifier.weight()처럼 자식 컴포저블이 부모 레이아웃 측정에 데이터를 전달해줘야 할 때 사용
3) LayoutAwareModifierNode
- 레이아웃의 크기가 측정된 직후나 부모 레이아웃에 배치된 직후의 이벤트 감지
- 노드의 크기가 새롭게 측정되었을때, 부모 레이아웃 안에서 위치가 결정 되었을때 감지하여 호출됨
4) GlobalPositionAwareModifierNode
- 레이아웃 배치가 완료된 후 윈도우 기준 Node의 최종 위치/크기 감지
- Modifier.onGloballyPositioned에 대응
※ LayoutAwareModifierNode는 부모 레이아웃 내 상대적 위치 결정 시점을 감지하는 것이고, GlobalPositionAwareModifierNode는 윈도우 전체 기준의 절대 위치 변화를 감지하는 것
나. 그리기 관련
1) DrawModifierNode
- 레이아웃 공간에 그리기 작업을 수행
- Modifier.drawBehind나 drawWithContent에 대응
다. 입력 관련
1) PointerInputModifierNode
- 터치, 드래그, 마우스 등의 포인터 입력 처리
- pointerInput {}과 유사한 역할
2) KeyInputModifierNode / RotaryInputModifierNode
- 하드웨어 키보드 키 입력 및 Rotary 입력 처리
라. Focus 관련
1) FocusEventModifierNode / FocusPropertiesModifierNode / FocusRequesterModifierNode
- Focus 상태 감지 및 제어
- 포커스 이동/획득/상실 상태 감지 및 포커스 순서 변경 등 관리
마. Compose 관련
1) CompositionLocalConsumerModifierNode
- @Composable이 아닌 Modifier.Node 내부에서 CompositionLocal 데이터(LocalContext, LocalDensity 등) 접근
2) ObserverModifierNode
- Compose의 State 변화를 관찰하고 반응
- @Composable이 아닌 Modifier.Node 내부에서 특정 State 변경을 감지하고, State 변경 시 리컴포지션 없이 노드의 특정 로직을 수행할 수 있음
바. Modifier.Node 관련
1) TraversableNode
- Modifier Node 트리 간 통신/순회
- Node 트리 상하위에 위치한 다른 TraversableNode를 찾아 데이터를 공유하거나 이벤트 전달
2) DelegatingNode
- Modifier.Node를 만들 때 기존에 작성된 다른 Modifier.Node들의 기능을 조합해서 노드 위임을 구현할 수 있게 해주는 클래스
사. 접근성 관련
1) SemanticsModifierNode
- 접근성, semantics 정보 제공
8. 추가로 궁금했던 것들
1) Modifier = composed{}의 {} 내에서 remember를 써서 구현한 값들은 모두 Node의 매개변수로 대체할 수 있는건지?
- 답변 : 매개변수 또는 멤버변수로 대체 가능
- composed {}에서는 리컴포지션 시 상태를 유지하기 위해 remember가 필수였음
- 반면 Modifier.Node는 일반 클래스의 객체이므로 리컴포지션 시 그대로 유지됨
- 그러므로 Modifier.Node에서는 remember 없이 그냥 멤버변수로 선언하면 됨
// 1. composed {} 방식
fun Modifier.myModifier() = composed {
val state = remember { MyState() } // remember 필요
}
// 2. Modifier.Node 방식
class MyNode(
... // 매개변수들
) : Modifier.Node() {
val state = MyState() // remember 없이 그냥 멤버 변수로 선언하면 유지됨!
}
2) Modifier = composed{}의 LaunchedEffect는 다 update에서 구현하는 로직으로 대체 가능한건지?
- 답변 : 맞음
- Modifier.Node에는 기본적으로 coroutineScope 멤버변수가 있음
- 위 변수로 LaunchedEffect 내부 Scope 역할을 해서 기존 작업 cancel 후 launch 수행하면 됨
// 1. composed {} 방식
fun Modifier.customTagModifier(tag: String) = composed {
LaunchedEffect(tag) {
somthingFunc(tag)
}
}
// 2. Modifier.Node 방식
class CustomTagNode(var tag: String) : Modifier.Node() {
var job: Job? = null
fun updateKey(newTag: String) { // LaunchedEffect 내부 구현부 역할
job?.cancel()
job = coroutineScope.launch {
somthingFunc(newTag)
}
}
}
private data class CustomTagElement(
val tag: String
) : ModifierNodeElement<CustomTagNode>() {
...
override fun update(node: MyNode) {
node.updateKey(key) // update 호출 시 Node 객체에 구현해둔 함수 호출
}
}
3) Modifier = composed{}의 DisposableEffect, onDispose는 어떻게 대체할 수 있는지? 소멸시점에 수행할 동작을 정의 가능한지?
- 답변 : onAttach(), onDetach()로 대체 가능
- Modifier.Node에는 onAttach(), onDetach() 함수가 있음
- 위 두 함수를 override해서 해당 시점의 동작 정의
class CustomTagNode : Modifier.Node() {
// UI 트리에 추가될 때 (DisposableEffect 시작 시점)
override fun onAttach() {
super.onAttach()
...
}
// UI 트리에서 제거될 때 (onDispose 시점)
override fun onDetach() {
super.onDetach()
...
}
}