derivedStateOf๋ ํ๋ ์ด์์ Compose State๋ก๋ถํฐ "ํ์๋ ์ํ"๋ฅผ ๋ง๋ค๊ณ ,
๊ฒฐ๊ณผ ๊ฐ์ด ์ค์ ๋ก ๋ฐ๋์์ ๋๋ง ๋ฆฌ์ปดํฌ์ง์ ์ ์ ๋ฐํ๋ Effect API
var text by remember { mutableStateOf("") }
val filteredText by remember {
derivedStateOf {
text.filter { !it.isDigit() }
}
}
ํด๋น ์ฝ๋๋ฅผ ํตํด์ text ์ ๊ฐ์ด ๋ณ๊ฒฝ๋ ๋ ๋ด๋ถ ํจ์๋ก ๋ฑ๋ก๋ filter ๊ฐ ํธ์ถ๋์ด ์คํ๋ฉ๋๋ค.
text ๊ฐ ๋ฐ๋ ๋๋ง๋ค derivedStateOf ์์ ์์ ๋ค์ ๊ณ์ฐํ๊ณ , ๊ฒฐ๊ณผ๊ฐ ๋ฌ๋ผ์ก์ ๋๋ง UI๋ฅผ ๋ค์ ๊ทธ๋ฆฝ๋๋ค.
State๊ฐ ๋ณ๊ฒฝ๋๋ฉด ํด๋น State๋ฅผ ์ฝ์ Composable์ย ๋ฌด์กฐ๊ฑด ๋ฆฌ์ปดํฌ์ง์ ๋ฉ๋๋ค.
val text by remember { mutableStateOf("") }
val filteredTextby = remember(text) {
text.filter { !it.isDigit() }
}
text๊ฐ ๋ฐ๋ ๋๋ง๋คย filteredText๋ ์ฌ๊ณ์ฐ๋๊ณ ์ด๋ฅผ ์ฌ์ฉํ๋ UI๋ ๋ค์ ๊ทธ๋ฆฝ๋๋ค. ๋ฌธ์ ๋ย text๊ฐ ๋ฐ๋์ด๋ ํํฐ๋ง ๊ฒฐ๊ณผ๊ฐ ๋์ผํ ๊ฒฝ์ฐ์๋ ๋ฆฌ์ปดํฌ์ง์
์ด ๋ฐ์ํ๋ค๋ ์ ์
๋๋ค.
์๋ฅผ ๋ค์ด ์ ์ฝ๋๋ฅผ ์์๋ก ํ
์คํธ์์ ์ซ์๋ง ํํฐํ ๋, ย "abc"ย โย "abcd"ย โย "abc"ย ์ฒ๋ผ ์ซ์๊ฐ ์๋ ์ํ๊ฐ ๊ณ์ ์ ์ง๋์ด๋, text๊ฐ ๋ฐ๋๋ ๋งค ์๊ฐ UI๋ฅผ ๋ค์ ๊ทธ๋ฆฝ๋๋ค. ๊ณ์ฐ ๊ฒฐ๊ณผ๊ฐ ๊ฐ์ผ๋ฉด ์ค์ UI ๋ณํ๊ฐ ์๋๋ฐ๋ ๋ค์ ๊ทธ๋ฆฌ๋ ๊ฒ์ ๋นํจ์จ์ ์
๋๋ค.
@StateFactoryMarker
public fun <T> derivedStateOf(calculation: () -> T): State<T> =
DerivedSnapshotState(calculation, null)
derivedStateOf๋ ๋ด๋ถ์ ์ผ๋ก DerivedSnapshotState๋ผ๋ ํน์ํ State ๊ตฌํ์ฒด๋ฅผ ์์ฑํฉ๋๋ค.
DerivedSnapshotState๋ ๋ค์๊ณผ ๊ฐ์ ์ฑ
์์ ๊ฐ์ง๋๋ค.
calculation) ๋ณด๊ด์ด ๋ชจ๋ ๋ก์ง์ ๋ด๋ถ์ ResultRecord๋ฅผ ํตํด ๊ด๋ฆฌ๋ฉ๋๋ค.
class ResultRecord<T>(snapshotId: SnapshotId) : StateRecord(snapshotId), DerivedState.Record<T> {
override var dependencies: ObjectIntMap<StateObject> = emptyObjectIntMap()
var result: Any? = Unset
var resultHash: Int = 0
...
}
result: ๋ง์ง๋ง ๊ณ์ฐ ๊ฒฐ๊ณผ
dependencies: ๊ณ์ฐ ์ค ์ฝํ State ๋ชฉ๋ก
resultHash: dependencies๋ค์ด ํ์ฌ snapshot ์์๋ ๊ฐ์ ์ํ์ธ์ง ํ๋จํ๊ธฐ ์ํ ํด์

