[KMM] IOS에서 Flow 사용하기

WonDDak·2023년 7월 7일
0

KMP- Kotlin MultiPlatform

목록 보기
5/12

들어가며

kmm을 이용하여 앱을 만들어 리소스를 공유하여 사용할때 Flow를 사용하는 경우가 있다(많다)
android에서는 문제가 없지만 ,Ios에서 사용하려면 생각되로 되지 않는 경우가 많다.

Flow?

  • 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)
        }
    }
}

Android

다음과 같이 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초마다 정상적으로 로그가 찍힌다.

Ios

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항목까지 타네요.
그런데 매번 이렇게 사용하면 불편하지 않을까요?(저는 불편해요)


CommonFlow

CommmonFlow라는 class를 만들어 사용해 봅시다.

Platform.kt파일 아래에 추가로 작업합니다.

commonMain

interface Platform {
	...
    fun getCommonFlow() = getFlow().toCommonFlow()
}

expect class CommonFlow<T>(flow: Flow<T>): Flow<T>

fun <T> Flow<T>.toCommonFlow() = CommonFlow(this)

AndroidMain

actual class CommonFlow<T> actual constructor(
    private val flow: Flow<T>
) : Flow<T> by flow

안드로이드는 바꿀게 없죠 그냥 받아서 그대로 전달해주면 됩니다.

IosMain

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로 처리했습니다)

정상적으로 로그를 받아 올 수 있네요 ~


CommonStateFlow

flow도 많이 사용하지만 stateFlow또한 많이 사용하지요 애들도 Common형식으로 바꾸어봅시다.

CommonMain

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)

AndroidMain

actual open class CommonStateFlow<T> actual constructor(
    private val flow: StateFlow<T>
) : StateFlow<T> by flow

안드로이드는 위와 동일하게 그대로 넘겨줍니다.

IosMain

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 시켜주면?

##결과

Android

    LaunchedEffect(true) {
      val a= platform.getStateFlow()
 	 a.collect {num ->
  		Log.i("JWH",num.toString())
      }
    }


초기값 -1을 가져오구 그뒤로 1~10을 정상적으로 가져옵니다.

Ios

  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안에서 사용하셔야 합니다.

profile
안녕하세요. 원딱입니다.

0개의 댓글