๊นจ์•Œ ๊ฐœ๋… ๐Ÿ“‘/Android

[Android] Coroutines Flow - Jetpack Compose์—์„œ ์‚ฌ์šฉํ•˜๊ธฐ

interfacer_han 2024. 8. 6. 02:31

Coroutines Flow ๊ฒŒ์‹œ๊ธ€ ์‹œ๋ฆฌ์ฆˆ


#1 ๊ฐœ์š”

 

[Kotlin] Coroutines Flow - ๊ธฐ์ดˆ

#1 Coroutines Flow#1-1 ๊ฐœ์š” FlowFlow An asynchronous data stream that sequentially emits values and completes normally or with an exception. Intermediate operators on the flow such as map, filter, take, zip, etc are functions that are applied to the upst

kenel.tistory.com

์œ„์—์„œ ๋‹ค๋ฃฌ Coroutines Flow๋ฅผ Jetpack Compose์— ์ ์šฉ์‹œ์ผœ๋ณธ๋‹ค. ์—ฌ๊ธฐ์„œ ๋งํ•˜๋Š” '์ ์šฉ'์ด๋ž€, ๋‹จ์ˆœํ•œ ์‚ฌ์šฉ์ด ์•„๋‹ˆ๋ผ Flow ๊ฐ์ฒด๋ฅผ State๋กœ์„œ ๋‹ค๋ฃฌ๋‹ค๋Š” ์˜๋ฏธ๋‹ค.
 

#2 ์ˆ˜์ •ํ•  ์ƒ˜ํ”Œ ์ฝ”๋“œ

...

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

        // State ์„ ์–ธ
        val count = mutableStateOf(1)

        // State์˜ ๊ฐ’ ๋ณ€๊ฒฝ
        CoroutineScope(Dispatchers.Default).launch {
            for (i in 1..5) {
                delay(1000)
                count.value = i
            }
        }

        setContent {
            Box(
                modifier = Modifier.fillMaxSize(),
            ) {
                TextExample(
                    count.value, // State Hoisting (https://kenel.tistory.com/186)
                    Modifier.align(Alignment.Center)
                )
            }
        }
    }
}

@Composable
fun TextExample(
    currentCount: Int,
    modifier: Modifier = Modifier,
) {
    Text(
        text = "Count: $currentCount",
        fontSize = 40.sp,
        modifier = modifier
    )
}

State๊ฐ€ ์“ฐ์ธ ๊ฐ„๋‹จํ•œ ์ฝ”๋“œ๋‹ค. ์ด ์•ฑ์„ ์‹คํ–‰์‹œํ‚ค๋ฉด, Count: 1์œผ๋กœ ์‹œ์ž‘ํ•ด Count: 5๊นŒ์ง€ ํ‘œ์‹œํ•˜๋Š” Text๊ฐ€ ํ™”๋ฉด ์ค‘์•™์— ๋ณด์ผ ๊ฒƒ์ด๋‹ค. ์ด์ œ ์ด ์ฝ”๋“œ์™€ ๊ฐ™์€ ๋™์ž‘์„ ํ•˜๋Š” ์ฝ”๋“œ๋ฅผ Flow๋ฅผ ํ™œ์šฉํ•ด์„œ ์งœ๋ณด๊ฒ ๋‹ค.
 

#3 Flow๋กœ ๊ฐ™์€ ๋™์ž‘ ๊ตฌํ˜„ํ•˜๊ธฐ

#3-1 ์ฝ”๋“œ

...

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

        // Flow ์„ ์–ธ
        val countFlow = flow {
            for (i in 1..5) {
                delay(2000)
                emit(i)
            }
        }

        setContent {
            Log.i("interfacer_han", "Recomposition ...")
            Box(
                modifier = Modifier.fillMaxSize(),
            ) {
                // collectAsState()
                val count = countFlow.collectAsState(initial = 1)

                TextExample(
                    count.value, // State Hoisting (https://kenel.tistory.com/186)
                    Modifier.align(Alignment.Center)
                )
            }
        }
    }
}

@Composable
fun TextExample(...) { ... }

