在开发中,常常遇到需要异步完成的操作,例如网络请求。由于网络请求比较耗时,需要放在子线程去工作,而主线程可以继续与用户交互。
在子线程完成耗时的操作后,通常是通过回调来更新界面。回调的实现较为简单适合简单的场景,如果是比较复杂的场景,比如多次请求网络,下一次的请求依赖上一次的请求结果的话,这种结构的代码无论是阅读起来还是维护起来都是极其糟糕的。
//客户端顺序进行三次网络异步请求,并用最终结果更新UI
request1(parameter) { value1 ->
request2(value1) { value2 ->
request3(value2) { value3 ->
updateUI(value3)
}
}
}
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1'
fun main(args: Array<String>) {
launch(CommonPool) {
delay(1000L)
println("World!")
}
println("Hello,")
Thread.sleep(2000L)
}
/*
运行结果: ("Hello,"会立即被打印, 1000毫秒之后, "World!"会被打印)
Hello,
World!
*/
上述代码使用launch方法启动了一个协程,launch后面的花括号就是协程,花括号内的代码就是运行在协程内的代码。
接着来深入了解一下launch方法的声明:
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit): Job {...}
launch方法的参数:
返回值Job:对当前创建的协程的引用。可以通过Job的start、cancel、join等方法来控制协程的启动和取消。
除此之外为了方便我们的使用,在Google的Jetpack中也提供了一些生命周期感知型协程范围。实际开发中我们可以方便地选择适当的协程范围来为耗时操作(网络请求等)指定自动取消执行的时机,详情见:https://developer.android.google.cn/topic/libraries/architecture/coroutines
借用大佬的Demo,项目github地址
可以看到功能其实很简单,界面由一个按钮和三个图片组成。每次按下刷新按钮,就都会从网络上获取三张图片显示到界面上。从网络上获取图片的时候刷新按钮变为不可用状态,刷新完成后按钮恢复可用状态。
//添加Retrofit网络库和gsonConverter的依赖,注意一定要2.6.0版本以上
implementation 'com.squareup.retrofit2:retrofit:2.7.0'
implementation 'com.squareup.retrofit2:converter-gson:2.7.0'
//添加Jetpack中架构组件的依赖,注意viewmodel要添加viewmodel-ktx的依赖
implementation "androidx.lifecycle:lifecycle-livedata:2.1.0"
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0'
implementation "androidx.lifecycle:lifecycle-extensions:2.1.0"
//添加Glide的依赖用于图片加载
implementation 'com.github.bumptech.glide:glide:4.10.0'
<LinearLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_marginTop="10dp"
tools:context=".MainActivity">
<Button
android:id="@+id/button"
android:text="refresh"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<ImageView
android:id="@+id/imageView1"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:scaleType="centerCrop"
android:layout_marginTop="10dp"/>
<ImageView
android:id="@+id/imageView2"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:scaleType="centerCrop"
android:layout_marginTop="10dp"/>
<ImageView
android:id="@+id/imageView3"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:scaleType="centerCrop"
android:layout_marginTop="10dp"/>
LinearLayout>
数据格式很简单,我们可以很容易地创建出对应的实体类:
data class ImageDataResponseBody(
val code: String,
val imgurl: String
)
ApiService为我们网络接口的访问单例类
import com.njp.coroutinesdemo.bean.ImageDataResponseBody
import retrofit2.http.GET
import retrofit2.http.Query
//网络接口
interface ApiService {
//声明为suspend方法
@GET("image/sogou/api.php")
suspend fun getImage(@Query("type") type: String = "json"): ImageDataResponseBody
}
NetworkService为定义的网络接口:
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.create
import java.util.concurrent.TimeUnit
//网络层访问统一入口
object NetworkService {
//retorfit实例,在这里做一些统一网络配置,如添加转换器、设置超时时间等
private val retrofit = Retrofit.Builder()
.client(OkHttpClient.Builder().callTimeout(5, TimeUnit.SECONDS).build())
.baseUrl("https://api.ooopn.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
//网络层访问服务
val apiService = retrofit.create<ApiService>()
}
值得注意的是我们在定义我们的接口的时候一定要声明为suspend方法,这样就完成了对Kotlin协程的完美支持
首先由于我们的项目中要对网络加载的状态进行监听,以此来进行对刷新按钮是否可点击状态的设置和错误信息的显示。所以我们可以编写一个LoadState类来作为网络加载状态信息的承载:
sealed class LoadState(val msg: String) {
class Loading(msg: String = "") : LoadState(msg)
class Success(msg: String = "") : LoadState(msg)
class Fail(msg: String) : LoadState(msg)
}
接着我们来创建我们的ViewModel:
class MainViewModel : ViewModel() {
val imageData = MutableLiveData<List<String>>()
val loadState = MutableLiveData<LoadState>()
fun getData() {
launch(
{
loadState.value = LoadState.Loading()
val data1 = async { NetworkService.apiService.getImage() }
val data2 = async { NetworkService.apiService.getImage() }
val data3 = async { NetworkService.apiService.getImage() }
imageData.value = listOf(data1.await(), data2.await(), data3.await()).map {
it.imgurl
}
loadState.value = LoadState.Success()
},
{
loadState.value = LoadState.Fail(it.message ?: "加载失败")
}
)
}
}
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import com.bumptech.glide.Glide
import com.xxx.coroutinesdemo.R
import com.xxx.coroutinesdemo.bean.LoadState
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//获取ViewModel
viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
//对加载状态进行动态观察
viewModel.loadState.observe(this, Observer {
when (it) {
is LoadState.Success -> button.isEnabled = true
is LoadState.Fail -> {
button.isEnabled = true
Toast.makeText(this, it.msg, Toast.LENGTH_SHORT).show()
}
is LoadState.Loading -> {
button.isEnabled = false
}
}
})
//对图片Url数据进行观察
viewModel.imageData.observe(this, Observer {
//用Glide加载三张图片
Glide.with(this)
.load(it[0])
.into(imageView1)
Glide.with(this)
.load(it[1])
.into(imageView2)
Glide.with(this)
.load(it[2])
.into(imageView3)
})
//点击刷新按钮来网络加载
button.setOnClickListener {
viewModel.getData()
}
}
}