Keyword: Coroutine,Flow
前面说了这麽多有关於Coroutine Leak所带来的风险,但是iOS不像Android有那麽完善的支援,毕竟Apple也没有理由要支持Kotlin与Coroutine.
不过,大部分使用到Coroutine的耗时工作通常是有关於资料的,而有关於资料的部分在KMM内就是统一由Kotlin所管理,我们可以藉由自己实作一小套Coroutine的管理来达成类似的效果.
当然,是没办法像Android的版本具有”使用区块和作用域放在一起“的好处,毕竟这样设计需要官方支援,但是放得靠近一点还是做得到的.
在正式开始前,先修改一个小地方.
因为我们没有特别设计,所以现在刚进去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,这些就非常够用了.
现在资料在使用时,会是先显示读取中,再展示资料的内容,这个过程至少会回传两次结果,可以利用到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,让大部分的工作都在上面执行.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()
}
}
之前我们在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上使用这个新建立的物件
>>: 深不可测的海 - Regular Expression
从上一章了解各种K8s的特点,在这章将会教学如何安装Kind。 由於其利用docker的特性,会比使...
建立 Django 专案 Django 建立专案的指令相当简单: django-admin star...
刚好有一道面试题目,不能使用 input type=date 或任何现有套件,要做出类似Datepi...
建立文字物件 使用new建构出PIXI.Text()文字物件,并将文字的内容做为第一个参数传入,再将...
为什麽要选这个主题??? 原因就是我在某某补习班的主修就是这个~ 付出学费当然就是要验收阿~ Goo...