Flow๋ฅผ ํ™œ์šฉํ•ด #2์˜ ์ฝ”๋“œ๋ฅผ ๋‹ค์‹œ ์งœ๋ฉด ์ด๋Ÿฐ ์ฝ”๋“œ๊ฐ€ ๋œ๋‹ค. Flow๋ฅผ ๋งŒ๋“ค๊ณ , collect()๋ฅผ ์ˆ˜ํ–‰ํ•œ๋‹ค. ์ด ๋•Œ Jetpack Compose์™€ ๊ด€๋ จ๋œ ์ž‘์—…์—์„œ๋Š”, ๋‹ค์‹œ ๋งํ•ด UI์— ๊ด€๋ จ๋œ ์ž‘์—…์—์„œ๋Š” collect()๊ฐ€ ์•„๋‹Œ collectAsState()๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. collectAsState()๋Š” State ํƒ€์ž…์„ ๋ฐ˜ํ™˜ํ•˜๋Š” collect()๋‹ค. ๋‹จ์ˆœํžˆ ๋ฐ˜ํ™˜ํ˜• ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ, collect()๋œ ๊ฐ’์„ UI์— ํ‘œ์‹œํ•˜๊ธฐ ์œ„ํ•œ ์—ฌ๋Ÿฌ๊ฐ€์ง€ ์•”์‹œ์ ์ธ ๋™์ž‘๋“ค์ด ์ •์˜๋˜์–ด์žˆ๋Š” ํ•จ์ˆ˜๋‹ค.
 
๊ทธ๋ฆฌ๊ณ  count๊ฐ€ TextExample์—์„œ ์ฐธ์กฐ๋˜๊ณ  ์žˆ์œผ๋ฏ€๋กœ, ๋‹ค์‹œ ๋งํ•ด ๋ฐ์ดํ„ฐํ˜•์ด State์ธ ํ”„๋กœํผํ‹ฐ๊ฐ€ UI์—์„œ ์ฐธ์กฐ๋˜๊ณ  ์žˆ์œผ๋ฏ€๋กœ, count.value์˜ ๊ฐ’์ด ๋ณ€ํ•˜๋ฉด Recomposition์ด ์•”์‹œ์ ์œผ๋กœ ์ˆ˜ํ–‰๋  ๊ฒƒ์ด๋‹ค (์ด ๊ฒŒ์‹œ๊ธ€์˜ #4 ์ฐธ์กฐ).
 

#3-2 remember์— ๋Œ€ํ•œ ์˜๋ฌธ์ ๊ณผ ์„ค๋ช…

#3-1์˜ ์ฝ”๋“œ๋ฅผ ๋ณด๋ฉด ์ด๋Ÿฐ ์ƒ๊ฐ์ด ๋“ค ์ˆ˜ ์žˆ๋‹ค. Recomposition์ด ์ผ์–ด๋‚œ๋‹ค๋Š” ๊ฒƒ์€ setContent { ... } ์•ˆ์— ์œ„์น˜ํ•œ count๋„ ์ดˆ๊ธฐํ™”๋œ๋‹ค๋Š” ๋ง์ด ์•„๋‹Œ๊ฐ€? ์ฆ‰, countFlow.collectAsState(initial = 1)๋ฅผ remember { ... }๋กœ ๊ฐ์‹ธ Recomposition์— ๋Œ€ํ•œ ๋ฐฉ์–ด๋ง‰์„ ์ณ๋‘์ง€ ์•Š์œผ๋ฉด count ์ดˆ๊ธฐํ™”, Recomposition, count ์ดˆ๊ธฐํ™”, Recomposition, count ์ดˆ๊ธฐํ™”, ...์˜ ๋ฌดํ•œ ๋ฃจํ”„๊ฐ€ ์ผ์–ด๋‚˜๋Š” ๊ฒŒ ์•„๋‹Œ๊ฐ€?

ํ•˜์ง€๋งŒ ์•„๋‹ˆ๋‹ค. ์™œ๋ƒํ•˜๋ฉด collectAsState()๋Š” State object ๊ทธ ์ž์ฒด๊ฐ€ ์•„๋‹ˆ๋ผ, State object๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” Getter ํ•จ์ˆ˜์ด๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. State object๊ฐ€ Get๋˜๋Š” ์›๋ณธ ๋ฐ์ดํ„ฐ๋Š” setContent { ... } ๋ฐ–์— ์žˆ๋Š” countFlow์˜ ์ธ์Šคํ„ด์Šค์— ์œ„์น˜ํ•œ๋‹ค. ๊ทธ๋ž˜์„œ remember { ... }๋กœ ๊ฐ์‹ธ์ง€ ์•Š๋Š” ๊ฒƒ์ด๋‹ค. ์ดํ•ด๊ฐ€ ๊ฐ€์ง€ ์•Š๋Š”๋‹ค๋ฉด State object์˜ Remembering์— ๋‹ค๋ฃฌ ์ด ๊ฒŒ์‹œ๊ธ€์„ ์ฐธ์กฐํ•˜์ž.
 
๊ทธ๋ ‡๋‹ค๋ฉด ๋งŒ์•ฝ countFlow ํ”„๋กœํผํ‹ฐ๊ฐ€ setContent { ... } ๋ธ”๋ก ๋‚ด๋ถ€์— ์œ„์น˜ํ•œ๋‹ค๋ฉด,
 

...

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            // Flow ๊ฐ์ฒด
            val countFlow = remember {
                flow {
                    for (i in 1..5) {
                        delay(2000)
                        emit(i)
                    }
                }
            }

            Box(
                modifier = Modifier.fillMaxSize(),
            ) {
                // collectAsState()
                val count = countFlow.collectAsState(initial = 1)

                TextExample(
                    count.value, // State Hoisting (https://kenel.tistory.com/186)
                    Modifier.align(Alignment.Center)
                )
            }
        }
    }
}

