Android版kotlin協(xié)程入門(mén)(五):kotlin協(xié)程的網(wǎng)絡(luò)請(qǐng)求封裝

本章前言

本章節(jié)中除了會(huì)對(duì)協(xié)程做講解外勘天,不會(huì)對(duì)其他引入的框架做講解备恤。文章是基于用戶(hù)已經(jīng)對(duì)這些框架有一定的入門(mén)基礎(chǔ)上编整,對(duì)與框架如何結(jié)合kotlin協(xié)程的使用做一個(gè)引導(dǎo)。整個(gè)篇幅會(huì)有些長(zhǎng)元镀,我們會(huì)在結(jié)合使用的同時(shí)绍填,做一些架構(gòu)上的封裝,也是為了方便后續(xù)在實(shí)戰(zhàn)的時(shí)候栖疑,大家能更方便讨永、直觀(guān)的理解代碼。

筆者也只是一個(gè)普普通通的開(kāi)發(fā)者遇革,架構(gòu)上的設(shè)計(jì)不一定合理卿闹,大家可以自行吸收文章精華,去糟粕萝快。

kotlin協(xié)程的使用封裝

在上一章節(jié)中锻霎,我們已經(jīng)了解了協(xié)程在ActivityFragment揪漩、Lifecycle旋恼、Viewmodel的基礎(chǔ)使用,以及如何簡(jiǎn)單的自定義一個(gè)協(xié)程奄容。本章節(jié)中冰更,我們主要是做一些基礎(chǔ)的封裝工作。我們將在上一章節(jié)的內(nèi)容基礎(chǔ)上昂勒,引入DataBinding蜀细、LiveDataFlow等做一些基礎(chǔ)封裝戈盈。比如:Base類(lèi)的定義奠衔,協(xié)程的使用封裝,常用擴(kuò)展函數(shù)等塘娶。

我們先引入本章節(jié)所使用到的相關(guān)庫(kù):

    // Kotlin
    implementation "org.jetbrains.kotlin:kotlin-stdlib:1.4.32"
    // 協(xié)程核心庫(kù)
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3"
    // 協(xié)程Android支持庫(kù)
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3"

    implementation "androidx.activity:activity-ktx:1.2.2"
    implementation "androidx.fragment:fragment-ktx:1.3.3"

    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1"
    implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.1"
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"

     // ok http
    implementation "com.squareup.okhttp3:okhttp:4.9.0"
    implementation 'com.squareup.okhttp3:logging-interceptor:4.9.0'

    // retrofit
    implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
    implementation "com.squareup.retrofit2:converter-scalars:$retrofit_version"
    implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"

復(fù)制代碼

現(xiàn)在我們就可以開(kāi)始做一些基礎(chǔ)的封裝工作归斤,同時(shí)在app的bulid.gradle文件中開(kāi)啟dataBinding的使用

android {
    buildFeatures {
        dataBinding  = true
    }
   //省略...
}
復(fù)制代碼

文章中基于DataBinding的使用,可以參考【封裝DataBinding讓你少寫(xiě)萬(wàn)行代碼】

ViewModel的使用可以參考【ViewModel的日常使用封裝】

這兩篇文章是為了本章節(jié)專(zhuān)門(mén)寫(xiě)的擴(kuò)展性閱讀血柳。

最近因?yàn)殡娔X炸機(jī)了官册,信號(hào)輸出不穩(wěn)定生兆,開(kāi)始以為是顯卡壞了难捌,折騰了幾天還是沒(méi)整好膝宁,最后發(fā)現(xiàn)是主板被腐蝕導(dǎo)致線(xiàn)路故障,當(dāng)前用的主板停產(chǎn)很久了根吁,最后只能找個(gè)兼容的员淫,等了幾天才到貨,最終也導(dǎo)致本章節(jié)稍微延后了幾天击敌。電子產(chǎn)品太脆弱了介返,一定要注意防摔防磕碰沃斤、防腐蝕圣蝎!廢話(huà)不多說(shuō),下面進(jìn)入我們今天的正題衡瓶。

