本節(jié)內(nèi)容
1.JavaThread下載數(shù)據(jù)回調(diào)
2.引入?yún)f(xié)程
3.launch和async
4.coroutineScope和CoroutineContext
5.WithContext切換線程
6.啰嗦OkHttp
7.okhtttp獲取數(shù)據(jù)
8.聚合數(shù)據(jù)頭條新聞API說(shuō)明
9.使用OkHttp3獲取數(shù)據(jù)
10.手動(dòng)創(chuàng)建數(shù)據(jù)模型
11.使用插件自動(dòng)創(chuàng)建模型
12.使用retrofit獲取數(shù)據(jù)
一、JavaThread下載數(shù)據(jù)回調(diào)
1.Thread會(huì)阻塞當(dāng)前的線程 main-UI Thread,耗時(shí)比較短的小任務(wù)會(huì)放到主線程去做。
2.有時(shí)候主線程上會(huì)有一些耗時(shí)很長(zhǎng)的任務(wù),它會(huì)阻塞主線程的其他任務(wù)。為了解決這個(gè)問(wèn)題,可以開(kāi)啟一個(gè)新的線程,把它稱為子線程。
3.UI線程是提供給用戶進(jìn)行交互的,盡量不要讓它被阻塞。
4.以下面的代碼為例,在實(shí)現(xiàn)按鈕的點(diǎn)擊事件時(shí),我們打印完start,等待一會(huì)再打印end
button.setOnClickListener {
Log.v("swl","start ${Thread.currentThread()}")
Thread.sleep(2000)
Log.v("swl","end ${Thread.currentThread()}")
}
這樣的話,第一次點(diǎn)擊按鈕之后,只有等這個(gè)事件過(guò)了,才能第二次點(diǎn)擊按鈕。

直接阻塞主線程
5.為了不讓它阻塞主線程,我們可以創(chuàng)建一個(gè)新的線程,這樣每次點(diǎn)擊按鈕的時(shí)候,就不用等待也能直接開(kāi)始運(yùn)行了。
button.setOnClickListener {
Thread(object :Runnable{
override fun run() {
Log.v("swl","start ${Thread.currentThread()}")
Thread.sleep(2000)
Log.v("swl","end ${Thread.currentThread()}")
}
}
).start()
}
這種由于參數(shù)繼承自一個(gè)接口,而該接口里面又只有一種方法,所以可以使用lambda表達(dá)式。
button.setOnClickListener {
Thread{
Log.v("swl","start ${Thread.currentThread()}")
Thread.sleep(2000)
Log.v("swl","end ${Thread.currentThread()}")
}.start()
}

