鉴於我文章越写越长,偏离了我原本想让人轻松阅读的感觉,决定写个新手实用,以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
恩恩都很简单的概念,记得去文档翻翻,这里就不赘述了
>>: Day 12. slate × Interfaces × Data-Model
Day 26 - HBuilderX 与 Native.js API 读取图片 在 Day 25 -...
为甚麽 「需要」 Kubernetes? 一个走完开发流程之後所产出的软件应用程序(或称系统),都会...
练习使用selenium来登入FB from selenium import webdriver d...
我们很常会在function中看到使用this这个关键字。但它是甚麽,要怎麽用?听说它的判断方法很麻...
python中引用不同文件夹下面的函数的时候,使用了__init__.py依然没有用,後来发现原因:...