協(xié)程的常用環(huán)境

在實(shí)際的開(kāi)發(fā)過(guò)程中徘公,我們經(jīng)常需要把耗時(shí)處理移到非主線(xiàn)程上執(zhí)行,等耗時(shí)操作異步完成以后哮针,再回到主線(xiàn)程上刷新界面关面。基于這些需求十厢,我們大致可以把使用協(xié)程的環(huán)境分為下面五種環(huán)境:

  • 網(wǎng)絡(luò)請(qǐng)求
  • 回調(diào)處理
  • 數(shù)據(jù)庫(kù)操作
  • 文件操作
  • 其他耗時(shí)操作

下面我們首先對(duì)網(wǎng)絡(luò)請(qǐng)求這塊進(jìn)行處理等太。目前市面上大多數(shù)APP的在處理網(wǎng)絡(luò)請(qǐng)求時(shí)候,都是使用的RxJava結(jié)合Retrofit蛮放、OkHttp進(jìn)行網(wǎng)絡(luò)請(qǐng)求處理缩抡。我們最終的目的也是使用協(xié)程結(jié)合RetrofitokHttp進(jìn)行網(wǎng)絡(luò)請(qǐng)求處理包颁。

我們?cè)谶@里只是針對(duì)Retrofit缝其、OkHttp結(jié)合協(xié)程ViewModel徘六、LiveData使用講解内边,如果需要了解RetrofitokHttp的原理,可以看看其他作者的原理分解文章待锈。

協(xié)程在網(wǎng)絡(luò)請(qǐng)求下的封裝及使用

為了演示效果漠其,筆者在萬(wàn)維易源申請(qǐng)了一面免費(fèi)的天氣API,我們使用的接口地址:

http[s]://route.showapi.com/9-2?showapi_appid=替換自己的值&showapi_sign=替換自己的值 
復(fù)制代碼

此接口返回的通用數(shù)據(jù)格式,其中showapi_res_body返回的json內(nèi)容比較多竿音,筆者從中挑選了我們主要關(guān)注的幾個(gè)字段:

參數(shù)名稱(chēng) 類(lèi)型 描述
showapi_res_body String 消息體的JSON封裝和屎,所有應(yīng)用級(jí)的返回參數(shù)將嵌入此對(duì)象 。
showapi_res_code int 查看錯(cuò)誤碼
showapi_res_error String 錯(cuò)誤信息的展示
{
    "showapi_res_error":"",
    "showapi_res_code":0,
    "showapi_res_body":{
        "time":"20210509180000", //預(yù)報(bào)發(fā)布時(shí)間
        "now":{
            "wind_direction":"西風(fēng)", //風(fēng)向
            "temperature_time":"01:30", //獲得氣溫的時(shí)間
            "wind_power":"0級(jí)", //風(fēng)力
            "aqi":"30", //空氣指數(shù)春瞬,越小越好
            "sd":"40%", //空氣濕度
            "weather_pic":"http://app1.showapi.com/weather/icon/day/00.png", //天氣小圖標(biāo)
            "weather":"晴", //天氣
            "rain":"0.0", //降水量(mm)
            "temperature":"15" //氣溫
        }
    }
}
復(fù)制代碼

當(dāng)然我們還需要一個(gè)接收數(shù)據(jù)的對(duì)象柴信,為了避免和其他庫(kù)容易弄混淆,我們命名為CResponse宽气,這個(gè)結(jié)構(gòu)大家都很熟悉:

data class CResponse<T>(
   @SerializedName("showapi_res_code")
   val code: Int,
   @SerializedName("showapi_res_error")
   val msg: String? = null,
   @SerializedName("showapi_res_body")
   val data: T
)
復(fù)制代碼

由于A(yíng)PI返回的字段名稱(chēng)實(shí)在是不符合筆者的胃口随常,而且用起來(lái)也不美觀(guān)潜沦。所以筆者通過(guò)Gson的注解SerializedName將屬性進(jìn)行重命名。我們?cè)趯?shí)際開(kāi)發(fā)中常常也會(huì)遇到這種問(wèn)題绪氛,同樣可以通過(guò)這種方法進(jìn)行處理唆鸡。