每次點(diǎn)擊按鈕后都開(kāi)啟一個(gè)新的線程
Thread.sleep(2000)的意思是阻塞當(dāng)前線程,如果把它直接寫(xiě)在MainActivity里面的話,那么它就會(huì)阻塞主線程,只有等待一段時(shí)間end打印完了之后,才能繼續(xù)點(diǎn)擊按鈕。
但是如果我們每次點(diǎn)擊都創(chuàng)建一個(gè)新的線程的話,第一次點(diǎn)擊之后先打印start,因?yàn)樗蛔枞?s,所以不會(huì)立刻打印end。如果我立刻又點(diǎn)了一下按鈕的話,這個(gè)時(shí)候就又會(huì)創(chuàng)建一個(gè)新的線程,又打印了一個(gè)start。因?yàn)槲以?S內(nèi)點(diǎn)了四次按鈕,所以打印了四個(gè)start,2S之后才打印end。
6.Java開(kāi)啟線程的弊端:
Java里面有線程池,每個(gè)線程池里面都只能放規(guī)定數(shù)量的線程。如果超過(guò)了這個(gè)數(shù)量,那么超過(guò)的那個(gè)就要進(jìn)入等待序列,直到線程池中有線程執(zhí)行完了,它才能進(jìn)入線程池執(zhí)行。
線程是很消耗內(nèi)存的,所以不能大量地開(kāi)辟線程。當(dāng)線程達(dá)到一定程度的時(shí)候,就會(huì)出現(xiàn)警告。
線程之間的數(shù)據(jù)交互:①通過(guò)Handler來(lái)傳遞數(shù)據(jù)(回調(diào)) ②進(jìn)行線程之間的切換(Rxjava)。
7.回調(diào)數(shù)據(jù)的方法
定義一套接口,實(shí)現(xiàn)兩個(gè)線程之間的數(shù)據(jù)回調(diào)。在需要傳遞數(shù)據(jù)的類(lèi)里面定義一個(gè)接口(接口里面定義兩個(gè)方法),在這個(gè)類(lèi)里面還需要定義一個(gè)接口類(lèi)型的listener。類(lèi)里面還有一個(gè)方法,在里面需要判斷有沒(méi)有l(wèi)istener,如果有的話就進(jìn)行相應(yīng)的操作。①在接收數(shù)據(jù)的類(lèi)里面,先繼承一下前面那個(gè)類(lèi)的接口,然后實(shí)現(xiàn)里面的方法,并把該類(lèi)作為它的listener。②這樣的話listener就和接口里的兩個(gè)方法分離開(kāi)了,還有一種方法。直接使用匿名類(lèi),讓listener等于這個(gè)匿名類(lèi),在里面實(shí)現(xiàn)接口里的兩個(gè)方法。(推薦使用第二種)
傳遞數(shù)據(jù)的類(lèi):
class UtilNetWork {
var listener: callBack? = null
fun data(){
Thread{
Log.v("swl","開(kāi)始下載。。${Thread.currentThread()}")
Thread.sleep(2000)
Log.v("swl","下載結(jié)束。。${Thread.currentThread()}")
val result = "jack"
}
listener.let {
}
}
interface callBack{
fun onSuccess(data:String)
fun onFailure(error:String)
}
}
接收數(shù)據(jù)的類(lèi),在MainActivity里面
button.setOnClickListener {
val util = UtilNetWork()
util.listener = object :UtilNetWork.callBack{
override fun onSuccess(data: String) {
}
override fun onFailure(error: String) {
}
}
}
8.切換線程。
runOnUIThread{
//進(jìn)行需要的操作
}
二、引入?yún)f(xié)程Coroutine
1.線程與協(xié)程的區(qū)別:
①一個(gè)任務(wù)可以創(chuàng)建多個(gè)線程,但是線程的數(shù)量是有限的。因?yàn)榫€程數(shù)量越多,那么消耗的內(nèi)存越多,速度越慢。②對(duì)于協(xié)程來(lái)說(shuō),一共就兩個(gè)線程,主線程和子線程。在子線程上可以創(chuàng)建無(wú)數(shù)個(gè)協(xié)程,資源消耗量不大,就在一個(gè)線程上進(jìn)行調(diào)度,可以有成千上百個(gè)協(xié)程同時(shí)執(zhí)行。協(xié)程會(huì)被阻塞,線程基本上不會(huì)被阻塞,因?yàn)橐粋€(gè)線程上有很多和協(xié)程。
線程執(zhí)行任務(wù)是按順序的,如果線程上有任務(wù)在執(zhí)行,后面的必須等它執(zhí)行完了才能接著執(zhí)行。但是線程上如果有執(zhí)行時(shí)間很長(zhǎng)的協(xié)程,那么就會(huì)把它掛起,讓它自己去執(zhí)行,然后在線程上接著執(zhí)行下一個(gè)協(xié)程。等到前面這個(gè)協(xié)程執(zhí)行完了之后,又從掛起的那個(gè)地方恢復(fù)。
2.協(xié)程的特點(diǎn):
- 輕量:您可以在單個(gè)線程上運(yùn)行多個(gè)協(xié)程,因?yàn)閰f(xié)程支持掛起,不會(huì)使正在運(yùn)行協(xié)程的線程阻塞。掛起比阻塞節(jié)省內(nèi)存,且支持多個(gè)并行操作。
- 內(nèi)存泄漏更少:使用結(jié)構(gòu)化并發(fā)機(jī)制在一個(gè)作用域內(nèi)執(zhí)行多項(xiàng)操作。
- 內(nèi)置取消支持:取消操作會(huì)自動(dòng)在運(yùn)行中的整個(gè)協(xié)程層次結(jié)構(gòu)內(nèi)傳播。
- Jetpack 集成:許多 Jetpack 庫(kù)都包含提供全面協(xié)程支持的擴(kuò)展。某些庫(kù)還提供自己的協(xié)程作用域,可供您用于結(jié)構(gòu)化并發(fā)。
3.使用協(xié)程
1.創(chuàng)建一個(gè)新的工程,在里面添加一個(gè)library,然后將以下依賴項(xiàng)添加到應(yīng)用的 build.gradle 文件中。(如果只是在kotlin里面使用,那么直接導(dǎo)入下面的依賴庫(kù)即可。但是如果是在安卓里面使用,那么還需要導(dǎo)入implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9")這個(gè)依賴庫(kù))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9")
2.在library的包里面再創(chuàng)建一個(gè)MyClass類(lèi),在類(lèi)外面寫(xiě)一個(gè)main()方法,在里面創(chuàng)建一個(gè)線程。
fun main(){
println("main start ${Thread.currentThread()}")
Thread{
println("start ${Thread.currentThread()}")
Thread.sleep(2000)
println("end ${Thread.currentThread()}")
}.start()
println("main end ${Thread.currentThread()}")
}
運(yùn)行結(jié)果如下圖所示:

運(yùn)行結(jié)果
主線程不會(huì)被阻塞,所以main start之后立馬執(zhí)行main end。如果開(kāi)啟新線程的時(shí)間很短,那么輸出順序也可能為main start ->start->main end->end
3.使用協(xié)程的方式。(那么就不能使用Thread,而要使用delay,因?yàn)門(mén)hread會(huì)阻塞線程,delay不會(huì)阻塞線程)
fun main(){
println("main start ${Thread.currentThread()}")
GlobalScope.launch {
load() }
println("main end ${Thread.currentThread()}")
}
suspend fun load(){
println("start ${Thread.currentThread()}")
delay(2000)
println("end ${Thread.currentThread()}")
}
任何一個(gè)協(xié)程都有自己的CoroutineScope,在這個(gè)協(xié)程域里面可以創(chuàng)建無(wú)數(shù)個(gè)子協(xié)程。
如何創(chuàng)建一個(gè)CoroutineScope:(一般使用前面兩種)
- launch :創(chuàng)建一個(gè)獨(dú)立的CoroutineScope。同步,不返回?cái)?shù)據(jù)。
- async :異步,需要返回?cái)?shù)據(jù)。
- runBlocking:它會(huì)在當(dāng)前線程上創(chuàng)建一個(gè)協(xié)程域,并且這個(gè)執(zhí)行會(huì)阻塞當(dāng)前的線程。
- GlobalScope:創(chuàng)建一個(gè)全局的CoroutineScope,不推薦使用。作用域?yàn)檎麄€(gè)app的lifecycle。缺點(diǎn):當(dāng)主線程結(jié)束,不會(huì)等待GlobalScope的協(xié)程執(zhí)行完畢。它會(huì)創(chuàng)建一個(gè)新的線程。
suspend:掛起函數(shù)只能在另外一個(gè)掛起函數(shù)或者一個(gè)coroutineScope(協(xié)程域包括協(xié)程的所有使用方法,其中就有掛起功能)里面調(diào)用

GlobalScope運(yùn)行結(jié)果
因?yàn)橹骶€程執(zhí)行速度太快了,調(diào)用load方法還要延遲2S鐘。所以新的線程還沒(méi)開(kāi)始,主線程就已經(jīng)運(yùn)行結(jié)束了。
4.前面用的是GlobalScope,為了解決速度過(guò)快導(dǎo)致子線程無(wú)法開(kāi)啟的問(wèn)題,我們可以延長(zhǎng)主線程的執(zhí)行時(shí)間,也delay一下,但是delay是一個(gè)掛起方法,所以我們使用sunBlocking。
fun main()= runBlocking{
println("main start ${Thread.currentThread()}")
GlobalScope.launch {
load() }
delay(3000)
println("main end ${Thread.currentThread()}")
}
suspend fun load(){
println("start ${Thread.currentThread()}")
delay(2000)
println("end ${Thread.currentThread()}")
}

runBlocking運(yùn)行結(jié)果
可以發(fā)現(xiàn)由于主線程有延遲,而且延遲時(shí)間多于子線程,所以子線程執(zhí)行完畢之后,主線程才結(jié)束。
三、lunch和asyno
1.只用runBlocking,不用GlobalScope的話,那么整個(gè)runBlocking都是協(xié)程域,整個(gè)協(xié)程都被掛起,不會(huì)有阻塞。
fun main()= runBlocking{
println("main start ${Thread.currentThread()}")
loadTask1()
println("main end ${Thread.currentThread()}")
}
suspend fun loadTask1(){
println("start1 ${Thread.currentThread()}")
delay(1000)
println("end1 ${Thread.currentThread()}")
}

