Android開(kāi)發(fā)(30)——協(xié)程Coroutine和OkHttp請(qǐng)求

本節(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.DefaultDispatchers.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)題和前面不一樣。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容