data class Weather(
   val now: WeatherDetail,
   val time: String
)

data class WeatherDetail(
   val aqi: String,
   val rain: String,
   val sd: String,
   val temperature: String,
   @SerializedName("temperature_time")
   val temperatureTime: String,
   val weather: String,
   @SerializedName("weather_pic")
   val weatherPic: String,
   @SerializedName("wind_direction")
   val windDirection: String,
   @SerializedName("windPower")
   val windPower: String
)
復(fù)制代碼

然后我們創(chuàng)建一下okHttpRetrofit枣察。在Retrofit2.6版本以后我們不再需要引入Retrofitcoroutine-adapter適配器庫(kù)争占,我們直接使用即可:

object ServerApi {
   val service: CoroutineService by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
       build()
   }

   private fun build():CoroutineService{
       val retrofit = Retrofit.Builder().apply {
           baseUrl(HttpConstant.HTTP_SERVER)
           client(OkHttpClientManager.mClient)
           addConverterFactory(ScalarsConverterFactory.create())
           addConverterFactory(GsonConverterFactory.create())
       }.build()
      return  retrofit.create(CoroutineService::class.java)
   }
}

object HttpConstant {
   internal val HTTP_SERVER = "https://route.showapi.com"
}
復(fù)制代碼
object OkHttpClientManager {

   val mClient: OkHttpClient by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
      buildClient()
  }

  private fun buildClient(): OkHttpClient {
      val logging = HttpLoggingInterceptor()
      logging.level = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE
      return OkHttpClient.Builder().apply {
          addInterceptor(CommonInterceptor())
          addInterceptor(logging)
          followSslRedirects(true)
      }.build()
  }
}
復(fù)制代碼

由于我們?cè)谡{(diào)用的天氣API接口的時(shí)候showapi_appidshowapi_sign必傳的值,所以我們?cè)黾恿艘粋€(gè)CommonInterceptor攔截器來(lái)統(tǒng)一處理:

class CommonInterceptor : Interceptor {

   @Throws(IOException::class)
   override fun intercept(chain: Interceptor.Chain): Response {
       val oldRequest = chain.request()
       val httpUrl = oldRequest.url
       val host = httpUrl.host
       if (HttpConstant.HTTP_SERVER != host ) {
           return chain.proceed(oldRequest)
       }
       val urlBuilder = httpUrl.newBuilder()
       //這里填寫(xiě)自己申請(qǐng)appid和sign
       urlBuilder.addQueryParameter("showapi_appid", SHOW_API_APPID)
       urlBuilder.addQueryParameter("showapi_sign", SHOW_API_SIGN) 

       val request = oldRequest
           .newBuilder()
           .url(urlBuilder.build())
           .build()
       return chain.proceed(request)
   }
}
復(fù)制代碼

為了方便快速演示序目,筆者從請(qǐng)求的參數(shù)列表中只抽取了一個(gè)用來(lái)演示臂痕,接下來(lái)我們定義我們?cè)谡?qǐng)求需要通過(guò)Retrofit使用的接口CoroutineService

請(qǐng)求參數(shù) 類(lèi)型 描述
area String 要查詢(xún)的地區(qū)名稱(chēng) 。
interface CoroutineService {
  @FormUrlEncoded
  @POST("/9-2")
  suspend fun getWeather(
      @Field("area") area: String
  ): CResponse<Weather>
復(fù)制代碼

可以看到我們?cè)谑褂?code>Retrofit結(jié)合協(xié)程使用時(shí)猿涨,我們只需要在函數(shù)前增加suspend關(guān)鍵字就可以刻蟹,同時(shí)返回結(jié)果可以直接定義為,我們需要從請(qǐng)求結(jié)果中解析出來(lái)的數(shù)據(jù)對(duì)象嘿辟,而不再是像以前一樣定義為Call<T>舆瘪。