@Composable
fun TextExample(...) { ... }

์ด๋ ‡๊ฒŒ countFlow ํ”„๋กœํผํ‹ฐ์— ๋Œ€์ž…๋˜๋Š” Flow ์ธ์Šคํ„ด์Šค๋ฅผ remember { ... }๋กœ ๊ฐ์‹ธ์ฃผ์–ด์•ผ ํ•œ๋‹ค. ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด ๋ฌดํ•œ ๋ฃจํ”„์— ๋น ์ง€๊ณ  ๋งŒ๋‹ค. ๋ฌผ๋ก  ์ด ๊ฒฝ์šฐ์—๋„ collectAsState()๋Š” ๊ทธ์ € Getter๋กœ์„œ์˜ ์—ญํ• ์„ ์ˆ˜ํ–‰ํ•  ๋ฟ์ด๋‹ค.
 

#3-3 ์„ค๋ช…์— ๋Œ€ํ•œ ์ฆ๊ฑฐ

#3-2์˜ ์ฝ”๋“œ์—์„œ ๋งŒ์•ฝ Flow ์ธ์Šคํ„ด์Šค๋ฅผ remember { ... }๋กœ ๊ฐ์‹ธ์ง€ ์•Š๋Š”๋‹ค๋ฉด? ์ด๋ ‡๊ฒŒ ๋ฌดํ•œ ๋ฃจํ”„๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.
 

๋ฐ˜๋ฉด, remember { ... }๋กœ ๊ฐ์‹ธ๋ฉด, Count: 1์œผ๋กœ ์‹œ์ž‘ํ•ด Count: 5๊นŒ์ง€ ํ‘œ์‹œํ•˜๋Š” Text๊ฐ€ ์˜๋„ํ•œ ๋Œ€๋กœ ์ž˜ ํ‘œ์‹œ๋œ๋‹ค.
 

#4 collectAsState()๋Š” ๋™๊ธฐ ์ฝ”๋“œ๋‹ค

// collect()
public abstract suspend fun collect(
    collector: FlowCollector<T>
): Unit

// collectAsState()
@Composable
public fun <T : R, R> Flow<T>.collectAsState(
    initial: R,
    context: CoroutineContext = EmptyCoroutineContext
): State<R>

collectAsState()์—๋Š” ํ•œ ๊ฐ€์ง€ ๋” ์งš๊ณ  ๋„˜์–ด๊ฐ€์•ผ ํ•˜๋Š” ๊ฒƒ์ด ์žˆ๋‹ค. ๋ฐ”๋กœ collect()๊ฐ€ ๋น„๋™๊ธฐ ์ฝ”๋“œ๋กœ์„œ suspend ํ‚ค์›Œ๋“œ๊ฐ€ ๋ถ™์–ด์žˆ๋Š” ๋ฐ˜๋ฉด, collectAsState()๋Š” ๋™๊ธฐ ์ฝ”๋“œ๊ณ  ๋”ฐ๋ผ์„œ suspend ํ‚ค์›Œ๋“œ๋„ ๋ณด์ด์ง€ ์•Š๋Š”๋‹ค๋Š” ์ ์ด๋‹ค. ๋‹น์—ฐํ•˜๊ฒ ์ง€๋งŒ, collectAsState() ๋™์ž‘์€ ๋น„๋™๊ธฐ์ ์ด์ง€ ์•Š์„ ์ˆ˜ ์—†๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์‹ค์ œ๋กœ๋„ ๋‚ด๋ถ€์ ์ธ ๋™์ž‘์€ ๋น„๋™๊ธฐ์ ์œผ๋กœ ์ž‘๋™ํ•œ๋‹ค. ํ•˜์ง€๋งŒ ๊ฒ‰์œผ๋กœ๋Š” State๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ํ‰๋ฒ”ํ•œ ํ•จ์ˆ˜์˜ ๋ชจ์–‘์ด๋‹ค. ๊ฒฐ๋ก ์ ์œผ๋กœ collectAsState()๋Š” ๋™๊ธฐ ์ฝ”๋“œ์ธ ์ฒ™์„ํ•˜๊ณ  ์žˆ๋‹ค๋Š” ๋ง์ด๋‹ค. ๊ทธ๋ž˜์•ผ Compose ๋Ÿฐํƒ€์ž„์ด ์ˆ˜ํ–‰ํ•˜๋Š” Recomposition์˜ ๊ธฐ์ œ(UI ํ‘œ์‹œ)์— ์Šค๋ฉฐ๋“ค์–ด ํ”„๋กœ๊ทธ๋ž˜๋จธ๊ฐ€ ์‰ฝ๊ฒŒ ๋‹ค๋ฃฐ ์ˆ˜ ์žˆ๊ฒŒ ๋˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.
 
