iT邦幫忙

2021 iThome 鐵人賽

DAY 16
0

Keyword: Coroutine,Flow


前面說了這麼多有關於Coroutine Leak所帶來的風險,但是iOS不像Android有那麼完善的支援,畢竟Apple也沒有理由要支持Kotlin與Coroutine.

不過,大部分使用到Coroutine的耗時工作通常是有關於資料的,而有關於資料的部分在KMM內就是統一由Kotlin所管理,我們可以藉由自己實作一小套Coroutine的管理來達成類似的效果.

當然,是沒辦法像Android的版本具有”使用區塊和作用域放在一起“的好處,畢竟這樣設計需要官方支援,但是放得靠近一點還是做得到的.

DataState

在正式開始前,先修改一個小地方.

因為我們沒有特別設計,所以現在剛進去App的頁面時,會是一片空白的.等到資料回傳回來,才會刷新畫面.中間的流程只能讓使用者癡癡等待,感覺好像當機了,使用者體驗不好.

讓我們對資料層再做一層封裝,讓資料除了純粹的List以外,還能攜帶目前的狀況.

由於這是給雙平台都共用的邏輯,我們把這個封裝DataState放在commonMain底下.

data class DataState(
    val data: List<CafeResponseItem>? = null,
    val exception: String? = null,
    val empty: Boolean = false,
    val loading: Boolean = false
)

除了原本的List資料外,還有發生錯誤時的exception,回傳資料數目為0時的empty,以及正在讀取資料的loading,這些就非常夠用了.

Flow

現在資料在使用時,會是先顯示讀取中,再展示資料的內容,這個過程至少會回傳兩次結果,可以利用到Coroutine的Flow應用了,Flow可以回傳suspend function的多個結果,而外部使用觀察者模式來使用這些資料.

讓我們將之前的FetchCafesFromNetwork封裝進flow之中.

fun refreshCafes(cityName:String): Flow<DataState> = flow{//建立一個DataState的flow
        emit(DataState(loading = true))//開始讀取,狀態為loading
        val networkCafeDataState:DataState = fetchCafesFromNetwork(cityName)
        emit(networkCafeDataState)//讀取完成,狀態為可以展示Data
    }

    suspend fun fetchCafesFromNetwork(cityName: String): DataState {
        return try {
            val cafeResponseItemList =  ktorApi.fetchCafeFromApi(cityName)
            if(cafeResponseItemList.isEmpty()){
                DataState(empty = true)//回傳為0 狀態為空內容
            } else {
                DataState(cafeResponseItemList)
            }
        } catch (e: Exception) {
            println(e.message)
            DataState(exception = "Can't fetch data from Network")
						//發生錯誤 狀態為exception
        }

    }

iOS的MainScope

來建立一條iOS專用的MainScope,讓大部分的工作都在上面執行.Android因為Coroutine會主動把MainScope建立起來(就是最重要的UI Thread),所以不需要再額外進行這個部分.

在shared的iosMain資料夾下,建立一個物件,命名iOSMainScope,是需要一個CoroutineContext的CoroutineScope,然後內建一個exeptionHandler,這個物件在Coroutine發生錯誤的時候會執行,比較好追蹤.最後在這個ScopeDestroy時把job取消,避免job Leak

class iOSMainScope (private val mainContext: CoroutineContext) : CoroutineScope {
    override val coroutineContext: CoroutineContext
        get() = mainContext + job + exceptionHandler

    internal val job = SupervisorJob()
    private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
        throwable.printStackTrace()
        showError(throwable)
    }

    // TODO: Some way of exposing this to the caller without trapping a reference and freezing it.
    private fun showError(t: Throwable) {
        println(t.message)
    }

    fun onDestroy() {
        job.cancel()
    }
}

ViewModel

之前我們在iOS的原生內寫ViewModel,而現在我們將這層ViewModel下放,讓shared內的物件來擔當這個角色.

首先,先加入剛建立的那些物件.

class iOSCafeViewModel(private val onDataState: (DataState) -> Unit) {
    private val scope = iOSMainScope(Dispatchers.Main)//使用Main 作為Coroutine的環境
    private val dataRepository  =  DataRepository()//資料源
    private val cafeFlow: MutableStateFlow<DataState> = MutableStateFlow(
        DataState(loading = true)//對iOS 提供的資料
    )
}

然後放入需要的功能實作

fun observeCafeData(cityName:String) {
        scope.launch {
            dataRepository.refreshCafes(cityName)
                .collect { dataState ->
                    if (dataState.loading) {
                        val temp = cafeFlow.value.copy(loading = true)
                        cafeFlow.value = temp
                    } else {
                        cafeFlow.value = dataState//更新收到的數據
                    }
                }
        }

        scope.launch {
            cafeFlow.collect { dataState ->
                onDataState(dataState)//根據目前狀態,選擇處理方式
            }
        }
    }

最後提供一個方法,讓iOS在被回收時呼叫,避免Coroutine Leak

fun onDestroy() {
        scope.onDestroy()
    }

整個Class就像這樣

class iOSCafeViewModel(private val onDataState: (DataState) -> Unit) {
    private val scope = iOSMainScope(Dispatchers.Main)
    private val dataRepository  =  DataRepository()
    private val cafeFlow: MutableStateFlow<DataState> = MutableStateFlow(
        DataState(loading = true)
    )

    fun observeCafeData(cityName:String) {
        scope.launch {
            dataRepository.refreshCafes(cityName)
                .collect { dataState ->
                    if (dataState.loading) {
                        val temp = cafeFlow.value.copy(loading = true)
                        cafeFlow.value = temp
                    } else {
                        cafeFlow.value = dataState
                    }
                }
        }

        scope.launch {
            cafeFlow.collect { dataState ->
                onDataState(dataState)
            }
        }
    }

    fun onDestroy() {
        scope.onDestroy()
    }
}

明天我們會在iOS上使用這個新建立的物件


上一篇
Day 15:完了,我的Coroutine漏出去了.Coroutine的Leak與結構化
下一篇
Day 17: swiftUI與Coroutine強強聯手,迸出新滋味.
系列文
挑戰 Kotlin Multiplatform Mobile 跨平台開發,透過共同的Kotlin模組同時打造iOS與Android應用!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言