到此為止,我們基于基礎(chǔ)數(shù)據(jù)的定義已經(jīng)結(jié)束了红伦,下面我們將正式進(jìn)入我們今天的主題英古。為了更加清晰的理解,筆者這里不會(huì)采用直接一步到位的方式昙读。那樣可能會(huì)有很多人閱讀理解起來(lái)有困難召调。筆者將會(huì)對(duì)請(qǐng)求過(guò)程進(jìn)行一步一步的封裝,這里需要一點(diǎn)耐心蛮浑。

我們先創(chuàng)建一個(gè)Repository來(lái)請(qǐng)求數(shù)據(jù):

class WeatherRepository {
    suspend fun getWeather(
        area: String
    ): CResponse<Weather>{
        return ServerApi.service.getWeather(area)
    }
}
復(fù)制代碼

同時(shí)在創(chuàng)建一個(gè)MainViewModel來(lái)使用Repository

class MainViewModel(private val repository: WeatherRepository):ViewModel() {

    private val _weather:MutableLiveData<Weather> = MutableLiveData()
    val mWeather: LiveData<Weather> = _weather

    fun getWeather( area: String){
        requestMain {
           val result =  repository.getWeather(area)
            _weather.postValue(result.data)
        }
    }
}
復(fù)制代碼

現(xiàn)在我們就可以在MainActivity中創(chuàng)建MainViewModel來(lái)調(diào)用方法獲取天氣數(shù)據(jù)唠叛。我們?cè)趧?chuàng)建ViewModel對(duì)象的時(shí)候不再使用 ViewModelProviders.of(this).get(MainViewModel::class.java) 這種方式。而是使用activity-ktx庫(kù)中的viewModels方法去創(chuàng)建:

public inline fun <reified VM : ViewModel> ComponentActivity.viewModels(
    noinline factoryProducer: (() -> Factory)? = null
): Lazy<VM> {
    val factoryPromise = factoryProducer ?: {
        defaultViewModelProviderFactory
    }
    return ViewModelLazy(VM::class, { viewModelStore }, factoryPromise)
}
復(fù)制代碼

這個(gè)方法需要我們傳入一個(gè)Factory沮稚,我們自己定義一個(gè)實(shí)現(xiàn):

object ViewModelUtils {
    fun provideMainViewModelFactory(
    ): MainViewModelFactory {
        return MainViewModelFactory(MainRepository())
    }
}

class MainViewModelFactory(
    private val repository: MainRepository
) : ViewModelProvider.Factory {
    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return MainViewModel(repository) as T
    }
}
復(fù)制代碼

接下來(lái)我們?cè)?code>MainActivity使用艺沼,通過(guò)使用ViewModelUtils獲取MainViewModelFactory,然后使用viewModels進(jìn)行創(chuàng)建我們需要的viewModel對(duì)象:

class MainActivity : BaseActivity<ActivityMainBinding>() {
    private val viewModel by viewModels<MainViewModel> {
        ViewModelUtils.provideMainViewModelFactory()
    }
    override fun initObserve() {
        viewModel.mWeather.observe(this) {
            mBinding.contentTv.text = "$it"
        }
    }

    override fun ActivityMainBinding.initBinding() {
        this.mainViewModel = viewModel
    }
}
復(fù)制代碼

initObserve是我們?cè)?code>BaseActivity中定義的抽象方法。我們只在activity_main.xml簡(jiǎn)單定義了一個(gè)Textview來(lái)顯示數(shù)據(jù)蕴掏,雖然在XML中引入了mainViewModel障般,但是為演示過(guò)程,我們沒(méi)有使用DataBinding直接做數(shù)據(jù)綁定盛杰。而在實(shí)際開(kāi)發(fā)中應(yīng)該是使用DataBinding直接在XML中數(shù)據(jù)綁定挽荡。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>
        <variable
            name="mainViewModel"
            type="com.carman.kotlin.coroutine.request.viewmodel.MainViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".ui.MainActivity">

        <TextView
            android:id="@+id/content_tv"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="這里顯示獲取到的數(shù)據(jù)"
            android:textColor="@color/black"
            android:textSize="18sp"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
