Non-Compose 상태를 Compose 상태로 변환하여 주는 함수로 람다 스코프 내는 CoroutineScope 이기 때문에 이를 활용할 수 있다.
@Composable
fun <T> produceState(
initialValue: T,
producer: suspend ProduceStateScope<T>.() -> Unit
): State<T> {
val result = remember { mutableStateOf(initialValue) }
LaunchedEffect(Unit) {
ProduceStateScopeImpl(result, coroutineContext).producer()
}
return result
}
내부 코드는 위와 같은데, remember { mutableStateOf(initialValue) } 로 MutableState 를 우선 구현하고 LaunchedEffect 에서 이 값을 업데이트하도록 한다.
LaunchedEffect 의 경우 모든 컴포지션이 완료된 후 실행되므로, initialValue 가 우선 표기된 후 Coroutine 이 실행된다.
1초마다 count state 를 늘려주고, 이를 텍스트로 표기하는 코드가 있다고 해보자.
보통 이는 다음과 같이 나타낼 것이다.
@Composable
fun ProduceStateTest(){
var count by remember { mutableIntStateOf(0) }
Text(text = count.toString())
LaunchedEffect(true) {
while (true){
delay(1000)
count++
}
}
}
위의 코드가 틀린 코드는 아니다.
하지만 count 의 경우 var + MutableState 로 되어있기에 새롭게 수정이 가능 하며 코드가 길어진다면 이 count 객체에 대한 변경이 어디서 이루어지는지 헷갈릴 수 있다.
이 때, produceState 를 통해 State 생산자로 만들어주면 다음과 같이 만들 수 있다.
@Composable
fun ProduceStateTest(){
val count by produceState(initialValue = 0, key1 = true) {
while (true){
delay(1000)
value++
}
}
}
이렇게 설정해준다면 count value 를 변경하는 코드를 바로 확인하기도 쉽고, val + State 로 설정되어 있기에 다른 코드에서 값 수정이 불가능해진다.
BroadCastReciever 를 사용하여 안드로이드 시스템과 상호작용한다고 생각해보자.
@Composable
fun ProduceStateTest(){
val context = LocalContext.current
val battery by produceState<Int?>(initialValue = null) {
val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BATTERY_CHANGED) {
val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
value = (level * 100 / scale.toFloat()).toInt()
}
}
}
context.registerReceiver(receiver, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
}
Text(text = battery.toString())
}
위처럼 구현되는 경우 ProduceStateTest() Composable 이 종료된 이후에도 브로드캐스트리시버에 대한 참조가 남아있어 메모리 누수가 발생할 수 있다.
이런 상황들을 대비하기 위해 produceState 에서는 awaitDispose 라는 함수를 제공한다.
이를 사용하면 Composition leave 상태에서 참조 상태를 해제하여 메모리 누수를 방지할 수 있다.
코드를 올바르게 수정하면 다음과 같이 된다.
@Composable
fun ProduceStateTest(){
val context = LocalContext.current
val battery by produceState<Int?>(initialValue = null) {
val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BATTERY_CHANGED) {
val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
value = (level * 100 / scale.toFloat()).toInt()
}
}
}
context.registerReceiver(receiver, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
awaitDispose { context.unregisterReceiver(receiver) }
}
Text(text = battery.toString())
}