執(zhí)行結(jié)果
很明顯它是按照順序執(zhí)行的,因?yàn)樗鼈兌荚谕粋€(gè)域里面。
2.使用launch創(chuàng)建協(xié)程
fun main()= runBlocking{
println("main start ${Thread.currentThread()}")
launch {
loadTask1()
}
println("main end ${Thread.currentThread()}")
}
suspend fun loadTask1(){
println("start1 ${Thread.currentThread()}")
delay(1000)
println("end1 ${Thread.currentThread()}")
}

launch執(zhí)行結(jié)果
launch并沒(méi)有阻塞主線程的執(zhí)行,如果再添加一個(gè)launch
fun main()= runBlocking{
println("main start ${Thread.currentThread()}")
launch {
loadTask1()
}
launch {
loadTask2()
}
println("main end ${Thread.currentThread()}")
}
suspend fun loadTask1(){
println("start1 ${Thread.currentThread()}")
delay(1000)
println("end1 ${Thread.currentThread()}")
}
suspend fun loadTask2(){
println("start2 ${Thread.currentThread()}")
delay(1000)
println("end2 ${Thread.currentThread()}")
}

兩個(gè)launch執(zhí)行結(jié)果
還是沒(méi)有阻塞主線程,但是在執(zhí)行1的時(shí)候,遇到了delay,所以1被掛起,執(zhí)行2,然后1結(jié)束,2結(jié)束。
使用measureTimeMillis方法來(lái)計(jì)算掛起的時(shí)間
fun main()= runBlocking{
println("main start ${Thread.currentThread()}")
val time = measureTimeMillis {
launch {
loadTask1()
}
launch {
loadTask2()
}
}
println("time $time")
println("main end ${Thread.currentThread()}")
}

計(jì)算結(jié)果為15ms
為什么會(huì)出現(xiàn)這個(gè)結(jié)果呢?因?yàn)槲覀兇蛴〉闹皇欠峙涞臅r(shí)間,并不是執(zhí)行的時(shí)間。當(dāng)我們查看launch的源碼,發(fā)現(xiàn)里面有一個(gè)join方法,它的作用是掛起一個(gè)協(xié)程知道它結(jié)束為止。
fun main()= runBlocking{
println("main start ${Thread.currentThread()}")
val time = measureTimeMillis {
val job1 =launch {
loadTask1()
}
job1.join()
val job2= launch {
loadTask2()
}
job2.join()
}
println("time $time")
println("main end ${Thread.currentThread()}")
}
調(diào)用join方法,發(fā)現(xiàn)end main到最后去了。

調(diào)用join方法的執(zhí)行結(jié)果
所以我們可以發(fā)現(xiàn)這里協(xié)程是同步執(zhí)行的,也就是執(zhí)行完了1才會(huì)執(zhí)行2,由于線程開(kāi)啟關(guān)閉還需要時(shí)間,所以比2s多一點(diǎn)時(shí)間。
3.使用async創(chuàng)建協(xié)程。
fun main()= runBlocking{
println("main start ${Thread.currentThread()}")
val time = measureTimeMillis {
val job1 =async {
loadTask1()
}
val job2= async {
loadTask2()
}
job1.await()
job2.await()
}
println("time $time")
println("main end ${Thread.currentThread()}")
}

異步執(zhí)行結(jié)果
async是異步執(zhí)行的。誰(shuí)先讀取完就執(zhí)行誰(shuí)的,沒(méi)有順序。很明顯異步執(zhí)行要比同步執(zhí)行的時(shí)間短。
四、CoroutineScope和CoroutineContext
1.當(dāng)我們執(zhí)行以下代碼時(shí),會(huì)得到如下結(jié)果:
fun main(){
runBlocking {
println("1: ${Thread.currentThread()}")
launch {
println("2: ${Thread.currentThread()}")
}
println("3: ${Thread.currentThread()}")
}
}