復(fù)制代碼

我們成功的請(qǐng)求到數(shù)據(jù),并且顯示在我們界面上即供。但是有個(gè)問(wèn)題上我們現(xiàn)在的請(qǐng)求是沒(méi)有做異常處理的《猓現(xiàn)在我們處理下請(qǐng)求過(guò)程中的異常:

    fun getWeather(area: String){
        requestMain {
           val result = try {
               repository.getWeather(area)
           } catch (e: Exception) {
              when(e) {
                   is UnknownHostException -> {
                       //...
                   }
                    //...  各種需要單獨(dú)處理的異常
                   is ConnectException -> {
                       //...
                   }
                   else ->{
                       //...
                   }
               }
               null
           }
            _weather.postValue(result?.data)
        }
    }
復(fù)制代碼

這種做法雖然處理了異常,但是非常丑陋逗嫡,而且我們需要在每一個(gè)請(qǐng)求地方寫(xiě)的時(shí)候青自,那將會(huì)是一個(gè)噩夢(mèng)般的詛咒株依。 接下來(lái)將會(huì)是我們的重點(diǎn)內(nèi)容,筆者將會(huì)封裝出三種形式的調(diào)用性穿,開(kāi)始的時(shí)候?qū)?yīng)的場(chǎng)景使用即可。

高階函數(shù)方式

這個(gè)時(shí)候我們需要?jiǎng)?chuàng)建一個(gè)BaseRepository來(lái)進(jìn)行封裝處理,我們通過(guò)onSuccess獲取成功的結(jié)果雷滚,通過(guò)onError來(lái)處理針對(duì)此次請(qǐng)求的特有異常需曾,以及通過(guò)onComplete來(lái)處理執(zhí)行完成的操作:

open class BaseRepository {

    suspend inline fun <reified T : Any> launchRequest(
        crossinline block: suspend () -> CResponse<T>,
        noinline onSuccess:  ((T?) -> Unit)? = null,
        noinline onError:  ((Exception)-> Unit) ? = null,
        noinline onComplete:  (() -> Unit)? = null ){
        try {
            val response = block()
            onSuccess?.invoke(response?.data)
        } catch (e: Exception) {
            e.printStackTrace()
            when (e) {
                is UnknownHostException -> {
                    //...
                }
                //...  各種需要單獨(dú)處理的異常
                is ConnectException -> {
                    //...
                }
                else -> {
                    //...
                }
            }
            onError?.invoke(e)
        }finally {
            onComplete?.invoke()
        }
    }
}
復(fù)制代碼

這個(gè)時(shí)候我們?cè)傩薷囊幌?code>WeatherRepository中的getWeather方法,我們需要通過(guò)launchRequest來(lái)包裹一下請(qǐng)求就可以:

    suspend fun getWeather(
        area: String,
        onSuccess:  (Weather?) -> Unit,
        onError:  (Exception) -> Unit,
        onComplete: () -> Unit,
    ){
        launchRequest({
            ServerApi.service.getWeather(area)
        }, onSuccess,onError, onComplete)
    }
復(fù)制代碼

然后我們修改一下MainViewModel中的getWeather方法祈远,我們?cè)谔幚懋惓5奈恢锰幚泶舜谓涌谔赜械漠惓<纯纱敉颍瑫r(shí)可以在請(qǐng)求結(jié)束后做一些收尾工作:

    fun getWeather(area: String) {
        requestMain {
            repository.getWeather(area, {
                _weather.postValue(it)
            }, {
                it.printStackTrace()
                Log.d(Companion.TAG, "異常提示處理")
            }, {
                Log.d(TAG, "請(qǐng)求結(jié)束,處理收尾工作")
            })
        }
    }
復(fù)制代碼

