kmm을 이용하여 앱을 만들어 리소스를 공유하여 사용할때 Flow를 사용하는 경우가 있다(많다)
android에서는 문제가 없지만 ,Ios에서 사용하려면 생각되로 되지 않는 경우가 많다.
shared 의 build.gradle에 코루틴을 추가해주자
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.2")
새 KMM 프로젝트를 만들고 기본으로 생성되는 Platform 인터페이스에 다음을 추가해보자
1 부터 10까지 emit을 하며 사이에 0.5초의 딜레이를 가지는 flow이다
interface Platform {
val name: String
fun getFlow() : Flow<Int> = flow {
(1..10).forEach {
delay(500)
emit(it)
}
}
}
다음과 같이 MainActiviy를 수정하고 실행시키면
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val platform = getPlatform()
setContent {
MyApplicationTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
LaunchedEffect(true) {
platform.getFlow().collect { num ->
Log.i("JWH",num.toString())
}
}
GreetingView(platform.name)
}
}
}
}
}
0.5초마다 정상적으로 로그가 찍힌다.
struct ContentView: View {
let platform = Platform_iosKt.getPlatform()
var body: some View {
Text(platform.name)
.onAppear {
platform.getFlow()
}
}
}
collect를 할려고 확인해보면
Kotlinx_coroutines_coreFlowCollector를 collecter로 받아서 사용 할수 있다.
completionHandler있나 유무로 async 인지 구분된다.
보통은 다음과 같이 Kotlinx_coroutines_coreFlowCollector를 상속받는 Collector 클래스를 만들어 사용한다.
class Collector<T> : Kotlinx_coroutines_coreFlowCollector {
let callback:(T) -> Void
init(callback: @escaping (T) -> Void) {
self.callback = callback
}
func emit(value: Any?, completionHandler: @escaping (Error?) -> Void) {
callback(value as! T)
completionHandler(nil)
}
}
T는 제너릭입니다.
struct ContentView: View {
let platform = Platform_iosKt.getPlatform()
var body: some View {
Text(platform.name)
.onAppear {
platform.getFlow().collect(collector:Collector<Int> { value in
print("JWH-Task1",value)
}) { error in
print("JWH-Task1",error ?? "err")
}
Task {
do {
try await platform.getFlow().collect(collector:Collector<Int> { value in
print("JWH-Task2",value)
})
} catch {
print("JWH-Task2", "err")
}
}
}
}
}
두가지 형태 completionHandler있나 없냐로 받을수 있는데. 편하신대로 쓰시면 됩니다.
잘 출력되고 Task1의 경우 error항목까지 타네요.
그런데 매번 이렇게 사용하면 불편하지 않을까요?(저는 불편해요)
CommmonFlow라는 class를 만들어 사용해 봅시다.
Platform.kt파일 아래에 추가로 작업합니다.
interface Platform {
...
fun getCommonFlow() = getFlow().toCommonFlow()
}
expect class CommonFlow<T>(flow: Flow<T>): Flow<T>
fun <T> Flow<T>.toCommonFlow() = CommonFlow(this)
actual class CommonFlow<T> actual constructor(
private val flow: Flow<T>
) : Flow<T> by flow
안드로이드는 바꿀게 없죠 그냥 받아서 그대로 전달해주면 됩니다.
actual open class CommonFlow<T> actual constructor(
private val flow: Flow<T>
) : Flow<T> by flow {
fun collect(
onCollect: (T) -> Unit
): DisposableHandle {
val job = CoroutineScope(Dispatchers.Main).launch {
flow.collect(onCollect)
}
return DisposableHandle { job.cancel() }
}
}
새로운 collect함수를 정의해줍니다.
그러면 위에서 만든 Collector없이도 다음과 같이 사용가능합니다.
struct ContentView: View {
let platform = Platform_iosKt.getPlatform()
var body: some View {
Text(platform.name)
.onAppear {
platform.getCommonFlow().collect { value in
let num = value as! Int
print("JWH",num)
}
}
}
}
다만 넘어오는 값이 Optinal을 붙혀 오기떄문에 nil처리를 알아서 해주심 됩니다.
(여기서는 1~10이 확실히 오는걸 알고있으므로 as! Int로 처리했습니다)
정상적으로 로그를 받아 올 수 있네요 ~
flow도 많이 사용하지만 stateFlow또한 많이 사용하지요 애들도 Common형식으로 바꾸어봅시다.
interface Platform {
...
suspend fun getStateFlow() = getCommonFlow().stateIn(
CoroutineScope(Dispatchers.Main),
SharingStarted.WhileSubscribed(1000L),
-1
).toCommonStateFlow()
}
expect open class CommonStateFlow<T>(flow: StateFlow<T>) : StateFlow<T>
fun <T> StateFlow<T>.toCommonStateFlow() = CommonStateFlow(this)
actual open class CommonStateFlow<T> actual constructor(
private val flow: StateFlow<T>
) : StateFlow<T> by flow
안드로이드는 위와 동일하게 그대로 넘겨줍니다.
actual open class CommonStateFlow<T> actual constructor(
private val flow: StateFlow<T>
) : CommonFlow<T>(flow), StateFlow<T> {
override val replayCache: List<T>
get() = flow.replayCache
override val value: T
get() = flow.value
override suspend fun collect(collector: FlowCollector<T>) = flow.collect(collector)
}
IOS의 경우 CommonFlow를 추가로 받아줍시다.
그리고 받아온 flow를 다 override 시켜주면?
LaunchedEffect(true) {
val a= platform.getStateFlow()
a.collect {num ->
Log.i("JWH",num.toString())
}
}
초기값 -1을 가져오구 그뒤로 1~10을 정상적으로 가져옵니다.
struct ContentView: View {
let platform = Platform_iosKt.getPlatform()
var body: some View {
Text(platform.name)
.onAppear {
Task {
do {
try await platform.getStateFlow().collect { value in
let num = value as! Int
print("JWH",num)
}
} catch {
}
}
}
}
}
동일하게 초기값 -1을 가져오구 그뒤로 1~10을 정상적으로 가져옵니다.
만 stateflow특성상 suspend 함수이다 보니 위와같이 Task안에서 사용하셔야 합니다.