rememberUpdatedState
공식문서에 따르면
'값이 변경되는 경우 다시 시작되지 않아야하는 효과에서 값 참조에 사용합니다'
이 경우에 rememberUpdatedState를 사용한다는데, 이런 경우가 어떤 경우가 있는지 잘 모르기에 타 사이트에 있는 예제를 참고해보겠습니당.
https://proandroiddev.com/jetpack-compose-side-effects-iii-rememberupdatedstate-c8df7b90a01d
위의 사진과 같이 버튼이 두가지가 존재합니다.
@Composable
fun TwoButtonScreen() {
var buttonColour by remember {
mutableStateOf("Unknown")
}
Column {
Button(
onClick = {
buttonColour = "Red"
}
){...}
Spacer(Modifier.height(24.dp))
Button(
onClick = {
buttonColour = "Black"
}
){...}
Timer(buttonColor = buttonColour)
}
}
Timer Compoasble의 경우 다음과 같습니다.
@Composable
fun Timer(
buttonColour: String
) {
val timerDuration = 10000L
LaunchedEffect(key1 = Unit, block = {
startTimer(timerDuration) {
println("Last pressed was $buttonColour")
}
})
}
suspend fun startTimer(time: Long, onTimerEnd: () -> Unit) {
delay(timeMillis = time)
onTimerEnd()
}
코드를 설명하자면
1. buttonColour가 변경함에 따라 Timer는 recomposition이 일어납니다.
2. 처음 Composition에서 10초후, buttonColour의 값을 출력합니다.
이 컴포저블을 실행했을 때 10초의 딜레이가 끝나기전 buttonColour가 바뀌었다고 봅시다.
delay가 끝난후 최종 출력되는 buttonColour의 값은 어떻게 될까요?
코드 그대로 실행된다면 "UnKnown"이 출력될 것입니다.
왜일까요?
초기 컴포지션에서 buttonColour의 값이 캡처되어 람다에 전달이 되는데, 이 캡쳐된 값이 수정되지 않기 때문입니다.
람다의 캡처란?
람다의 내부에서 외부의 변수에 접근하려고 할 때 값을 final로 복사해오는데, java에서 final의 경우 초기 할당이후, 다시 할당할 수 없습니다.
하지만 객체나 배열에 적용된다면, 다시 할당은 할 수 없지만 여전히 변경은 가능합니다. (우리가 val이여도 mutableList 내부의 값은 변경할 수 있듯이)
다음과 같은 방법으로 우리는 캡쳐된 변수를 수정할 수 있습니다.
1. 키로 buttonColour를 전달한다. 하지만 key의 수정으로 인해 CoroutineScope가 취소되고 재실행되기 때문에, delay가 다시 적용되서 이 방법은 쓰지 않으려고합니다.
2. rememberUpdatedState를 사용한다.
rememberUpdatedState가 무엇인가
@Composable
fun <T> rememberUpdatedState(newValue :T): State<T> = remember{ mutableStateOf(newValue)}.apply{ value = newValue }
다음과 같이 구현이 되어있는데, 이 rememberUpdatedState의 특징은 State 객체를 다시 만들지 않고, 값을 업데이트한다는 점이다.
처음 Composition이후 recomposition될때 remember 내부는 실행되지않고, apply{} 내부만 호출이 된다. 이것이 새로운 객체를 할당하지 않고서 값을 업데이트하는 방법!
아까 작성했던 예시를 수정해보자.
@Composable
fun Timer(
buttonColour: String
) {
val timerDuration = 10000L
val testColor = rememberUpdatedState(buttonColour)
println("startTest1HashCode : ${testColor.hashCode()} test1Color : ${testColor.value}")
LaunchedEffect(key1 = Unit, block = {
startTimer(timerDuration) {
println("Last pressed test1Color was ${testColor.hashCode()} ${testColor.value}")
}
})
}
초기 Composition때 testColor State 객체를 재할당하는 것이 아닌, 내부 값을 수정한 것. 10초가 지나면 변경된 값에 대해서 제대로 출력을 한다.
그렇다면 그냥 remember는?
@Composable
fun <T> rememberTest(newValue: T): State<T> = remember(newValue) {
mutableStateOf(newValue)
}
결국 이 코드는 새로운 State객체들을 생성한다.
buttonColour가 변경되면, 초기 Composition때 생성되고 캡처 되었던 State와는 다른 State 객체들이 생성된거라 같은 값을 가리키지 않는다.
지금까지 rememberUpdatedStatte에 대해 알아보았는데, 실제 적용했던 부분에 대해서 말해보려고한다.
위의 사진을 보면 Notification Group들을 담고 펼처지는 컴포저블을 만들고, SwipeToDismiss할 수 있는 컴포저블을 만들려고 했다.
아주 간략히 소개하자면, LazyColumn에 List<GroupNotiItem>을 아이템으로 넣어주고, GroupNotiItem이 펼처지면, 하위로 SwipeToDismissLockNotiItem들이 보이게 된다.
LazyColumn이 같은 방향으로 중첩이 안되기 때문에 Column안에 For-Loop로 SwipeToDismissItem들을 나열해주고 있다.
이 컴포저블의 매개변수로 Notification과 현재 Notification이 속한 GroupNotification, 그리고 삭제 람다는 List를 넘기고 있다.
접힘/펼침 상태를 GroupLockNotiItem이 들고 있어서, 이 상태가 접힘이라면 Swipe 됐을때, GroupNotification에 속한 SwipeToDismissLockNotiItem들을 전부 삭제해주기 위해서 이러한 구조로 구현을 하게 되었다...
실제 적용했던 코드와 비슷한 테스트 컴포저블을 만들어보았다
@Composable
fun Outer(
list: List<String>,
removeTempList: (List<String>) -> Unit
) {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
items(list, key = { it }) {
SwipeToDismissWithUpdatedState(list = list, removeItem = removeTempList, singleItem = it)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SwipeToDismissWithUpdatedState(
singleItem: String,
list: List<String>,
removeItem: (List<String>) -> Unit
) {
val dismissState = rememberDismissState(confirmStateChange = { dismissValue ->
when (dismissValue) {
DismissValue.Default -> {
false
}
DismissValue.DismissedToStart -> {
println("remove AllList : ${list.joinToString(" ") { it }}")
removeItem(list)
true
}
DismissValue.DismissedToEnd -> {
println(singleItem)
removeItem(listOf(singleItem))
true
}
}
})
SwipeToDismiss(
state = dismissState,
dismissThresholds = { FractionalThreshold(0.25f) },
dismissContent = {
Text(
modifier = Modifier.fillMaxWidth().background(color = androidx.compose.ui.graphics.Color.Blue),
text = singleItem,
textAlign = TextAlign.Center
)
},
background = {
}
)
}
오른쪽으로 밀면 아이템 한개 삭제, 왼쪽으로 밀면 아이템 전체 삭제로 구현했습니다.
로그를 보면 이상하다.
전체삭제를 시도했을 때, 이전에 삭제했던 아이템까지 람다로 넘겨지는) 모습을 볼 수 있습니다.
그 이유는 바로 dismissState의 람다때문입니다.
val dismissState = rememberDismissState(confirmStateChange = { dismissValue ->
when (dismissValue) {
DismissValue.Default -> {
false
}
DismissValue.DismissedToStart -> {
removeItem(list)
true
}
DismissValue.DismissedToEnd -> {
removeItem(listOf(singleItem))
true
}
}
})
왼쪽으로 dismiss를 하면 전달된 리스트를 제거하는데 이미 초기 composition에 아이템이 전부 있는 List가 캡쳐되고, recomposition이 진행되도 rememberDismissState내부에 전달된 list가 업데이트가 되지 않기 때문에 일어나는 일입니다.
위에서의 해결책으로!! rememberUpdatedState를 사용해보겠습니다.
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SwipeToDismissWithUpdatedState(
singleItem: String,
list: List<String>,
removeItem: (List<String>) -> Unit
) {
var newList = rememberUpdatedState(newValue = list)
// confirmchange 람다의 list를 newList로 변경하자
{...
removeItem(newList)
}
}
이번에는 정상적으로 반영되는 모습을 보실수 있습니다.
LaunchedEffect(Unit){}의 역할을 rememDismissState가 했다고 생각해본다면, rememberUpdatedState로 해결할 수 있겠구나라고 알게 되는 이슈였던것 같습니다.
마무리
이 문제 때문에 오랜 시간동안 시간을 많이 들어서, rememerUpdatedState에 대해 알려줬던 [https://jaeryo2357.tistory.com/] 에게 감사합니다.
https://perfectacle.github.io/2019/06/30/java-8-lambda-capturing/
https://proandroiddev.com/jetpack-compose-side-effects-iii-rememberupdatedstate-c8df7b90a01d
'Compose' 카테고리의 다른 글
Jetpack Compose Theme 적용하기 (0) | 2023.06.07 |
---|---|
Compose와 함께 Motion Layout을 사용하여 애니메이션 구현하기 (0) | 2023.05.24 |
SwipeToDismiss Jetpack Compose로 구현하기 (0) | 2023.02.02 |
Jetpack Compose GapBuffer (1) | 2022.10.27 |
Jetpack Compose의 최적화 (0) | 2022.10.13 |