깨알 개념/Android

[Android] Jetpack Compose - StateFlow와 SharedFlow

interfacer_han 2024. 8. 29. 19:54

#1 이전 글

 

[Kotlin] Coroutines Flow - StateFlow

#1 개요 StateFlowA SharedFlow that represents a read-only state with a single updatable data value that emits updates to the value to its collectors. A state flow is a hot flow because its active instance exists independently of the presence of collecto

kenel.tistory.com

StateFlow에 대해 다룬 이전 글에서 이어진다. State가 쓰인 Jetpack Compose 프로젝트를 수정해, State 대신 StateFlow를 사용하게 만들어본다.

 

추가로, SharedFlow도 같이 사용해 본다. StateFlow와 SharedFlow가 나란히 위치하면 각각의 동작이 자연스럽게 비교될 것이고 둘의 미묘한 차이를 알게 될 것이다.

 

#2 수정할 샘플 앱

 

[Android] Jetpack Compose - State 기초

#1 개요 상태 및 Jetpack Compose  |  Android Developers이 페이지는 Cloud Translation API를 통해 번역되었습니다. 상태 및 Jetpack Compose 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고

kenel.tistory.com

위의 게시글의 #7에서, 버튼을 누르면 화면에 표시된 count가 1씩 증가하는 안드로이드 프로젝트를 다운로드 할 수 있다. 이 프로젝트는 화면에 표시될 count를 State로 구현했다. 해당 프로젝트의 코드를 수정해나가는 방식으로 본 게시글을 전개해보겠다.

 

#3 코드 수정 - StateFlow로 변경

#3-1 코드

...

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        setContent {
            ...
        }
    }
}

val count = MutableStateFlow(0)

@Composable
fun ButtonExample(modifierParam: Modifier = Modifier) {
    Button(
        onClick = {
            Log.i("interfacer_han", "Current count value: ${++count.value}")
        },
        modifier = modifierParam
    ) {
        Text(
            text = "Count: ${count.value}",
            fontSize = 40.sp
        )
    }
}

count의 데이터형을 StateFlow로 변경했다. StateFlow는 State의 특성을 지닌 Flow로 이 외에 변경할 점이 없다.

 

#3-2 작동 확인

이전과 동일하게 작동한다.

 

#4 코드 수정 - SharedFlow 추가

#4-1 코드

...

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        setContent {
            ...
        }

        // repeatOnLifecycle을 통해 Lifecycle이 STARTED 상태일 때만 SharedFlow 수집
        lifecycleScope.launch {
            lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
                toastEvent.collect { message ->
                    Toast.makeText(this@MainActivity, message, Toast.LENGTH_SHORT).show()
                }
            }
        }
    }
}

val count = MutableStateFlow(0)
val toastEvent = MutableSharedFlow<String>()

@Composable
fun ButtonExample(modifierParam: Modifier = Modifier) {
    ...

    // rememberCoroutineScope로 '안전한' 코루틴 스코프를 가져온다.
    val coroutineScope = rememberCoroutineScope()

    Button(
        onClick = {
            ...
            coroutineScope.launch {
                toastEvent.emit("${count.value}")
            }
        },
        modifier = modifierParam
    ) {
        ...
    }
}

count는 Composable에 표시하기 위해 StateFlow로 선언했었다. 하지만 버튼 클릭할 때마다 발생하는 토스트 메시지, 그 토스트 메시지를 Flow로 구현하려면 어떤 것을 사용해야 할까? 바로 SharedFlow다. 값을 굳이 저장할 필요가 없이 이벤트를 발생시키기만 하는 경우라면 StateFlow는 자원 낭비에 불과하다.

 

토스트 메시지로 표시할 String을 담는 SharedFlow, 그리고 해당 Flow의 리스너(colllector)를 setContent { ... } 아래 쪽에 선언했다 (lifeCycleScope 및 repeatOnLifecycle에 대한 내용은 여기를 참조하자).

 

#4-2 작동 확인

토스트 메시지가 잘 작동한다.

 

#5 (본 게시글 한정) 사실 SharedFlow는 필요하지 않았다

#5-1 코드

#4-1에서 서술했듯 사용할 필요가 없는 StateFlow는 자원낭비지만, 본 게시글에선 사용할 필요가 있다! 토스트 메시지로 표시할 String의 내용이 StateFlow.value에 종속되어있기 때문이다.

 

그 코드는 아래와 같다.

 

...

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        setContent {
            ...
        }

        // repeatOnLifecycle을 통해 Lifecycle의 STARTED 상태일 때만 StateFlow 수집
        lifecycleScope.launch {
            lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
                count.collect { value ->
                    Toast.makeText(this@MainActivity, value.toString(), Toast.LENGTH_SHORT).show()
                }
            }
        }
    }
}

val count = MutableStateFlow(0)

@Composable
fun ButtonExample(modifierParam: Modifier = Modifier) {
   ... // #3-1과 동일
}

처음부터 이런 코드로 리팩토링하면 되지만, 나는 SharedFlow를 설명하기 위해서 약간의 억지를 부려본 것이다. 물론, 일반적인 경우라면 StateFlow와 SharedFlow를 잘 구분하여 사용해야할 것이다. 

 

#5-2 작동 확인

StateFlow는 SharedFlow와는 달리 초깃값이 존재하며, 따라서 초깃값인 0이 앱 실행과 동시에 collect()되어 토스트 메시지로 표시되는 걸 확인할 수 있다.

 

#6 요약

State와 Flow 둘 다의 특성을 지닌 StateFlow는 다재다능하게 활용된다. 단, 값을 저장할 필요가 없다면 자원낭비다.