fun isValid(derivedState: DerivedState<*>, snapshot: Snapshot): Boolean {
val snapshotChanged = sync {
validSnapshotId != snapshot.snapshotId ||
validSnapshotWriteCount != snapshot.writeCount
}
val isValid =
result !== Unset &&
(!snapshotChanged || resultHash == readableHash(derivedState, snapshot))
if (isValid && snapshotChanged) {
sync {
validSnapshotId = snapshot.snapshotId
validSnapshotWriteCount = snapshot.writeCount
}
}
return isValid
}
ํด๋น ์ฝ๋๋ฅผ ํตํด ํ์ฌ ResultRecord ๊ฐ ์ฃผ์ด์ง snapshot ์์ ์์ ์ต์ ์ํ์ธ์ง ํ์ธํฉ๋๋ค.
ํด๋น ๋ถ๋ถ์ ์ต์ ์ํ๊ฐ ์๋๋ผ๋ฉด Snapshot.observe() ๋ฅผ ํตํด์ calculation()์ ํธ์ถํด ์๋ก์ด dependencies ์ result ๋ฅผ ์ค์ ํฉ๋๋ค.
derivedStateOf๋ state ๊ฐ์ด ๋ณ๊ฒฝ๋ ๋๋ง๋ค ๋์ โ๊ณ์ฐ + ๋น๊ตโ ๋น์ฉ์ ํญ์ ๊ฐ์ํ๋ ๊ตฌ์กฐ์ ๋๋ค.
์ํํ ์ฑ๋ฅ์ ๋ณด์ฅํ๊ธฐ ์ํด
derivedStateOf ์์ ์์ฃผ ๋ฌด๊ฑฐ์ด ์ฐ์ฐ์์ ๋ฃ๋ ๊ฒ์ ํผํด์ผ ํฉ๋๋ค.
์ฐ์ฐ์ด ๋ณต์กํด์ง์๋ก
derivedStateOf์ โ๊ณ์ฐ + ๋น๊ต ๋น์ฉโ์ด ๋ฆฌ์ปดํฌ์ง์ ์ผ๋ก UI๋ฅผ ๋ค์ ๊ทธ๋ฆฌ๋ ๋น์ฉ๋ณด๋ค ๋ ์ปค์ง ์ ์์ต๋๋ค.