執(zhí)行結(jié)果
把launch改為async,結(jié)果也是一樣的。這說(shuō)明就算開(kāi)啟協(xié)程的方式不同,但是線程是一樣的,它并不會(huì)開(kāi)啟新的線程。
2.CoroutineScope:使用launch和async時(shí),都是創(chuàng)建一個(gè)新的scope
3.CoroutineContext:使用launch和async時(shí),和parent scope在同一個(gè)context中
五、withContext切換線程
1.Dispatchers:調(diào)度器。
2.線程的切換主要有:
Dispatchers.Main - 使用此調(diào)度程序可在 Android 主線程上運(yùn)行協(xié)程。此調(diào)度程序只能用于與界面交互和執(zhí)行快速工作。示例包括調(diào)用
suspend函數(shù),運(yùn)行 Android 界面框架操作,以及更新LiveData對(duì)象。Dispatchers.IO - 此調(diào)度程序經(jīng)過(guò)了專(zhuān)門(mén)優(yōu)化,適合在主線程之外執(zhí)行磁盤(pán)或網(wǎng)絡(luò) I/O。示例包括使用 Room 組件、從文件中讀取數(shù)據(jù)或向文件中寫(xiě)入數(shù)據(jù),以及運(yùn)行任何網(wǎng)絡(luò)操作。
Dispatchers.Default - 此調(diào)度程序經(jīng)過(guò)了專(zhuān)門(mén)優(yōu)化,適合在主線程之外執(zhí)行占用大量 CPU 資源的工作。用例示例包括對(duì)列表排序和解析 JSON。
2.模擬一下用戶登錄的過(guò)程。在網(wǎng)絡(luò)上先讀取用戶的id,再獲取用戶的信息。這是切換線程的一種方式,從io線程切換到main線程(在io線程運(yùn)行結(jié)束后,會(huì)自動(dòng)切換到main線程)。
data class User(val name:String)
fun main(){
runBlocking {
val result = async (Dispatchers.IO) {
login()
}
val userInfo = async(Dispatchers.IO) {
userInfo(result.await())
}
println(userInfo.await().name)
}
}
suspend fun login():Int{
println("開(kāi)始login")
delay(1000)
println("login成功")
return 1001
}
suspend fun userInfo(id:Int):User{
println("獲取用戶信息:$id")
delay(1000)
println("獲取用戶信息成功")
return User("jack")
}
運(yùn)行結(jié)果如下:

運(yùn)行結(jié)果
3.如果一個(gè)任務(wù)既要在主線程執(zhí)行,又要在子線程執(zhí)行,那么我建議先指定在主線程,需要子線程的時(shí)候再指定IO線程,這樣它最終只會(huì)進(jìn)行一次跳轉(zhuǎn)。
fun main(){
runBlocking {
launch (Dispatchers.Main){
launch (Dispatchers.IO){
}
}
}
}
還有一種就是在函數(shù)內(nèi)部就提前指定好線程。這樣直接調(diào)用函數(shù)更容易理解。
fun main(){
runBlocking {
val id= login()
val user= userInfo(id)
println("user: ${user.name}")
}
}
suspend fun login():Int{
return withContext(Dispatchers.IO){
println("開(kāi)始login")
delay(1000)
println("login成功")
1001 //默認(rèn)返回值
}
}
suspend fun userInfo(id:Int):User{
return withContext(Dispatchers.IO){
println("獲取用戶信息:$id")
delay(1000)
println("獲取用戶信息成功")
User("jack")
}
}