同時(shí)除第一個(gè)執(zhí)行請(qǐng)求的參數(shù)外车份,后面三個(gè)參數(shù)都可以傳入為空實(shí)現(xiàn)谋减。避免在不需要處理成功,異常扫沼,執(zhí)行完成等操作的時(shí)候出爹,出現(xiàn)這種影響美觀(guān)的代碼。假如我們通過(guò)sendData服務(wù)器發(fā)送一個(gè)數(shù)據(jù)缎除,這個(gè)數(shù)據(jù)是否處理成功我們不需要關(guān)心严就,這個(gè)時(shí)候我們就可以如下操作:

    fun sendData(data: String) {
        requestMain {
            repository.launchRequest({
                repository.sendData(data)
            })
        }
    }
復(fù)制代碼

我們?cè)倩剡^(guò)頭來(lái)看看launchRequest方法,我們?cè)谔幚碚?qǐng)求返回的結(jié)果時(shí)候直接就返回response器罐。但是實(shí)際開(kāi)發(fā)中我們一般在請(qǐng)求接口返回?cái)?shù)據(jù)的時(shí)候梢为,是需要判斷接口數(shù)據(jù)狀態(tài)code值是成功的時(shí)候才能返回?cái)?shù)據(jù)。

我們本例中這個(gè)狀態(tài)值是0轰坊。這個(gè)時(shí)候我們需要處理一下增加一個(gè)處理response的方法.我們?cè)傩薷囊幌?code>launchRequest方法:

    suspend inline fun <reified T : Any> launchRequest(
        crossinline block: suspend () -> CResponse<T>,
        noinline onSuccess: ((T?) -> Unit)? = null,
        noinline onError: ((Exception) -> Unit)? = null,
        noinline onComplete: (() -> Unit)? = null
    ) {
        try {
            val response = block()
            when (response.code) {
                HttpConstant.OK -> {
                    val isListType = T::class.isSubclassOf(List::class)
                    if (response.data == null && isListType) {
                        onSuccess?.invoke(Collections.EMPTY_LIST as? T)
                    } else {
                        onSuccess?.invoke(response?.data)
                    }
                }
                else -> onError?.invoke(CException(response))
            }
        } catch (e: Exception) {
            e.printStackTrace()
            when (e) {
                is UnknownHostException -> {
                }
                //...  各種需要單獨(dú)處理的異常
                is ConnectException -> {
                }
                else -> {
                }
            }
            onError?.invoke(e)
        } finally {
            onComplete?.invoke()
        }
    }
復(fù)制代碼

可以看到我們?cè)谔幚?code>response的時(shí)候铸董,我們先通過(guò)判斷返回的詩(shī)句類(lèi)型是否為List集合類(lèi)型。如果是集合類(lèi)型且數(shù)據(jù)返回了一個(gè)null的時(shí)候肴沫,我們就嘗試把一個(gè)的空集合轉(zhuǎn)換為結(jié)果粟害。

 val isListType = T::class.isSubclassOf(List::class)
 if (response.data == null && isListType) {
     onSuccess?.invoke(Collections.EMPTY_LIST as? T)
 } else {
     onSuccess?.invoke(response?.data)
 }
復(fù)制代碼

多狀態(tài)函數(shù)返回值方式

上面的封裝方式我們是通過(guò)kotlin的高階函數(shù)去實(shí)現(xiàn)的。假如我們想直接通過(guò)請(qǐng)求結(jié)果的時(shí)候颤芬,再結(jié)合其他請(qǐng)求處理數(shù)據(jù)通知界面刷新的時(shí)候我磁,上面就顯得很麻煩,而且好像又走到的無(wú)限嵌套的坑里驻襟。

這個(gè)時(shí)候我們就需要直接通過(guò)函數(shù)返回值來(lái)處理《峒瑁現(xiàn)在我們首先的創(chuàng)建一個(gè)DataResult來(lái)封裝一下返回結(jié)果,我們將返回的數(shù)據(jù)分成成功或者失敗兩種:

sealed class DataResult<out T> {
    data class Success<out T>(val data: T) : DataResult<T>()
    data class Error(val exception: Exception) : DataResult<Nothing>()
}
復(fù)制代碼