derivedStateOfย ๋ด๋ถ ์ฐ์ฐ ์์ฒด๊ฐ ๊ฐ๋ฒผ์ด ๊ฒฝ์ฐ๋ํ ์์: ์คํฌ๋กค ๊ด๋ จ ์ฒ๋ฆฌ lazyListState
@Composable
fun ScrollToTopButton(lazyListState: LazyListState, threshold: Int) {
val isEnabled by remember(threshold) {
derivedStateOf { lazyListState.firstVisibleItemIndex > threshold }
}
Button(onClick = { }, enabled = isEnabled) {
Text("Scroll to top")
}
}
์คํฌ๋กค ์ด๋ฒคํธ๋ ํ๋ ์๋ง๋ค ๋ฐ์ํ์ง๋ง,ย firstVisibleItemIndex > threshold์ ๊ฒฐ๊ณผ(Boolean)๋ ์๊ณ๊ฐ์ ๋๋ ์๊ฐ์๋ง ๋ฐ๋๋๋ค.ย derivedStateOf๊ฐ ๋๋ถ๋ถ์ ํ๋ ์์์ ๋ฆฌ์ปดํฌ์ง์
์ ๋ง์ ์ค๋๋ค.
์ฌ์ฉ ์์)
derivedStateOf { text.filter { it.isDigit() } })๊ณ์ฐ์ ํตํ ์ค๋ฒํค๋๊ฐ recomposition์ ํตํด UI๋ฅผ ์ ๋ฐ์ดํธํ๋ ๋น์ฉ๋ณด๋ค ๋ ํฌ๋ฉด ์ฑ๋ฅ์ ์ญํจ๊ณผ๊ฐ ๋ ์ ์์ต๋๋ค.
1. ๊ฒฐ๊ณผ๊ฐ ๊ฑฐ์ ํญ์ ๋ฐ๋๋ ๊ฒฝ์ฐ
@Composable
fun ScrollProgress(listState: LazyListState) {
val progress by remember {
derivedStateOf {
val total = (listState.layoutInfo.totalItemsCount - 1).coerceAtLeast(1)
listState.firstVisibleItemIndex / total.toFloat()
}
}
LinearProgressIndicator(progress = progress)
}
ํด๋น ์์๋ ์คํฌ๋กคํ ๋๋ง๋คย progressย ๊ฐ์ด ๋ฐ๋๋๋ค. ๊ฒฐ๊ณผ๊ฐ ๊ฑฐ์ ํญ์ ๋ฌ๋ผ์ง๋ฏ๋กย derivedStateOf์ ๋น๊ต ๋น์ฉ๋ง ์ถ๊ฐ๋ ๋ฟ ๋ฆฌ์ปดํฌ์ง์
์คํต ํจ๊ณผ๋ ์์ต๋๋ค. ์ด๋ฐ ๊ฒฝ์ฐ๋ ๋จ์ํ recomposition์ ํ์ฉํ๋ ํธ์ด ๋ซ์ต๋๋ค.
2. ๊ณ์ฐ ๋น์ฉ์ด ๋น์ผ ๊ฒฝ์ฐ
filter/map/groupBy/sortedBy@Composable
fun GroupedSectionList(viewModel: MyViewModel) {
val items by viewModel.items.collectAsState()
val query by viewModel.query.collectAsState()
val grouped by remember {
derivedStateOf {
items
.asSequence()
.filter { it.name.contains(query, ignoreCase = true) }
.groupBy { it.category }
.mapValues { (_, v) -> v.sortedBy { it.date } }
.toMap()
}
}
}
์ด๊ฒฝ์ฐ items๋ query๊ฐ ๋ฐ๋ ๋๋ง๋ค filter/groupBy/sortedBy ์ ์ฒด๊ฐ ์ฌ์คํ๋ฉ๋๋ค. ๊ทธ๋ฅ ๋ฆฌ์ปดํฌ์ง์
์ ํ๋ ๊ฒฝ์ฐ ๋ณด๋ค ๋ ํด๋น ์์ ์คํํ๊ณ ๋น๊ตํ๋ ์ฐ์ฐ์ด ๋ ๋น์ธ์ง ์ ์์ต๋๋ค.
3. ๋จ์ ๊ณ์ฐ
์๋ ๊ฐ์ ์ฐ์ฐ์ ๋น์ฉ์ด ์ฌ์ค์ 0์ ๊ฐ๊น๊ณ , ๋ฆฌ์ปดํฌ์ง์ ๋น์ฉ๋ ๋๋ถ๋ถ ๋งค์ฐ ๋ฎ์ต๋๋ค.
derivedStateOf { list.isEmpty() }
์คํ๋ ค ์ฝ๋ ๋ณต์ก๋๋ฅผ ์ฆ๊ฐ ์ํค๊ณ ๋ง์ฐฌ๊ฐ์ง๋ก ์ถ๊ฐ์ ์ธ ์ค๋ฒํค๋๋ฅผ ๋ฐ์์ํฌ ์ ์์ต๋๋ค.
derivedStateOf ์์์ ์ ๋ ฌ/๊ทธ๋ฃนํ ๊ฐ์ ๋ฌด๊ฑฐ์ด ๊ณ์ฐ์ ์ํํ๋ฉด, UI ๋ ์ด์ด์์ ๋ถํ์ํ ์ฌ๊ณ์ฐ์ด ๋ฐ๋ณต๋ ์ ์์ต๋๋ค.
์ด๋ด ๋๋ ๊ณ์ฐ์ Compose ๋ฐ์ผ๋ก ์ฎ๊ธฐ๊ณ , Compose๋ ๊ฒฐ๊ณผ๋ง ๊ทธ๋ฆฌ๋๋ก ๋ง๋๋ ๋ฐฉ์์ด ๊ฐ์ฅ ์์ ํฉ๋๋ค.
UiState๋ก ๋ด๋ ค์ฃผ๊ธฐFlow.map {} / StateFlow๋ก ๋ฐ์ดํฐ ๋ณํ ๊ตฌ์ฑDispatchers.Default ๋ฑ ๋ฐฑ๊ทธ๋ผ์ด๋ ์ค๋ ๋์์ ์ฒ๋ฆฌclass MyViewModel(
private val repository: Repository
) : ViewModel() {
val uiState: StateFlow<UiState> =
repository.itemsFlow
.map { items ->
UiState(
grouped = items.groupBy { it.category }
)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = UiState()
)
}
data class UiState(
val grouped: Map<Category, List<Item>> = emptyMap()
)
์ฌ๋ฌ State๋ก๋ถํฐ ํ์๋ ๊ฐ์ด
์์ฃผ ์ฝํ์ง๋ง ์ค์ ๋ณ๊ฒฝ์ ๋๋ฌธ ๊ฒฝ์ฐ์ ํจ๊ณผ์ ์ ๋๋ค.
ํนํ ์คํฌ๋กค ์ํ๋ก๋ถํฐ Boolean ๊ฐ์ ํ์ํ๋ ๊ฒฝ์ฐ,
๋ถํ์ํ recomposition์ ์ค์ด๋ ๋ฐ ํฐ ๋์์ด ๋ฉ๋๋ค.
derivedStateOfย ์์ ๋ฌด๊ฑฐ์ด ์ฐ์ฐ์ ๋ฃ์ผ๋ฉด,์์กด State๊ฐ ๋ฐ๋ ๋๋ง๋ค ์ฌ๊ณ์ฐ ๋น์ฉ์ด ๋์ ๋์ด ์คํ๋ ค ์ฑ๋ฅ์ ์ ์ํฅ์ ์ค ์ ์์ต๋๋ค.
์ด๋ฐ ๊ฒฝ์ฐ ViewModel์ด๋ ๋ฐฑ๊ทธ๋ผ์ด๋ ์ฒ๋ฆฌ๋ก ๋ถ๋ฆฌํ๋ ๊ฒ์ด ์ ์ ํฉ๋๋ค.
์ ๋ฉ๋๋ค.ย
remember๊ฐ ์์ผ๋ฉด ๋ฆฌ์ปดํฌ์ง์ ๋ง๋คยDerivedSnapshotStateย ๊ฐ์ฒด๊ฐ ์๋ก ์์ฑ๋์ด ์บ์๊ฐ ์ฌ๋ผ์ง๋๋ค.์ด์ ๊ฒฐ๊ณผ์ ๋น๊ต ์์ฒด๊ฐ ๋ถ๊ฐ๋ฅํด์ง๋ฏ๋กย
derivedStateOf์ ํต์ฌ ๊ธฐ๋ฅ์ด ๋์ํ์ง ์์ต๋๋ค.ยremember๋ ๊ฐ์ฒด๋ฅผ ์ด๋ ค๋๋ ์ญํ , derivedStateOf๋ ๊ทธ ๊ฐ์ฒด๊ฐ ์ด์์๋ ๋์ ๋น๊ตํ๋ ์ญํ ๋ก ๋์ ๋ฐ๋์ ํจ๊ป ์ฌ์ฉํด์ผ ํฉ๋๋ค.
๋ค์์ ๋งํฌ๋ฅผ ์ฐธ๊ณ ํ์ต๋๋ค.