運(yùn)行結(jié)果
4.使用withContext切換線程。這個(gè)還是沒(méi)有上面的那個(gè)好。
launch (Dispatchers.Main){
withContext(Dispatchers.IO){
login()
}
userInfo()
}
}
5.與基于回調(diào)的等效實(shí)現(xiàn)相比,
withContext()不會(huì)增加額外的開(kāi)銷(xiāo)。此外,在某些情況下,還可以優(yōu)化withContext()調(diào)用,使其超越基于回調(diào)的等效實(shí)現(xiàn)。例如,如果某個(gè)函數(shù)對(duì)一個(gè)網(wǎng)絡(luò)進(jìn)行十次調(diào)用,您可以使用外部withContext()讓 Kotlin 只切換一次線程。這樣,即使網(wǎng)絡(luò)庫(kù)多次使用withContext(),它也會(huì)留在同一調(diào)度程序上,并避免切換線程。此外,Kotlin 還優(yōu)化了Dispatchers.Default與Dispatchers.IO之間的切換,以盡可能避免線程切換。
重要提示:利用一個(gè)使用線程池的調(diào)度程序(例如 Dispatchers.IO 或 Dispatchers.Default)不能保證塊在同一線程上從上到下執(zhí)行。在某些情況下,Kotlin 協(xié)程在 suspend 和 resume 后可能會(huì)將執(zhí)行工作移交給另一個(gè)線程。這意味著,對(duì)于整個(gè) withContext() 塊,線程局部變量可能并不指向同一個(gè)值。
6.使用withContext其實(shí)還是會(huì)阻塞主線程,如果想不阻塞主線程的話,另外開(kāi)啟一個(gè)新線程來(lái)調(diào)用login()和userInfo()函數(shù),這樣只會(huì)阻塞當(dāng)前線程,不會(huì)阻塞主線程。
六、啰嗦OkHttp
1.先向gradle中導(dǎo)入依賴庫(kù)
androidTestImplementation 'androidx.test.espresso:espresso-android:3.3.0'
2.進(jìn)入OkHttp官網(wǎng)https://square.github.io/okhttp/,找到Releases導(dǎo)入依賴庫(kù)。
androidTestImplementation 'androidx.test.espresso:espresso-android:3.3.0'
3.OkHttp3實(shí)際上一個(gè)封裝,封裝的內(nèi)容包括:
OkHttpClient:提供給用戶,用戶通過(guò)這個(gè)來(lái)創(chuàng)建OkHttp,也就是對(duì)象。
Request:包括請(qǐng)求的地址和其他信息。
Call接口:里面定義了一些操作。真正來(lái)操作的是一個(gè)RealCall對(duì)象。
okio:真正做傳輸?shù)暮诵摹?/h6>
4.application->okhttp3->Caching或服務(wù)器。在application與okhttp3之間還有一個(gè)攔截器(NetWorkInterpreter,攔截網(wǎng)絡(luò)的每一個(gè)操作,也就是獲取詳細(xì)信息)
OkHttpClient里面有一個(gè)call對(duì)象指向RealCall,還有一個(gè)Dispatchers,Caching
Request包括url,method
5.具體的使用詳見(jiàn)https://square.github.io/okhttp/recipes/
七、okhttp獲取數(shù)據(jù)
1.數(shù)據(jù)有兩種類(lèi)型:
XML :幾乎不用
JSON:將JSON的數(shù)據(jù)轉(zhuǎn)化為kotlin里面的數(shù)據(jù)類(lèi)型。
這里需要使用聚合數(shù)據(jù),我們先提前在聚合數(shù)據(jù)里面申請(qǐng)一個(gè)賬號(hào),然后選擇API里面的免費(fèi)項(xiàng)目,比如新聞?lì)^條,按照它的格式發(fā)送請(qǐng)求

請(qǐng)求詳情
在網(wǎng)頁(yè)中輸入以下網(wǎng)址:https://v.juhe.cn/toutiao/index?type=&page=&page_size=&is_filter=&key=4494d20d3e853ec01a1dafc8b901e716,然后打開(kāi)一個(gè)JSON解析器,將網(wǎng)頁(yè)內(nèi)的數(shù)據(jù)轉(zhuǎn)化為我們能看懂的代碼。

解析之后
解析之后折疊起來(lái),只有三個(gè)元素,相當(dāng)于三個(gè)map(包含key和value)
展開(kāi)result:

result展開(kāi)之后
2.布局一下activity_main,我的布局加了一個(gè)按鈕和一個(gè)progressBar:

