鑒於我文章越寫越長,偏離了我原本想讓人輕鬆閱讀的感覺,決定寫個新手實用,以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
)
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>()
}
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
恩恩都很簡單的概念,記得去文檔翻翻,這裡就不贅述了