day12 轻松一下,用 coroutine 接个 restful api

鉴於我文章越写越长,偏离了我原本想让人轻松阅读的感觉,决定写个新手实用,以coroutine接个restful api的例子,如果你已经很会接了,这篇完全可以跳过

文档有一页我觉得新手友善同时又特别重要的,如何最佳化的使用coroutine,将变数作为类别的建构子应该很习以为常了,今天就只是把它换成dispatcher而已,简单吧

文档推荐的最佳做法

而这种依赖注入模式可以简化测试难度,同时也避免打错的情况

阿,讲完了,对新手而言,光看我讲也不知道是甚麽,我这边带个接api的code好了

我会用这支api拿到这个

{
  "userId": 1,
  "id": 1,
  "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
  "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}

工具引入

build.gradle

// retrofit
    implementation "com.squareup.retrofit2:retrofit:2.9.0"
    implementation "com.squareup.retrofit2:converter-gson:2.9.0"
    
//viewModelScope
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1"

//coroutine
    def coroutine_version = "1.5.1"
    //coroutine
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutine_version"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutine_version"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:$coroutine_version"

正文开始

我会跳过fragment和viewModel的介绍,如果你对这部分还不理解的,建议先从这边开始,之後再回来看

首先,建立一个data class,和回传资料相符,有需要的可以序列化

data class Post(
    val userId:Int,
    val id:Int,
    val title:String,
    val body:String
)

retrofit常规用法

object NetworkService {
    private const val BASE_URL = "https://jsonplaceholder.typicode.com/"
    
    private val rtf = Retrofit.Builder()
        .addConverterFactory(GsonConverterFactory.create())
        .baseUrl(BASE_URL)
        .build()
    val retrofit:Connect = rtf.create(Connect::class.java)
}

interface Connect{
    @GET("posts/1")
    suspend fun getOneost(): Post
}

对新手而言很重要的一步,封装一下response

sealed class ResResult<T> {
    data class Success<T>(val data: T) : ResResult<T>()
    data class Fail<T>(
            val message: String? = null,
            val throwable: Throwable? = null
       ) : ResResult<T>()
}

seal class用法参考自这里

repository是MVVM架构中,可选的其中一层,非常重要的一点,你不应该在Repository中创建coroutine,因为repo仅做为一个物件存在,并没有lifecycle,这也表示在这里创建的coroutine除非特别处理,否则他将不会被清除,也有可能导致work leak,比较好的做法是在viewModel开启coroutine,这里用withContext( dispatcher )可以用

class SomeRepo(
    private val netConnect: NetworkService,
    private val defaultDispatcher: CoroutineDispatcher = Dispatchers.IO
){
    suspend fun repoData() = withContext(defaultDispatcher) {
        netConnect.retrofit.getOneost()
    }
}
//全域方法
suspend fun <T> safeApiCall(
    apiCall: suspend () -> T
): ResResult<T> {
    return try {
            ResResult.Success(apiCall.invoke())
        } catch (throwable: Throwable) {
            when (throwable) {
                is IOException -> ResResult.Fail("${throwable.message} IOException : Network error !!",throwable)
                is HttpException -> {
                    ResResult.Fail(throwable.message ?: "",throwable)
                }
                else -> {
                    ResResult.Fail(throwable.message,throwable)
                }
            }
        }
}
class RestViewModel(val repo:SomeRepo): ViewModel() {

    fun getData(){
        viewModelScope.launch {
            Timber.d("call safeApi")
            when ( val postResult = safeApiCall { repo.repoData() } ){
                is ResResult.Success ->{
                    Timber.d(postResult.data.body )
                }
                is ResResult.Fail ->{
                    Timber.e(postResult.message)
                    Timber.e(postResult.throwable)
                }
            }
        }
    }
}

来解释一下,为甚麽要封装呢? try/catch不是本来就能抓到Exception了吗?

其实原因很简单,透过封装
我们可以定义受限的类别结构,以when来说,差异在於需不需要写else的分支,尽管可能永远用不到,而ide通常会强迫我们写个预设行为,结果就是我们或许会写成

is Success ->// do something
is Fail -> //do something
else -> throw Exception("Unknown expression")

但这还不是最麻烦的,今天如果你要加个LOADING的类别,但你忘记要到某个when 判断式,loading的状态就会走到else,又得花时间debug

另一方面,我们也能更容易的包装错误讯息,针对不同的错误做处理,进而让用户知道发生什麽事情,以及透过提示告诉他们该如何解决,比如说,网路没开启之类的小问题

封装的其他概念自己上网查

其他什麽在viewModel开启coroutine

viewModelScope.launch{

}

model层公开suspend fun 或flow等等

class repo(){
    suspend fun getOneDtaa(){
        //
    }
    fun getDataFlow():Flow<Post>{
        //
    }
}

不要公开可变类型等等

//viewModel
private val _post : MutableLiveData<List<Post>> by lazy {
    MutableLiveData<List<Post>>().apply {  }
}
val post: LiveData<List<Post>>
    get() = _post

恩恩都很简单的概念,记得去文档翻翻,这里就不赘述了

连结整理

必看

文档推荐的最佳做法
coroutine简介


<<:  [DAY13]影片及音档

>>:  Day 12. slate × Interfaces × Data-Model

Day 26 - HBuilderX 与 Native.js API 读取图片

Day 26 - HBuilderX 与 Native.js API 读取图片 在 Day 25 -...

[Day 02] 为什麽要用 Kubernetes?

为甚麽 「需要」 Kubernetes? 一个走完开发流程之後所产出的软件应用程序(或称系统),都会...

Day17 参加职训(机器学习与资料分析工程师培训班),Python程序设计

练习使用selenium来登入FB from selenium import webdriver d...

this指向who

我们很常会在function中看到使用this这个关键字。但它是甚麽,要怎麽用?听说它的判断方法很麻...

python ModuleNotFoundError

python中引用不同文件夹下面的函数的时候,使用了__init__.py依然没有用,後来发现原因:...