activity_main布局
3.新建一個(gè)工程,向里面導(dǎo)入我們需要的依賴庫(kù)。
//coroutine
androidTestImplementation 'androidx.test.espresso:espresso-android:3.3.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9")
//okhttp
implementation("com.squareup.okhttp3:okhttp:4.9.0")
// ViewModel
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0-alpha02")
// LiveData
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.4.0-alpha02")
// Lifecycles only (without ViewModel or LiveData)
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.4.0-alpha02")
4.我們要根據(jù)前面獲取到的信息做一個(gè)新聞?wù)故卷?yè)面,使用MVVM模式,所以要另外創(chuàng)建一個(gè)類(lèi)作為ViewModel。
class NewsViewModel:ViewModel() {
val news:MutableLiveData<String?> = MutableLiveData()
init {
news.value = null
}
fun loadNews(){
viewModelScope.launch {
news.value = realLoad()
}
}
suspend fun realLoad():String? {
return withContext(Dispatchers.IO) {
val client = OkHttpClient()
val request = Request.Builder()
.url("http://v.juhe.cn/toutiao/index?type=&key=4494d20d3e853ec01a1dafc8b901e716")
.get()
.build()
val response = client.newCall(request).execute()
if (response.isSuccessful) {
delay(2000)
response.body?.string()
} else {
null
}
}
}
}
MainActivity代碼如下:
class MainActivity : AppCompatActivity() {
private lateinit var viewModel:NewsViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
ViewModelProvider(this,ViewModelProvider.NewInstanceFactory())
.get(NewsViewModel::class.java)
viewModel.news.observe(this){value->
if(value!=null){
progressBar.visibility = View.GONE
Log.v("swl","$value")
}
}
button.setOnClickListener {
progressBar.visibility = View.VISIBLE
viewModel.loadNews()
}
}
}
5.運(yùn)行成功之后就能看到打印出來(lái)的結(jié)果了。
八、聚合數(shù)據(jù)頭條新聞API說(shuō)明
1.OkHttp3和Retrofit請(qǐng)求數(shù)據(jù)要使用的知識(shí)點(diǎn)
聚合數(shù)據(jù)API使用
OkHttp3請(qǐng)求數(shù)據(jù)
Gson解析json數(shù)據(jù)
json To kotlin 插件使用
Retrofit請(qǐng)求數(shù)據(jù)的步驟
2.進(jìn)入聚合數(shù)據(jù)官網(wǎng)https://www.juhe.cn/首頁(yè),選擇生活服務(wù),進(jìn)入新聞?lì)^條板塊。

新聞?lì)^條
3.http://v.juhe.cn/toutiao/index?type=top&key=APPKEY,按照這個(gè)格式就可以得到一個(gè)接口的地址http://v.juhe.cn/toutiao/index?type=guonei&key=4494d20d3e853ec01a1dafc8b901e716,type和key見(jiàn)下面的圖片。

type和key
4.將我們按照需求設(shè)計(jì)好的網(wǎng)址在瀏覽器中輸入,最后可以得到一串?dāng)?shù)據(jù),將其放在json解析器里面進(jìn)行解析。解析結(jié)果差不多如下圖所示:

解析結(jié)果
九、使用OkHttp3獲取數(shù)據(jù)
1.進(jìn)入github官網(wǎng),搜一下okhttp,點(diǎn)進(jìn)去第一個(gè),查看詳細(xì)信息。https://github.com/square/okhttp
2.添加依賴庫(kù)。
implementation("com.squareup.okhttp3:okhttp:4.9.1")
3.我們新建一個(gè)項(xiàng)目工程先測(cè)試一下,把依賴庫(kù)導(dǎo)進(jìn)去,然后在manifest里面添加以下代碼。后面那個(gè)是在<application>里面。
<uses-permission android:name="android.permission.INTERNET"/>
android:usesCleartextTraffic="true"
4.在MainActivity里面編寫(xiě)一下代碼。
class MainActivity : AppCompatActivity() {
private val xinwen_url = "http://v.juhe.cn/toutiao/index?type=guonei&key=4494d20d3e853ec01a1dafc8b901e716"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
if(event?.action == MotionEvent.ACTION_DOWN){
val httpClient = OkHttpClient()
val request = Request.Builder()
.url(xinwen_url)
.build()
httpClient.newCall(request).enqueue(object:Callback{
override fun onFailure(call: Call, e: IOException) {
e.printStackTrace()
}
override fun onResponse(call: Call, response: Response) {
if(response.isSuccessful){
val BodyStr = response.body?.string()
Log.v("swl","下載的內(nèi)容為:$BodyStr")
}
}
})
}
return super.onTouchEvent(event)
}
}
最后的結(jié)果如下所示,這說(shuō)明我們解析成了。

