本節(jié)教程我們將使用Retrofit網(wǎng)絡(luò)請(qǐng)求庫(kù)實(shí)現(xiàn)網(wǎng)易云音樂(lè)的推薦歌單的數(shù)據(jù)請(qǐng)求。請(qǐng)求的過(guò)程中我們將使用Coroutines實(shí)現(xiàn)異步操作,并且利用Moshi進(jìn)行網(wǎng)絡(luò)數(shù)據(jù)的解析。
我們的接口來(lái)自于開(kāi)源庫(kù)NeteaseCloudMusicApi,這個(gè)NodeJS API 庫(kù)的文檔非常完善,并且支持的接口非常多。這個(gè)庫(kù)的安裝請(qǐng)?jiān)旈喸擁?xiàng)目的參考文檔。

kotlin - Coroutine 協(xié)程
協(xié)程是kotlin的一個(gè)異步處理框架,是輕量級(jí)的線程。
協(xié)程的幾大優(yōu)勢(shì):
- 可以用寫(xiě)同步的代碼結(jié)構(gòu)樣式實(shí)現(xiàn)異步的功能
- 非常容易將代碼邏輯分發(fā)到不同的線程中
- 和作用域綁定,避免內(nèi)存泄露??梢詿o(wú)縫銜接LifeCycle和ViewModel等JetPack庫(kù)
- 減少模板代碼和避免了地獄回調(diào)
接下來(lái)我將詳細(xì)介紹下協(xié)程的概念和使用方法。
啟動(dòng)協(xié)程
啟動(dòng)協(xié)程使用最多的方式(主要)有launch和async
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T>
返回值 Job
Deferred其實(shí)是Job的子類,所以這兩個(gè)啟動(dòng)方法的返回值都是Job,那Job有什么特性呢?
- Job 代表一個(gè)異步的任務(wù)
- Job 具有生命周期并且可以取消。
- Job 還可以有層級(jí)關(guān)系,一個(gè)Job可以包含多個(gè)子Job,當(dāng)父Job被取消后,所有的子Job也會(huì)被自動(dòng)取消;當(dāng)子Job出現(xiàn)異常后父Job也會(huì)被取消。
Deferred有一個(gè)await方法就能取到協(xié)程的返回值,這是和Job的重要區(qū)別:
launch啟動(dòng)的協(xié)程的結(jié)果沒(méi)有返回值,async啟動(dòng)的協(xié)程會(huì)返回值.這就是Kotlin為什么設(shè)計(jì)有兩個(gè)啟動(dòng)方法的原因了。
public interface Deferred<out T> : Job {
public suspend fun await(): T
}
總結(jié):launch 更多是用來(lái)發(fā)起一個(gè)無(wú)需結(jié)果的耗時(shí)任務(wù)(如批量文件刪除、混合圖片等),async用于異步執(zhí)行耗時(shí)任務(wù),并且需要返回值(如網(wǎng)絡(luò)請(qǐng)求、數(shù)據(jù)庫(kù)讀寫(xiě)、文件讀寫(xiě))。
調(diào)用對(duì)象 CoroutineScope
啟動(dòng)協(xié)程需要在一定的協(xié)程作用域CoroutineScope下啟動(dòng)。
public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
ContextScope(if (context[Job] != null) context else context + Job())
通過(guò)CoroutineScope的構(gòu)造方法我們得知:
- 構(gòu)造的時(shí)候需要Job,如果沒(méi)有傳入就會(huì)在內(nèi)部新建一個(gè)Job做為這個(gè)協(xié)程的父Job來(lái)管理該協(xié)程的所有任務(wù)Job。
- 這兒的CoroutineContext我們可以簡(jiǎn)單的等于CoroutineDispatcher。這個(gè)稍后介紹。
協(xié)程作用域可以通過(guò)以下方式獲得:
- Global Scope --- 和APP的生命周期一致
- LiveDataScope, ViewModelScope, lifecycleScope 等 --- 和這些類的生命周期一致 (涉及到的內(nèi)容后面的教程會(huì)有解釋)
- 自定義 Scope --- 自己定義Scope,生命周期和定義相關(guān)。
協(xié)程作用域CoroutineScope的主要作用是規(guī)定了協(xié)程的執(zhí)行范圍,超過(guò)這個(gè)作用域范圍協(xié)程將會(huì)被自動(dòng)取消。
這就是前面提到的協(xié)程會(huì)和作用域綁定,避免內(nèi)存泄露。
協(xié)程向下文環(huán)境 CoroutineContext
上下文環(huán)境主要是傳如下Dispatchers的值,Dispatchers根據(jù)名字可以猜測(cè)它是分發(fā)器,把異步任務(wù)分發(fā)到對(duì)應(yīng)的線程去執(zhí)行。主要的值有以下:
- Dispatchers.Main --- 分發(fā)任務(wù)到主線程,主要執(zhí)行UI繪制等。
- DefaultScheduler.IO --- 分發(fā)任務(wù)IO線程,它用于輸入/輸出的場(chǎng)景。主要用來(lái)執(zhí)行網(wǎng)絡(luò)請(qǐng)求、數(shù)據(jù)庫(kù)操作、文件讀寫(xiě)等。
- DefaultScheduler.Default --- 主要執(zhí)行CPU密集的運(yùn)算操作
- DefaultScheduler.Unconfined --- 這個(gè)分發(fā)的線程不可控的,一般不建議使用。
階段總結(jié)
剛才我們介紹了協(xié)程launch函數(shù)的context參數(shù),接下來(lái)看看其他兩個(gè)參數(shù):
- start參數(shù)的意思是什么時(shí)候開(kāi)始分發(fā)任務(wù),CoroutineStart.DEFAULT代表的是協(xié)程啟動(dòng)的時(shí)候立即分發(fā)任務(wù)。
- block參數(shù)的意思啟動(dòng)的協(xié)程需要執(zhí)行的任務(wù)代碼。以不寫(xiě)內(nèi)容,直接傳空{(diào)} 執(zhí)行。明顯這樣啟動(dòng)的協(xié)程沒(méi)有意義,暫時(shí)僅為學(xué)習(xí)。
學(xué)習(xí)到到目前為止,我們應(yīng)該可以啟動(dòng)一個(gè)協(xié)程了
// 1
private val myJob = Job()
// 2
private val myScope = CoroutineScope(myJob + Dispatchers.Main)
// 3
myScope.launch() {
// 4 TODO
}
總結(jié)如下:
- 創(chuàng)建一個(gè)父Job,作為協(xié)程的父Job
- 使用 myJob 和 Dispatchers.Main 這個(gè)協(xié)程向下文環(huán)境創(chuàng)建一個(gè)myScope協(xié)程作用域
- 在myScope這個(gè)協(xié)程作用域下啟動(dòng)協(xié)程
- 執(zhí)行異步任務(wù)
協(xié)程中的異步操作 --- suspend函數(shù)
suspend函數(shù)的流程
實(shí)現(xiàn)異步操作的核心關(guān)鍵就是掛起函數(shù)suspend函數(shù),那究竟什么是掛起函數(shù)。
掛起函數(shù)的申明是在普通的函數(shù)前面加上suspend關(guān)鍵字,掛起函數(shù)執(zhí)行的時(shí)候會(huì)中斷協(xié)程,當(dāng)掛起函數(shù)執(zhí)行完成后,會(huì)把結(jié)果返回到當(dāng)前協(xié)程的中,然后執(zhí)行接下來(lái)的代碼。
上面這段話說(shuō)起來(lái)很枯燥,我們接下來(lái)利用代碼來(lái)解釋:
suspend fun login(username: String, password: String): User = withContext(Dispatchers.IO) {
println("threadname = ${Thread.currentThread().name}")
return@withContext User("Johnny")
}
myScope.launch() {
println("threadname = ${Thread.currentThread().name}")
val user = login("1111", "111111")
println("threadname = ${Thread.currentThread().name}")
println("$user")
}
-
掛起函數(shù)執(zhí)行的時(shí)候會(huì)中斷協(xié)程: suspend函數(shù)
login("1111", "111111")執(zhí)行的時(shí)候到會(huì)切換新的線程即IO線程去執(zhí)行,當(dāng)前的協(xié)程所在的主線程的流程被掛起中止了,主線程可以接著處理其他的事情。 -
當(dāng)掛起函數(shù)執(zhí)行完成后,會(huì)把結(jié)果返回到當(dāng)前協(xié)程中:
login("1111", "111111")在IO線程執(zhí)行完成后返回user,并且返回到主線程。即協(xié)程所在的線程。 -
然后執(zhí)行接下來(lái)的代碼: 接下來(lái)打印
println("$user")是在協(xié)程所在的主線程執(zhí)行。
結(jié)果如下所示:

