Android开发(30)——协程Coroutine和OkHtt
2021-08-04 本文已影响0人
让时间走12138
本节内容
1.JavaThread下载数据回调
2.引入协程
3.launch和async
4.coroutineScope和CoroutineContext
5.WithContext切换线程
6.啰嗦OkHttp
7.okhtttp获取数据
8.聚合数据头条新闻API说明
9.使用OkHttp3获取数据
10.手动创建数据模型
11.使用插件自动创建模型
12.使用retrofit获取数据
一、JavaThread下载数据回调
1.Thread会阻塞当前的线程 main-UI Thread,耗时比较短的小任务会放到主线程去做。
2.有时候主线程上会有一些耗时很长的任务,它会阻塞主线程的其他任务。为了解决这个问题,可以开启一个新的线程,把它称为子线程。
3.UI线程是提供给用户进行交互的,尽量不要让它被阻塞。
4.以下面的代码为例,在实现按钮的点击事件时,我们打印完start,等待一会再打印end
button.setOnClickListener {
Log.v("swl","start ${Thread.currentThread()}")
Thread.sleep(2000)
Log.v("swl","end ${Thread.currentThread()}")
}
直接阻塞主线程
这样的话,第一次点击按钮之后,只有等这个事件过了,才能第二次点击按钮。
5.为了不让它阻塞主线程,我们可以创建一个新的线程,这样每次点击按钮的时候,就不用等待也能直接开始运行了。
button.setOnClickListener {
Thread(object :Runnable{
override fun run() {
Log.v("swl","start ${Thread.currentThread()}")
Thread.sleep(2000)
Log.v("swl","end ${Thread.currentThread()}")
}
}
).start()
}
这种由于参数继承自一个接口,而该接口里面又只有一种方法,所以可以使用lambda表达式。
button.setOnClickListener {
Thread{
Log.v("swl","start ${Thread.currentThread()}")
Thread.sleep(2000)
Log.v("swl","end ${Thread.currentThread()}")
}.start()
}
每次点击按钮后都开启一个新的线程
Thread.sleep(2000)的意思是阻塞当前线程,如果把它直接写在MainActivity里面的话,那么它就会阻塞主线程,只有等待一段时间end打印完了之后,才能继续点击按钮。
但是如果我们每次点击都创建一个新的线程的话,第一次点击之后先打印start,因为它被阻塞了2s,所以不会立刻打印end。如果我立刻又点了一下按钮的话,这个时候就又会创建一个新的线程,又打印了一个start。因为我在2S内点了四次按钮,所以打印了四个start,2S之后才打印end。
6.Java开启线程的弊端:
Java里面有线程池,每个线程池里面都只能放规定数量的线程。如果超过了这个数量,那么超过的那个就要进入等待序列,直到线程池中有线程执行完了,它才能进入线程池执行。
线程是很消耗内存的,所以不能大量地开辟线程。当线程达到一定程度的时候,就会出现警告。
线程之间的数据交互:①通过Handler来传递数据(回调) ②进行线程之间的切换(Rxjava)。
7.回调数据的方法
定义一套接口,实现两个线程之间的数据回调。在需要传递数据的类里面定义一个接口(接口里面定义两个方法),在这个类里面还需要定义一个接口类型的listener。类里面还有一个方法,在里面需要判断有没有listener,如果有的话就进行相应的操作。①在接收数据的类里面,先继承一下前面那个类的接口,然后实现里面的方法,并把该类作为它的listener。②这样的话listener就和接口里的两个方法分离开了,还有一种方法。直接使用匿名类,让listener等于这个匿名类,在里面实现接口里的两个方法。(推荐使用第二种)
传递数据的类:
class UtilNetWork {
var listener: callBack? = null
fun data(){
Thread{
Log.v("swl","开始下载。。${Thread.currentThread()}")
Thread.sleep(2000)
Log.v("swl","下载结束。。${Thread.currentThread()}")
val result = "jack"
}
listener.let {
}
}
interface callBack{
fun onSuccess(data:String)
fun onFailure(error:String)
}
}
接收数据的类,在MainActivity里面
button.setOnClickListener {
val util = UtilNetWork()
util.listener = object :UtilNetWork.callBack{
override fun onSuccess(data: String) {
}
override fun onFailure(error: String) {
}
}
}
8.切换线程。
runOnUIThread{
//进行需要的操作
}
二、引入协程Coroutine
1.线程与协程的区别:
①一个任务可以创建多个线程,但是线程的数量是有限的。因为线程数量越多,那么消耗的内存越多,速度越慢。②对于协程来说,一共就两个线程,主线程和子线程。在子线程上可以创建无数个协程,资源消耗量不大,就在一个线程上进行调度,可以有成千上百个协程同时执行。协程会被阻塞,线程基本上不会被阻塞,因为一个线程上有很多和协程。
线程执行任务是按顺序的,如果线程上有任务在执行,后面的必须等它执行完了才能接着执行。但是线程上如果有执行时间很长的协程,那么就会把它挂起,让它自己去执行,然后在线程上接着执行下一个协程。等到前面这个协程执行完了之后,又从挂起的那个地方恢复。
2.协程的特点:
- 轻量:您可以在单个线程上运行多个协程,因为协程支持挂起,不会使正在运行协程的线程阻塞。挂起比阻塞节省内存,且支持多个并行操作。
- 内存泄漏更少:使用结构化并发机制在一个作用域内执行多项操作。
- 内置取消支持:取消操作会自动在运行中的整个协程层次结构内传播。
- Jetpack 集成:许多 Jetpack 库都包含提供全面协程支持的扩展。某些库还提供自己的协程作用域,可供您用于结构化并发。
3.使用协程
1.创建一个新的工程,在里面添加一个library,然后将以下依赖项添加到应用的 build.gradle 文件中。(如果只是在kotlin里面使用,那么直接导入下面的依赖库即可。但是如果是在安卓里面使用,那么还需要导入implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9")这个依赖库)
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9")
2.在library的包里面再创建一个MyClass类,在类外面写一个main()方法,在里面创建一个线程。
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()}")
}
运行结果运行结果如下图所示:
主线程不会被阻塞,所以main start之后立马执行main end。如果开启新线程的时间很短,那么输出顺序也可能为main start ->start->main end->end
3.使用协程的方式。(那么就不能使用Thread,而要使用delay,因为Thread会阻塞线程,delay不会阻塞线程)
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()}")
}
GlobalScope运行结果任何一个协程都有自己的CoroutineScope,在这个协程域里面可以创建无数个子协程。
如何创建一个CoroutineScope:(一般使用前面两种)
- launch :创建一个独立的CoroutineScope。同步,不返回数据。
- async :异步,需要返回数据。
- runBlocking:它会在当前线程上创建一个协程域,并且这个执行会阻塞当前的线程。
- GlobalScope:创建一个全局的CoroutineScope,不推荐使用。作用域为整个app的lifecycle。缺点:当主线程结束,不会等待GlobalScope的协程执行完毕。它会创建一个新的线程。
suspend:挂起函数只能在另外一个挂起函数或者一个coroutineScope(协程域包括协程的所有使用方法,其中就有挂起功能)里面调用
因为主线程执行速度太快了,调用load方法还要延迟2S钟。所以新的线程还没开始,主线程就已经运行结束了。
4.前面用的是GlobalScope,为了解决速度过快导致子线程无法开启的问题,我们可以延长主线程的执行时间,也delay一下,但是delay是一个挂起方法,所以我们使用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运行结果
可以发现由于主线程有延迟,而且延迟时间多于子线程,所以子线程执行完毕之后,主线程才结束。
三、lunch和asyno
1.只用runBlocking,不用GlobalScope的话,那么整个runBlocking都是协程域,整个协程都被挂起,不会有阻塞。
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()}")
}
执行结果
很明显它是按照顺序执行的,因为它们都在同一个域里面。
2.使用launch创建协程
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执行结果
launch并没有阻塞主线程的执行,如果再添加一个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()}")
}
两个launch执行结果
还是没有阻塞主线程,但是在执行1的时候,遇到了delay,所以1被挂起,执行2,然后1结束,2结束。
使用measureTimeMillis方法来计算挂起的时间
fun main()= runBlocking{
println("main start ${Thread.currentThread()}")
val time = measureTimeMillis {
launch {
loadTask1()
}
launch {
loadTask2()
}
}
println("time $time")
println("main end ${Thread.currentThread()}")
}
计算结果为15ms
为什么会出现这个结果呢?因为我们打印的只是分配的时间,并不是执行的时间。当我们查看launch的源码,发现里面有一个join方法,它的作用是挂起一个协程知道它结束为止。
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()}")
}
调用join方法的执行结果
调用join方法,发现end main到最后去了。
所以我们可以发现这里协程是同步执行的,也就是执行完了1才会执行2,由于线程开启关闭还需要时间,所以比2s多一点时间。
3.使用async创建协程。
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()}")
}
异步执行结果
async是异步执行的。谁先读取完就执行谁的,没有顺序。很明显异步执行要比同步执行的时间短。
四、CoroutineScope和CoroutineContext
1.当我们执行以下代码时,会得到如下结果:
fun main(){
runBlocking {
println("1: ${Thread.currentThread()}")
launch {
println("2: ${Thread.currentThread()}")
}
println("3: ${Thread.currentThread()}")
}
}
执行结果
把launch改为async,结果也是一样的。这说明就算开启协程的方式不同,但是线程是一样的,它并不会开启新的线程。
2.CoroutineScope:使用launch和async时,都是创建一个新的scope
3.CoroutineContext:使用launch和async时,和parent scope在同一个context中
五、withContext切换线程
1.Dispatchers:调度器。
2.线程的切换主要有:
Dispatchers.Main - 使用此调度程序可在 Android 主线程上运行协程。此调度程序只能用于与界面交互和执行快速工作。示例包括调用
suspend
函数,运行 Android 界面框架操作,以及更新LiveData
对象。Dispatchers.IO - 此调度程序经过了专门优化,适合在主线程之外执行磁盘或网络 I/O。示例包括使用 Room 组件、从文件中读取数据或向文件中写入数据,以及运行任何网络操作。
Dispatchers.Default - 此调度程序经过了专门优化,适合在主线程之外执行占用大量 CPU 资源的工作。用例示例包括对列表排序和解析 JSON。
2.模拟一下用户登录的过程。在网络上先读取用户的id,再获取用户的信息。这是切换线程的一种方式,从io线程切换到main线程(在io线程运行结束后,会自动切换到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("开始login")
delay(1000)
println("login成功")
return 1001
}
suspend fun userInfo(id:Int):User{
println("获取用户信息:$id")
delay(1000)
println("获取用户信息成功")
return User("jack")
}
运行结果运行结果如下:
3.如果一个任务既要在主线程执行,又要在子线程执行,那么我建议先指定在主线程,需要子线程的时候再指定IO线程,这样它最终只会进行一次跳转。
fun main(){
runBlocking {
launch (Dispatchers.Main){
launch (Dispatchers.IO){
}
}
}
}
还有一种就是在函数内部就提前指定好线程。这样直接调用函数更容易理解。
fun main(){
runBlocking {
val id= login()
val user= userInfo(id)
println("user: ${user.name}")
}
}
suspend fun login():Int{
return withContext(Dispatchers.IO){
println("开始login")
delay(1000)
println("login成功")
1001 //默认返回值
}
}
suspend fun userInfo(id:Int):User{
return withContext(Dispatchers.IO){
println("获取用户信息:$id")
delay(1000)
println("获取用户信息成功")
User("jack")
}
}
运行结果
4.使用withContext切换线程。这个还是没有上面的那个好。
launch (Dispatchers.Main){
withContext(Dispatchers.IO){
login()
}
userInfo()
}
}
5.与基于回调的等效实现相比,
withContext()
不会增加额外的开销。此外,在某些情况下,还可以优化withContext()
调用,使其超越基于回调的等效实现。例如,如果某个函数对一个网络进行十次调用,您可以使用外部withContext()
让 Kotlin 只切换一次线程。这样,即使网络库多次使用withContext()
,它也会留在同一调度程序上,并避免切换线程。此外,Kotlin 还优化了Dispatchers.Default
与Dispatchers.IO
之间的切换,以尽可能避免线程切换。
重要提示:利用一个使用线程池的调度程序(例如 Dispatchers.IO 或 Dispatchers.Default)不能保证块在同一线程上从上到下执行。在某些情况下,Kotlin 协程在 suspend 和 resume 后可能会将执行工作移交给另一个线程。这意味着,对于整个 withContext() 块,线程局部变量可能并不指向同一个值。
6.使用withContext其实还是会阻塞主线程,如果想不阻塞主线程的话,另外开启一个新线程来调用login()和userInfo()函数,这样只会阻塞当前线程,不会阻塞主线程。
六、啰嗦OkHttp
1.先向gradle中导入依赖库
androidTestImplementation 'androidx.test.espresso:espresso-android:3.3.0'
2.进入OkHttp官网https://square.github.io/okhttp/,找到Releases导入依赖库。
androidTestImplementation 'androidx.test.espresso:espresso-android:3.3.0'
3.OkHttp3实际上一个封装,封装的内容包括:
OkHttpClient:提供给用户,用户通过这个来创建OkHttp,也就是对象。
Request:包括请求的地址和其他信息。
Call接口:里面定义了一些操作。真正来操作的是一个RealCall对象。
okio:真正做传输的核心。
4.application->okhttp3->Caching或服务器。在application与okhttp3之间还有一个拦截器(NetWorkInterpreter,拦截网络的每一个操作,也就是获取详细信息)
OkHttpClient里面有一个call对象指向RealCall,还有一个Dispatchers,Caching
Request包括url,method
5.具体的使用详见https://square.github.io/okhttp/recipes/
请求详情七、okhttp获取数据
1.数据有两种类型:
XML :几乎不用
JSON:将JSON的数据转化为kotlin里面的数据类型。
这里需要使用聚合数据,我们先提前在聚合数据里面申请一个账号,然后选择API里面的免费项目,比如新闻头条,按照它的格式发送请求
解析之后在网页中输入以下网址:https://v.juhe.cn/toutiao/index?type=&page=&page_size=&is_filter=&key=4494d20d3e853ec01a1dafc8b901e716,然后打开一个JSON解析器,将网页内的数据转化为我们能看懂的代码。
result展开之后解析之后折叠起来,只有三个元素,相当于三个map(包含key和value)
展开result:
activity_main布局2.布局一下activity_main,我的布局加了一个按钮和一个progressBar:
3.新建一个工程,向里面导入我们需要的依赖库。
//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.我们要根据前面获取到的信息做一个新闻展示页面,使用MVVM模式,所以要另外创建一个类作为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.运行成功之后就能看到打印出来的结果了。
新闻头条八、聚合数据头条新闻API说明
1.OkHttp3和Retrofit请求数据要使用的知识点
聚合数据API使用
OkHttp3请求数据
Gson解析json数据
json To kotlin 插件使用
Retrofit请求数据的步骤
2.进入聚合数据官网https://www.juhe.cn/首页,选择生活服务,进入新闻头条板块。
type和key3.http://v.juhe.cn/toutiao/index?type=top&key=APPKEY,按照这个格式就可以得到一个接口的地址http://v.juhe.cn/toutiao/index?type=guonei&key=4494d20d3e853ec01a1dafc8b901e716,type和key见下面的图片。
解析结果4.将我们按照需求设计好的网址在浏览器中输入,最后可以得到一串数据,将其放在json解析器里面进行解析。解析结果差不多如下图所示:
九、使用OkHttp3获取数据
1.进入github官网,搜一下okhttp,点进去第一个,查看详细信息。https://github.com/square/okhttp
2.添加依赖库。
implementation("com.squareup.okhttp3:okhttp:4.9.1")
3.我们新建一个项目工程先测试一下,把依赖库导进去,然后在manifest里面添加以下代码。后面那个是在<application>里面。
<uses-permission android:name="android.permission.INTERNET"/>
android:usesCleartextTraffic="true"
4.在MainActivity里面编写一下代码。
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","下载的内容为:$BodyStr")
}
}
})
}
return super.onTouchEvent(event)
}
}
运行结果最后的结果如下所示,这说明我们解析成了。
十、手动创建数据模型
1.按照前面的方法,数据是获取到了,但是无法直接加进我们的项目中,所以我们要先建一个数据模型。
2.打开github,搜索Gson,点击第一个。进入以下网住https://github.com/google/gson
3.根据上面的网址,导入一下依赖库。
implementation 'com.google.code.gson:gson:2.8.7'
4.创建一个数据类,NewsModel,里面包含的数据和json解析器里面解析出来的数据类似。
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)
下载插件十一、使用插件自动创建模型
1.前面我们手动来创建模型,其实是很麻烦的,对于结构比较清晰的数据来说没问题,但如果对于结构比较复杂的数据就很麻烦了,很容易出错。这时候就可以使用插件了。
2.在Android Studio里面打开设置,点击plungs,搜索JSON,下载第一个即可。
创建过程3.然后new一个kotlin data class file from json,把http://v.juhe.cn/toutiao/index?type=guonei&key=4494d20d3e853ec01a1dafc8b901e716这个网址的内容全部拷贝进去,一个都不能漏,Annotation记得勾Gson,其他都不变。
4.自动创建好的代码如下图所示,我已经把不需要的删掉了。
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里面,在前面的代码后面进行解析。
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.运行结果如下图所示:
十二、使用retrofit获取数据
1.retrofit相比于okhttp更简单,更简洁,所以用它更好。去https://square.github.io/retrofit/网站查看它的使用方法。
2.导入一下依赖库。
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.创建一个接口,作为API。
interface NewsAPI {
@GET("index?type=guonei&key=4494d20d3e853ec01a1dafc8b901e716")
suspend fun getNews():NewsModel
}
4.在MainActivity里面写一个useRetrofit()方法,使用Retrofit来获取数据。在onTouchEvent里面调用这个方法。
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)
}
}
}
运行结果
因为新闻每3分钟就会刷新一次,所以新闻标题和前面不一样。