Day 16:自己动手,丰衣足食.IOS的Coroutine管理

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 6] - 阵列与物件的混合使用

>>:  深不可测的海 - Regular Expression

Day8-安装Kind要在docker之後

从上一章了解各种K8s的特点,在这章将会教学如何安装Kind。 由於其利用docker的特性,会比使...

DAY 26 Django 简易入门教学(三)-建立 Django 专案与 APP

建立 Django 专案 Django 建立专案的指令相当简单: django-admin star...

纯Javascript,使用new Date()制作date picker

刚好有一道面试题目,不能使用 input type=date 或任何现有套件,要做出类似Datepi...

学习笔记:一起进入 PixiJS 的世界 (四)

建立文字物件 使用new建构出PIXI.Text()文字物件,并将文字的内容做为第一个参数传入,再将...

终於要来开赛了~第一天

为什麽要选这个主题??? 原因就是我在某某补习班的主修就是这个~ 付出学费当然就是要验收阿~ Goo...