์‰ฌ์šด ์‚ฌ์šฉ์„ฑ ์™ธ์—๋„ collectAsState()๊ฐ€ ๋™๊ธฐ ์ฝ”๋“œ์ธ ์ฒ™์„ ํ•ด์•ผํ•˜๋Š” ์ด์œ ๋Š” ๋ฐ”๋กœ ์ดˆ๊นƒ๊ฐ’์˜ ์กด์žฌ๋‹ค. ์ด ์ดˆ๊นƒ๊ฐ’์€ collectAsState() ๋‚ด๋ถ€์—์„œ flow์˜ emit()์„ ๊ธฐ๋‹ค๋ฆฌ(๋น„๋™๊ธฐ ์ฝ”๋“œ์˜ ์ •์˜๋Š” '๊ธฐ๋‹ค๋ฆผ'์˜ ์กด์žฌ๋‹ค)๋Š” ๋™์•ˆ, ์‚ฌ์šฉ์ž์—๊ฒŒ ํ‘œ์‹œํ•  ๊ธฐ๋ณธ๊ฐ’์˜ ์—ญํ• ์„ ์ˆ˜ํ–‰ํ•œ๋‹ค. ์ด๋ ‡๊ฒŒ ๋ฐ์ดํ„ฐ๊ฐ€ collectAsState()๋กœ ์ „๋‹ฌ๋˜๋Š”๋™์•ˆ, UI๊ฐ€ ๋นˆ ํ™”๋ฉด์œผ๋กœ ํ‘œ์‹œ๋˜๋Š” ๊ฒƒ์„ ์˜ˆ๋ฐฉํ•œ๋‹ค. ๋ฐ์ดํ„ฐ๊ฐ€ ์˜ค๋ฉด ์˜ค๋Š” ๋Œ€๋กœ ๋ฐ›์•„ Recomposition์„ ์ˆ˜ํ–‰ํ•˜๋ฉด ๋˜๊ณ , ๋ฐ์ดํ„ฐ๊ฐ€ ์˜ค๊ธฐ ์ „๊นŒ์ง€๋Š” ์ดˆ๊นƒ๊ฐ’์„ ์ฆ‰์‹œ ๋ฐ˜ํ™˜ํ•˜๋ฏ€๋กœ collectAsState()๋Š” ๊ฒ‰์œผ๋กœ ๋ด์„œ๋Š” ๋™๊ธฐ ์ฝ”๋“œ์ธ ๊ฒƒ์ด๋‹ค.
 

#5 ViewModel์—์„œ Flow ์‚ฌ์šฉํ•˜๊ธฐ

#5-1 ์ด์ „ ๊ฒŒ์‹œ๊ธ€

 

[Android] Jetpack Compose - ViewModel์—์„œ State ์‚ฌ์šฉํ•˜๊ธฐ

#1 ๊ฐœ์š”Jetpack Compose์—์„œ ViewModel์„ ์‚ฌ์šฉํ•ด๋ณธ๋‹ค. Jetpack Compose๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ์ „ํ†ต์ ์ธ ๋ฐฉ์‹์—์„œ์˜ ViewModel๊ณผ ํฌ๊ฒŒ ๋‹ค๋ฅผ ๊ฒŒ ์—†๋‹ค. Jetpack Compose์— ViewModel์„ ๊ตฌํ˜„ํ•จ์œผ๋กœ์จ State Hoisting ํŒจํ„ด์„ ๊ทน๋Œ€ํ™”์‹œ

kenel.tistory.com

์œ„ ๊ฒŒ์‹œ๊ธ€์— ๊ธฐ๋ฐ˜ํ•ด #3-1์˜ ์ฝ”๋“œ์™€ ๋˜‘๊ฐ™์€ ๋™์ž‘์„ ํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ViewModel์„ ์ด์šฉํ•ด ์ž‘์„ฑํ•ด๋ณธ๋‹ค.
 

