iT邦幫忙

2021 iThome 鐵人賽

DAY 12
0
Mobile Development

解鎖kotlin coroutine的各種姿勢-新手篇系列 第 12

day12 輕鬆一下,用 coroutine 接個 restful api

  • 分享至 

  • xImage
  •  

鑒於我文章越寫越長,偏離了我原本想讓人輕鬆閱讀的感覺,決定寫個新手實用,以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簡介


上一篇
day11 Kotlin coroutine 花生什麼事?
下一篇
day13 Kotlin coroutine channel操作
系列文
解鎖kotlin coroutine的各種姿勢-新手篇30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言