然然后在創(chuàng)建一個(gè)launchRequestForResult把之前的launchRequest代碼拷貝過(guò)來(lái)稍作修改:

    suspend inline fun <reified T : Any> launchRequestForResult(
        noinline block: suspend () -> CResponse<T>
    ): DataResult<T> {
        return try {
            val response = block()
            if (0 == response.code) {
                val isListType = T::class.isSubclassOf(List::class)
                if (response.data == null && isListType) {
                    DataResult.Success(Collections.EMPTY_LIST as? T) as DataResult<T>
                } else {
                    DataResult.Success(response.data)
                }
            } else {
                DataResult.Error(CException(response))
            }
        } catch (e: Exception) {
            when (e) {
                is UnknownHostException -> {
                }
                //...  各種需要單獨(dú)處理的異常
                is ConnectException -> {
                }
                else -> {
                }
            }
            DataResult.Error(e)
        }
    }
復(fù)制代碼

我們?cè)?code>WeatherRepository中再增加getWeather方法沉衣,通過(guò)launchRequestForResult來(lái)處理請(qǐng)求:

    suspend fun getWeather(area: String): DataResult<Weather> {
        return launchRequestForResult {
            ServerApi.service.getWeather(area)
        }
    }
復(fù)制代碼

然后我們同時(shí)我們也在MainViewModel中增加一個(gè)getWeatherForResult方法郁副,這個(gè)時(shí)候我們就可以按我們的常規(guī)的編寫(xiě)代碼順序處理結(jié)果:

    fun getWeatherForResult(area: String) {
        requestMain {
            val result = repository.getWeather(area)
            when(result){
                is   DataResult.Success ->{
                    _weather.postValue(result.data)
                }
                is   DataResult.Error ->{
                    Log.d(TAG, "${(result?.exception}")
                }
            }
        }
    }
復(fù)制代碼

當(dāng)然,這種方式處理起來(lái)還是相對(duì)有些繁瑣豌习,因?yàn)楫?dāng)我們有多個(gè)請(qǐng)求是存谎,我們需要寫(xiě)多個(gè)when來(lái)判斷結(jié)果拔疚。那如果我們也不想寫(xiě)這些模板代碼又該如何處理呢

直接返回值的方式

這個(gè)時(shí)候我們就需要在launchRequestForResult的基礎(chǔ)上進(jìn)一步的處理:

    suspend inline fun <reified T : Any> launchRequest(
        crossinline block: suspend () -> CResponse<T>
    ): T? {
        return try {
            block()
        } catch (e: Exception) {
            e.printStackTrace()
            when (e) {
                is UnknownHostException -> {
                }
                //...  各種需要單獨(dú)處理的異常
                is ConnectException -> {
                }
                else -> {
                }
            }
            throw e
        }?.run {
            if (0 == code) {
                val isListType = T::class.isSubclassOf(List::class)
                return if (data == null && isListType) {
                    Collections.EMPTY_LIST as? T
                } else {
                    data
                }
            } else {
                throw CException(this)
            }
        }
    }
復(fù)制代碼

因?yàn)榭紤]到實(shí)際開(kāi)發(fā)環(huán)境中,我們還是可能需要在外部處理異常提示的所以在這里還是通過(guò)throw重新拋出異常既荚。如果外部不想處理非內(nèi)接口CException異常稚失,可以參考下面直接在catch返回null即可:

    suspend inline fun <reified T : Any> launchRequest(
        crossinline block: suspend () -> CResponse<T>
    ): T? {
        return try {
            block()
        } catch (e: Exception) {
            null
        }?.run {
            if (0 == code) {
                val isListType = T::class.isSubclassOf(List::class)
                if (data == null && isListType) {
                    Collections.EMPTY_LIST as? T
                } else {
                    data
                }
            } else {
                throw CException(this)
            }
        } ?: let {
            null
        }
    }
復(fù)制代碼