#5-2 ๋ชจ๋“ˆ ์ˆ˜์ค€ build.gradle ์ˆ˜์ •

plugins {
    ...
}

android {
    ...
}

dependencies {

    ...

    // ViewModel (Compose)
    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.3")
}

 

#5-3 ์ฝ”๋“œ - MainViewModel

// package com.example.collectasstate

import androidx.lifecycle.ViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flow

class MainViewModel : ViewModel() {
    val numberFlow = flow {
        for (i in 1..5) {
            delay(1000)
            emit(i)
        }
    }
}

 

#5-4 ์ฝ”๋“œ - MainActivity

...

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val viewModel = viewModel<MainViewModel>()

            Box(
                modifier = Modifier.fillMaxSize(),
            ) {
                // collectAsState()
                val count = viewModel.countFlow.collectAsState(initial = 1)

                TextExample(
                    count.value, // State Hoisting (https://kenel.tistory.com/186)
                    Modifier.align(Alignment.Center)
                )
            }
        }
    }
}

@Composable
fun TextExample(...) { ... }

 

#6 ์ž‘๋™ ํ™•์ธ

#6-1 Log๋ฅผ ์ถ”๊ฐ€ํ•œ ์ฝ”๋“œ

...

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

        // Flow ์„ ์–ธ
        val countFlow = flow {
            for (i in 1..5) {
                delay(2000)

                Log.i("interfacer_han", "---\nemit(): $i")
                emit(i)
            }
        }

        setContent {
            Log.i("interfacer_han", "Recomposition ...")

            Box(
                modifier = Modifier.fillMaxSize(),
            ) {
                // collectAsState()
                val count = countFlow.collectAsState(initial = 1)
                Log.i("interfacer_han", "Collected value: ${count.value}")

                TextExample(
                    count.value, // State Hoisting (https://kenel.tistory.com/186)
                    Modifier.align(Alignment.Center)
                )
            }
        }
    }
}

@Composable
fun TextExample(...) { ... }

์ž‘๋™ ํ™•์ธ์„ ์œ„ํ•ด #3-1์˜ ์ฝ”๋“œ์— Log๋ฅผ ์ถ”๊ฐ€ํ–ˆ๋‹ค.
 

#6-2 ์Šคํฌ๋ฆฐ์ƒท

์ž˜ ์ž‘๋™ํ•œ๋‹ค.
 

#6-3 ๋กœ๊ทธ ๋ฉ”์‹œ์ง€

Recomposition ...
Collected value: 1
---
emit(): 1
---
emit(): 2
Recomposition ...
Collected value: 2
---
emit(): 3
Recomposition ...
Collected value: 3
---
emit(): 4
Recomposition ...
Collected value: 4
---
emit(): 5
Recomposition ...
Collected value: 5

flow๊ฐ€ 1์„ emit()ํ–ˆ์„ ๋•Œ Recomposition์ด ์ผ์–ด๋‚˜์ง€ ์•Š์€ ์ด์œ ๋Š” State.value๊ฐ€ ์ด๋ฏธ 1์„ ๊ฐ€์ง€๊ณ  ์žˆ์—ˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ์ฆ‰, State.value๊ฐ€ ๋ณ€ํ•˜์ง€ ์•Š์•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. Compose ๋Ÿฐํƒ€์ž„์€ Stateํ˜• ํ”„๋กœํผํ‹ฐ์˜ ๊ฐ’์„ ๊ธฐ์–ตํ•œ๋‹ค. ๊ทธ๋ž˜์„œ ๋ฐœ์ƒ๋œ ์˜๋„์  ๋ˆ„๋ฝ์ด๋‹ค.
 

#7 ์š”์•ฝ

collectAsState()๋Š” ๋™๊ธฐ์ฝ”๋“œ์ธ ์ฒ™์„ ํ•˜๋Š” Getter ํ•จ์ˆ˜๋‹ค.
 

#8 ์™„์„ฑ๋œ ์•ฑ

 

android-practice/jetpack-compose/CollectAsState at master ยท Kanmanemone/android-practice

Contribute to Kanmanemone/android-practice development by creating an account on GitHub.

github.com

๊นƒํ—ˆ๋ธŒ์— ์˜ฌ๋ฆฐ ์•ˆ๋“œ๋กœ์ด๋“œ ํ”„๋กœ์ ํŠธ๋Š” ViewModel์„ ํ™œ์šฉํ•œ #5์˜ ์ฝ”๋“œ๋กœ ์ž‘์„ฑํ–ˆ๋‹ค.