withContext 函數(shù)
我們?cè)谏厦娴?strong>login函數(shù)中使用了withContext函數(shù),這個(gè)函數(shù)是非常實(shí)用和常見(jiàn)的suspend函數(shù)。 使用它能非常容易的實(shí)現(xiàn)線程的切換,從而實(shí)現(xiàn)異步操作。
public suspend fun <T> withContext(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): T
我們看到withContext函數(shù)也是個(gè)掛起函數(shù),那我們就沒(méi)有必要在掛起函數(shù)中調(diào)用掛起函數(shù),可以直接調(diào)用withContext的簡(jiǎn)寫(xiě):
myScope.launch() {
println("threadname = ${Thread.currentThread().name}")
val user = withContext(Dispatchers.IO) {
println("threadname = ${Thread.currentThread().name}")
return@withContext User("Johnny")
}
println("threadname = ${Thread.currentThread().name}")
println("$user")
}
協(xié)程中的異常處理機(jī)制
協(xié)程提供了一個(gè)異常處理的回調(diào)函數(shù)CoroutineExceptionHandler??梢詷?gòu)造一個(gè)函數(shù)對(duì)象,賦值給協(xié)程作用域,這樣協(xié)程中的異常就能被捕獲了。
private val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
Log.i("錯(cuò)誤信息", "${throwable.message}")
}
private val myScope = CoroutineScope(myJob + Dispatchers.Main + exceptionHandler)
提示:這里的 + 號(hào)不是數(shù)學(xué)意義的加號(hào),是把這些對(duì)象一起組合成一個(gè)協(xié)程向下文環(huán)境(鍵值對(duì))。
協(xié)程總結(jié)
- 協(xié)程作用域可以界定生命周期,避免內(nèi)存泄露
- suspend函數(shù)可以讓我們寫(xiě)同步代碼的結(jié)構(gòu)去實(shí)現(xiàn)異步功能
- withContext等函數(shù)能非常容易將代碼模塊分發(fā)的不同的線程中去。
- 協(xié)程還有良好的異常處理機(jī)制,
用協(xié)程和Retrofit實(shí)現(xiàn)網(wǎng)絡(luò)請(qǐng)求
Retrofit是負(fù)責(zé)網(wǎng)絡(luò)請(qǐng)求接口的封裝,通過(guò)大量的注解實(shí)現(xiàn)超級(jí)解耦。真正的網(wǎng)絡(luò)請(qǐng)求是OKHttp庫(kù)去實(shí)現(xiàn)。Retrofit常規(guī)使用方法不是本教程的講解范圍,本教程主要講Retrofit怎樣和協(xié)程無(wú)縫銜接實(shí)現(xiàn)網(wǎng)絡(luò)請(qǐng)求。
Moshi是一個(gè)JSON解析庫(kù),天生對(duì)Kotlin友好,特別是Kotlin的data數(shù)據(jù)類非常適合它。所以建議選擇它來(lái)解析JSON。
本地服務(wù)器環(huán)境搭建后好,訪問(wèn)http://localhost:3000/top/playlist/hot?limit=1&offset=0就能得到一系列的播單playlists