運(yùn)行結(jié)果
十、手動(dòng)創(chuàng)建數(shù)據(jù)模型
1.按照前面的方法,數(shù)據(jù)是獲取到了,但是無(wú)法直接加進(jìn)我們的項(xiàng)目中,所以我們要先建一個(gè)數(shù)據(jù)模型。
2.打開(kāi)github,搜索Gson,點(diǎn)擊第一個(gè)。進(jìn)入以下網(wǎng)住https://github.com/google/gson
3.根據(jù)上面的網(wǎng)址,導(dǎo)入一下依賴庫(kù)。
implementation 'com.google.code.gson:gson:2.8.7'
4.創(chuàng)建一個(gè)數(shù)據(jù)類(lèi),NewsModel,里面包含的數(shù)據(jù)和json解析器里面解析出來(lái)的數(shù)據(jù)類(lèi)似。
data class NewsModel(
val reason:String,
val result:Result,
val error_code:Int
)
data class Result (val data:List<New>, )
data class New(val title:String)
十一、使用插件自動(dòng)創(chuàng)建模型
1.前面我們手動(dòng)來(lái)創(chuàng)建模型,其實(shí)是很麻煩的,對(duì)于結(jié)構(gòu)比較清晰的數(shù)據(jù)來(lái)說(shuō)沒(méi)問(wèn)題,但如果對(duì)于結(jié)構(gòu)比較復(fù)雜的數(shù)據(jù)就很麻煩了,很容易出錯(cuò)。這時(shí)候就可以使用插件了。
2.在Android Studio里面打開(kāi)設(shè)置,點(diǎn)擊plungs,搜索JSON,下載第一個(gè)即可。

下載插件
3.然后new一個(gè)kotlin data class file from json,把http://v.juhe.cn/toutiao/index?type=guonei&key=4494d20d3e853ec01a1dafc8b901e716這個(gè)網(wǎng)址的內(nèi)容全部拷貝進(jìn)去,一個(gè)都不能漏,Annotation記得勾Gson,其他都不變。

創(chuàng)建過(guò)程
4.自動(dòng)創(chuàng)建好的代碼如下圖所示,我已經(jīng)把不需要的刪掉了。
data class NewsModel(
@SerializedName("result")
val result: Result
)
data class Result(
@SerializedName("data")
val data: List<Data>,
)
data class Data(
@SerializedName("author_name")
val authorName: String,
@SerializedName("category")
val category: String,
@SerializedName("date")
val date: String,
@SerializedName("is_content")
val isContent: String,
@SerializedName("thumbnail_pic_s")
val thumbnailPicS: String,
@SerializedName("title")
val title: String,
@SerializedName("uniquekey")
val uniquekey: String,
@SerializedName("url")
val url: String
)
5.在MainActivity里面,在前面的代碼后面進(jìn)行解析。
if(response.isSuccessful){
val bodyStr = response.body?.string()
val gson = Gson()
val model = gson.fromJson<NewsModel>(bodyStr,NewsModel::class.java)
model.result.data.forEach {
Log.v("swl",it.title)
}
}
6.運(yùn)行結(jié)果如下圖所示:

解析成功
十二、使用retrofit獲取數(shù)據(jù)
1.retrofit相比于okhttp更簡(jiǎn)單,更簡(jiǎn)潔,所以用它更好。去https://square.github.io/retrofit/網(wǎng)站查看它的使用方法。
2.導(dǎo)入一下依賴庫(kù)。
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.4.0-alpha02")
3.創(chuàng)建一個(gè)接口,作為API。
interface NewsAPI {
@GET("index?type=guonei&key=4494d20d3e853ec01a1dafc8b901e716")
suspend fun getNews():NewsModel
}
4.在MainActivity里面寫(xiě)一個(gè)useRetrofit()方法,使用Retrofit來(lái)獲取數(shù)據(jù)。在onTouchEvent里面調(diào)用這個(gè)方法。
fun useRetrofit(){
val retrofit = Retrofit.Builder()
.baseUrl("http://v.juhe.cn/toutiao/")
.addConverterFactory(GsonConverterFactory.create())
.build()
val api = retrofit.create(NewsAPI::class.java)
lifecycleScope.launch {
val news = api.getNews()
news.result.data.forEach{
Log.v("swl",it.title)
}
}
}

運(yùn)行結(jié)果
因?yàn)樾侣劽?分鐘就會(huì)刷新一次,所以新聞標(biāo)題和前面不一樣。