同樣在WeatherRepository中再增加getWeather方法,通過(guò)獲取返回值的launchRequest來(lái)處理請(qǐng)求:

    suspend fun getWeather(area: String): Weather? {
        return launchRequest{
            ServerApi.service.getWeather(area)
        }
    }
復(fù)制代碼

因?yàn)槲覀冊(cè)?code>launchRequest重新拋出了異常恰聘,所以我們需要在請(qǐng)求的地方捕獲一下:

    fun getWeather(area: String) {
        requestMain {
            val weather = try {
                repository.getWeather(area)
            } catch (e: Exception) {
                //二次異常處理...
            }
        }
    }
復(fù)制代碼

上面的三種方式算上一種拋磚引玉句各,其實(shí)我們還可以進(jìn)一步的通過(guò)抽象ViewModel來(lái)統(tǒng)一處理內(nèi)部接口請(qǐng)求異常。

如果您有更好的方法或者思路想法晴叨,歡迎交流凿宾。架構(gòu)的演進(jìn)以及代碼的封裝需要不斷的學(xué)習(xí)和溝通,每一次知識(shí)交流與碰撞都是有意義的兼蕊。

作者:一個(gè)被攝影耽誤的程序猿
鏈接:https://juejin.cn/post/6962921891501703175
來(lái)源:稀土掘金
著作權(quán)歸作者所有初厚。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處孙技。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末产禾,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子牵啦,更是在濱河造成了極大的恐慌下愈,老刑警劉巖,帶你破解...
    沈念sama閱讀 223,002評(píng)論 6 519
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蕾久,死亡現(xiàn)場(chǎng)離奇詭異势似,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)僧著,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,357評(píng)論 3 400
  • 文/潘曉璐 我一進(jìn)店門(mén)履因,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人盹愚,你說(shuō)我怎么就攤上這事栅迄。” “怎么了皆怕?”我有些...
    開(kāi)封第一講書(shū)人閱讀 169,787評(píng)論 0 365
  • 文/不壞的土叔 我叫張陵毅舆,是天一觀(guān)的道長(zhǎng)。 經(jīng)常有香客問(wèn)我愈腾,道長(zhǎng)憋活,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 60,237評(píng)論 1 300
  • 正文 為了忘掉前任虱黄,我火速辦了婚禮悦即,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己辜梳,他們只是感情好粱甫,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,237評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著作瞄,像睡著了一般茶宵。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上宗挥,一...
    開(kāi)封第一講書(shū)人閱讀 52,821評(píng)論 1 314
  • 那天乌庶,我揣著相機(jī)與錄音,去河邊找鬼秧倾。 笑死严蓖,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播罐寨,決...
    沈念sama閱讀 41,236評(píng)論 3 424
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼混卵!你這毒婦竟也來(lái)了纳像?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 40,196評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤淌山,失蹤者是張志新(化名)和其女友劉穎裸燎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體泼疑,經(jīng)...
    沈念sama閱讀 46,716評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡德绿,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,794評(píng)論 3 343
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了退渗。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片移稳。...
    茶點(diǎn)故事閱讀 40,928評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖会油,靈堂內(nèi)的尸體忽然破棺而出个粱,到底是詐尸還是另有隱情,我是刑警寧澤翻翩,帶...
    沈念sama閱讀 36,583評(píng)論 5 351
  • 正文 年R本政府宣布都许,位于F島的核電站,受9級(jí)特大地震影響嫂冻,放射性物質(zhì)發(fā)生泄漏胶征。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,264評(píng)論 3 336
  • 文/蒙蒙 一桨仿、第九天 我趴在偏房一處隱蔽的房頂上張望弧烤。 院中可真熱鬧,春花似錦、人聲如沸暇昂。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,755評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)急波。三九已至从铲,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間澄暮,已是汗流浹背名段。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,869評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留泣懊,地道東北人伸辟。 一個(gè)月前我還...
    沈念sama閱讀 49,378評(píng)論 3 379
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像馍刮,于是被迫代替她去往敵國(guó)和親信夫。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,937評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容