讓我們接下來(lái)寫(xiě)代碼吧。
- 在AndroidManifest.xml中加入網(wǎng)絡(luò)請(qǐng)求權(quán)限
<uses-permission android:name="android.permission.INTERNET"/>
- 新建network_security_config.xml文件配置,內(nèi)容如下
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>
- 然后在AndroidManifest.xml中配置,這樣APP就能通過(guò)HTTP協(xié)議訪問(wèn)服務(wù)器了
<application ...
android:networkSecurityConfig="@xml/network_security_config"
...>
</application>
- 添加依賴
def coroutines_version = '1.3.9'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
// Api - Retrofit (with Moshi) and OkHttp
def retrofit_version = '2.7.1'
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-moshi:$retrofit_version"
def okhttp_version = '4.2.1'
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
- 新建請(qǐng)求常量類MusicApiConstant
object MusicApiConstant {
const val BASE_URL = "http://10.0.2.2:3000" // BASEURL
const val PLAYLIST_HOT = "/top/playlist" // 推薦歌單
}
注意:我現(xiàn)在用的模擬器開(kāi)發(fā)測(cè)試,10.0.2.2代表的是模擬器所在機(jī)器的localhost地址,如果請(qǐng)求localhost訪問(wèn)的是模擬器的地址。
MusicApiConstant主要存放BASE_URL,各個(gè)請(qǐng)求的路徑等常量
- 新建網(wǎng)絡(luò)請(qǐng)求類 MusicApiService
interface MusicApiService {
companion object {
private const val TAG = "MusicApiService"
// 1
fun create(): MusicApiService {
val retrofit = Retrofit.Builder()
.baseUrl(MusicApiConstant.BASE_URL)
.client(okHttpClient)
.addConverterFactory(MoshiConverterFactory.create())
.build()
return retrofit.create(MusicApiService::class.java)
}
// 2
private val okHttpClient: OkHttpClient
get() = OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.build()
// 3
private val loggingInterceptor: HttpLoggingInterceptor
get() {
val interceptor = HttpLoggingInterceptor(object : HttpLoggingInterceptor.Logger{
override fun log(message: String) {
Log.i(TAG, message)
}
})
interceptor.level = HttpLoggingInterceptor.Level.BASIC
return interceptor
}
}
}
MusicApiService有一個(gè)伴生對(duì)象,里面有個(gè)create方法,是Retrofit的生成方法。其中配置了baseUrl,配置OKHttp為真正的請(qǐng)求類,配置了MoshiConverterFactory為JSON的轉(zhuǎn)換工廠。這個(gè)方法返回的對(duì)象是請(qǐng)求的發(fā)起者。
- 定義播單的數(shù)據(jù)類
data class PlayListResponse(
val code: Int,
val playlists: List<PlayItem>
)
data class PlayItem(val name: String,
val id: String,
val coverImgUrl: String,
val coverImgId: String,
val description: String,
val playCount: Int,
val highQuality: Boolean,
val shareCount: Int,
val subscribers: List<User>,
val creator: User
)
data class User(val nickname: String,
val userId: String,
val avatarUrl: String,
val gender: Int,
val followed: Boolean
)
- 配置請(qǐng)求接口
interface MusicApiService {
@GET(MusicApiConstant.PLAYLIST_HOT)
suspend fun getHotPlaylist(@Query("limit") limit: Int, @Query("offset") offset: Int) : PlayListResponse
....
}
在MusicApiService中加入所示代碼。
和普通寫(xiě)法的兩點(diǎn)重要區(qū)別:
- 需要定義接口為suspend函數(shù)
- 返回的直接是數(shù)據(jù),不是CallBack。
- Fragment中請(qǐng)求
在Fragment中定義Job,CoroutineExceptionHandler 和 CoroutineContext,構(gòu)建一個(gè)CoroutineScope。代碼如下:
private val myJob = Job()
private val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
Log.i("請(qǐng)求錯(cuò)誤信息", "${throwable.message}")
}
private val myScope = CoroutineScope(myJob + Dispatchers.Main + exceptionHandler)
- 在Fragment的onViewCreated方法中創(chuàng)建協(xié)程請(qǐng)求
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
myScope.launch {
val response = MusicApiService.create().getHotPlaylist(1, 0)
println("$response")
}
}
目前為止,請(qǐng)求結(jié)果就得到了。

- 及時(shí)取消協(xié)程
override fun onDestroy() {
super.onDestroy()
myScope.cancel()
}
在Fragment的onDestroy方法中要取消協(xié)程,否則有可能造成程序崩潰。
結(jié)語(yǔ) - 協(xié)程值得一學(xué)
協(xié)程是非常優(yōu)秀的異步處理框架,已經(jīng)和很多JetPack的庫(kù)無(wú)縫連接。使用起來(lái)非常方便。
譬如可以直接利用ViewModel的ViewModelScope感知Fragment的lifecycle,不需要手動(dòng)取消協(xié)程。此外Room和協(xié)程的Flow也能無(wú)縫連接,實(shí)現(xiàn)輕量級(jí)的RxJava類似的功能。這些后續(xù)都會(huì)有介紹。