RxJava與Retrofit
在出現(xiàn)LiveData之前兵多,Android上實(shí)現(xiàn)網(wǎng)絡(luò)請(qǐng)求最常用的方式是使用Retrofit+Rxjava。通常是RxJavaCallAdapterFactory
將請(qǐng)求轉(zhuǎn)成Observable
(或者Flowable
等)被觀察者對(duì)象,調(diào)用時(shí)通過(guò)subscribe
方式實(shí)現(xiàn)最終的請(qǐng)求姐叁。為了實(shí)現(xiàn)線程切換日杈,需要將訂閱時(shí)的線程切換成io線程嗤栓,請(qǐng)求完成通知被觀察者時(shí)切換成ui線程。代碼通常如下:
observable.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(subscriber)
為了能夠讓請(qǐng)求監(jiān)聽(tīng)到生命周期變化治唤,onDestroy時(shí)不至于發(fā)生view空指針棒动,要需要使用RxLifecycle或AutoDispose讓Observable
能夠監(jiān)聽(tīng)到Activity和Fragment的生命周期,在適當(dāng)?shù)纳芷谙氯∠嗛啞?/p>
LiveData與Retrofit
LiveData
和Rxjava中的Observable
類(lèi)似宾添,是一個(gè)被觀察者的數(shù)據(jù)持有類(lèi)船惨。但是不同的是LiveData
具有生命周期感知,相當(dāng)于RxJava+RxLifecycle缕陕。LiveData
使用起來(lái)相對(duì)簡(jiǎn)單輕便粱锐,所以當(dāng)它加入到項(xiàng)目中后,再使用RxJava便顯得重復(fù)臃腫了(RxJava包1~2M容量)扛邑。為了移除RxJava怜浅,我們將Retrofit的Call
請(qǐng)求適配成LiveData
,因此我們需要自定義CallAdapterFactory
蔬崩。根據(jù)接口響應(yīng)格式不同恶座,對(duì)應(yīng)的適配器工廠會(huì)有所區(qū)別。本次便以廣為人知的wanandroid的api為例子沥阳,來(lái)完成LiveData網(wǎng)絡(luò)請(qǐng)求實(shí)戰(zhàn)跨琳。
首先根據(jù)它的響應(yīng)格式:
{
data:[],//或者{}
errorCode:0,
errorMsg:""
}
定義一個(gè)通用的響應(yīng)實(shí)體ApiResponse
class ApiResponse<T>(
var data: T?,
var errorCode: Int,
var errorMsg: String
)
然后我們定義對(duì)應(yīng)的LiveDataCallAdapterFactory
import androidx.lifecycle.LiveData
import retrofit2.CallAdapter
import retrofit2.Retrofit
import java.lang.reflect.Type
import retrofit2.CallAdapter.Factory
import java.lang.reflect.ParameterizedType
class LiveDataCallAdapterFactory : Factory() {
override fun get(returnType: Type, annotations: Array<Annotation>, retrofit: Retrofit): CallAdapter<*, *>? {
if (getRawType(returnType) != LiveData::class.java) return null
//獲取第一個(gè)泛型類(lèi)型
val observableType = getParameterUpperBound(0, returnType as ParameterizedType)
val rawType = getRawType(observableType)
if (rawType != ApiResponse::class.java) {
throw IllegalArgumentException("type must be ApiResponse")
}
if (observableType !is ParameterizedType) {
throw IllegalArgumentException("resource must be parameterized")
}
return LiveDataCallAdapter<Any>(observableType)
}
}
然后在LiveDataCallAdapter
將Retrofit的Call
對(duì)象適配成LiveData
import androidx.lifecycle.LiveData
import retrofit2.Call
import retrofit2.CallAdapter
import retrofit2.Callback
import retrofit2.Response
import java.lang.reflect.Type
import java.util.concurrent.atomic.AtomicBoolean
class LiveDataCallAdapter<T>(private val responseType: Type) : CallAdapter<T, LiveData<T>> {
override fun adapt(call: Call<T>): LiveData<T> {
return object : LiveData<T>() {
private val started = AtomicBoolean(false)
override fun onActive() {
super.onActive()
if (started.compareAndSet(false, true)) {//確保執(zhí)行一次
call.enqueue(object : Callback<T> {
override fun onFailure(call: Call<T>, t: Throwable) {
val value = ApiResponse<T>(null, -1, t.message ?: "") as T
postValue(value)
}
override fun onResponse(call: Call<T>, response: Response<T>) {
postValue(response.body())
}
})
}
}
}
}
override fun responseType() = responseType
}
第一個(gè)請(qǐng)求
以首頁(yè)banner接口(https://www.wanandroid.com/banner/json)為例,完成第一個(gè)請(qǐng)求桐罕。
新建一個(gè)WanApi接口脉让,加入Banner列表api,以及Retrofit初始化方法,為方便查看http請(qǐng)求和響應(yīng)功炮,加入了okhttp自帶的日志攔截器溅潜。
interface WanApi {
companion object {
fun get(): WanApi {
val clientBuilder = OkHttpClient.Builder()
.connectTimeout(60, TimeUnit.SECONDS)
if (BuildConfig.DEBUG) {
val loggingInterceptor = HttpLoggingInterceptor()
loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
clientBuilder.addInterceptor(loggingInterceptor)
}
return Retrofit.Builder()
.baseUrl("https://www.wanandroid.com/")
.client(clientBuilder.build())
.addCallAdapterFactory(LiveDataCallAdapterFactory())
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(WanApi::class.java)
}
}
/**
* 首頁(yè)banner
*/
@GET("banner/json")
fun bannerList(): LiveData<ApiResponse<List<BannerVO>>>
}
BannerVO
實(shí)體
data class BannerVO(
var id: Int,
var title: String,
var desc: String,
var type: Int,
var url: String,
var imagePath:String
)
我們?cè)贛ainActivity中發(fā)起請(qǐng)求
private fun loadData() {
val bannerList = WanApi.get().bannerList()
bannerList.observe(this, Observer {
Log.e("main", "res:$it")
})
}
調(diào)試結(jié)果如下:
LiveData的map與switchMap操作
LiveData
可以通過(guò)Transformations
的map和switchMap操作,將一個(gè)LiveData轉(zhuǎn)成另一種類(lèi)型的LiveData死宣,效果與RxJava的map/switchMap操作符類(lèi)似伟恶。可以看看兩個(gè)函數(shù)的聲明
public static <X, Y> LiveData<Y> map(
@NonNull LiveData<X> source,
@NonNull final Function<X, Y> mapFunction)
public static <X, Y> LiveData<Y> switchMap(
@NonNull LiveData<X> source,
@NonNull final Function<X, LiveData<Y>> switchMapFunction)
根據(jù)以上代碼毅该,我們可以知道博秫,對(duì)應(yīng)的變換函數(shù)返回的類(lèi)型是不一樣的:map是基于泛型類(lèi)型的變換,而switchMap則返回一個(gè)新的LiveData
眶掌。
還是以banner請(qǐng)求為例挡育,我們將map和switchMap應(yīng)用到實(shí)際場(chǎng)景中:
1: 為了能夠手動(dòng)控制請(qǐng)求,我們需要一個(gè)refreshTrigger
觸發(fā)變量朴爬,當(dāng)這個(gè)變量被設(shè)置為true時(shí)即寒,通過(guò)switchMap生成一個(gè)新的LiveData
用作請(qǐng)求banner
private val refreshTrigger = MutableLiveData<Boolean>()
private val api = WanApi.get()
private val bannerLis:LiveData<ApiResponse<List<BannerVO>>> = Transformations.switchMap(refreshTrigger) {
//當(dāng)refreshTrigger的值被設(shè)置時(shí),bannerList
api.bannerList()
}
2: 為了展示banner,我們通過(guò)map將ApiResponse
轉(zhuǎn)換成最終關(guān)心的數(shù)據(jù)是List<BannerVO>
val banners: LiveData<List<BannerVO>> = Transformations.map(bannerList) {
it.data ?: ArrayList()
}
LiveData與ViewModel結(jié)合
為了將LiveData
與Activity
解耦母赵,我們通過(guò)ViewModel
來(lái)管理這些LiveData
逸爵。
class HomeVM : ViewModel() {
private val refreshTrigger = MutableLiveData<Boolean>()
private val api = WanApi.get()
private val bannerList: LiveData<ApiResponse<List<BannerVO>>> = Transformations.switchMap(refreshTrigger) {
//當(dāng)refreshTrigger的值被設(shè)置時(shí),bannerList
api.bannerList()
}
val banners: LiveData<List<BannerVO>> = Transformations.map(bannerList) {
it.data ?: ArrayList()
}
fun loadData() {
refreshTrigger.value = true
}
}
在activity_main.xml中加入banner布局凹嘲,這里使用BGABanner-Android來(lái)顯示圖片
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<data>
<variable
name="vm"
type="io.github.iamyours.wandroid.ui.home.HomeVM"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<cn.bingoogolapple.bgabanner.BGABanner
android:id="@+id/banner"
android:layout_width="match_parent"
android:layout_height="120dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
app:banner_indicatorGravity="bottom|right"
app:banner_isNumberIndicator="true"
app:banner_pointContainerBackground="#0000"
app:banner_transitionEffect="zoom"/>
<TextView
android:layout_width="match_parent"
android:layout_height="44dp"
android:background="#ccc"
android:gravity="center"
android:onClick="@{()->vm.loadData()}"
android:text="加載Banner"/>
</LinearLayout>
</layout>
然后在MainActivity完成Banner初始化,通過(guò)監(jiān)聽(tīng)ViewModel中的banners實(shí)現(xiàn)輪播圖片的展示师倔。
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
val vm = ViewModelProviders.of(this).get(HomeVM::class.java)
binding.lifecycleOwner = this
binding.vm = vm
initBanner()
}
private fun initBanner() {
binding.run {
val bannerAdapter = BGABanner.Adapter<ImageView, BannerVO> { _, image, model, _ ->
image.displayWithUrl(model?.imagePath)
}
banner.setAdapter(bannerAdapter)
vm?.banners?.observe(this@MainActivity, Observer {
banner.setData(it, null)
})
}
}
}
最終效果如下:
加載進(jìn)度顯示
SwipeRefreshLayout
請(qǐng)求網(wǎng)絡(luò)過(guò)程中,必不可少的是加載進(jìn)度的展示周蹭。這里我們列舉兩種常用的的加載方式趋艘,一種在布局中的進(jìn)度條(如SwipeRefreshLayout
),另一種是加載對(duì)話(huà)框凶朗。
為了控制加載進(jìn)度條顯示隱藏瓷胧,我們?cè)?code>HomeVM中添加loading變量,在調(diào)用loadData時(shí)通過(guò)loading.value=true
控制進(jìn)度條的顯示棚愤,在map
中的轉(zhuǎn)換函數(shù)中控制進(jìn)度的隱藏
val loading = MutableLiveData<Boolean>()
val banners: LiveData<List<BannerVO>> = Transformations.map(bannerList) {
loading.value = false
it.data ?: ArrayList()
}
fun loadData() {
refreshTrigger.value = true
loading.value = true
}
我們?cè)赼ctivity_main.xml的外層嵌套一個(gè)SwipeRefreshLayout
搓萧,通過(guò)databinding設(shè)置加載狀態(tài),添加刷新事件
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:onRefreshListener="@{() -> vm.loadData()}"
app:refreshing="@{vm.loading}">
...
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
然后我們?cè)倏聪滦Ч?/p>
加載對(duì)話(huà)框KProgressHUD
為了能和ViewModel解藕遇八,我們將加載對(duì)話(huà)框封裝到一個(gè)Observer中矛绘。
class LoadingObserver(context: Context) : Observer<Boolean> {
private val dialog = KProgressHUD(context)
.setStyle(KProgressHUD.Style.SPIN_INDETERMINATE)
.setCancellable(false)
.setAnimationSpeed(2)
.setDimAmount(0.5f)
override fun onChanged(show: Boolean?) {
if (show == null) return
if (show) {
dialog.show()
} else {
dialog.dismiss()
}
}
}
然后在MainActivity添加這個(gè)Observer
vm.loading.observe(this, LoadingObserver(this))
效果:
我們還可以將
LoadingObserver
注冊(cè)到BaseActivity
class BaseActivity : AppCompatActivity() {
val loadingState = MutableLiveData<Boolean>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
loadingState.observe(this, LoadingObserver(this))
}
}
然后在HomeVM
中添加一個(gè)attachLoading
方法
class HomeVM:ViewModel{
fun attachLoading(otherLoadingState: MutableLiveData<Boolean>) {
loading.observeForever {
otherLoadingState.value = it
}
}
}
最終如果想要顯示進(jìn)度對(duì)話(huà)框,在BaseActivity
到子類(lèi)中刃永,只需調(diào)用vm.attachLoading(loadingState)
即可货矮。
分頁(yè)請(qǐng)求
分頁(yè)請(qǐng)求是另個(gè)一常用請(qǐng)求,它的請(qǐng)求狀態(tài)就比刷新數(shù)據(jù)多了幾種斯够。以wanandroid首頁(yè)文章列表api為例囚玫,我們?cè)?code>HomeVM中加入page
,refreshing
,moreLoading
,hasMore
變量控制分頁(yè)請(qǐng)求
private val page = MutableLiveData<Int>() //分頁(yè)數(shù)據(jù)
val refreshing = MutableLiveData<Boolean>()//下拉刷新?tīng)顟B(tài)
val moreLoading = MutableLiveData<Boolean>()//上拉加載更多狀態(tài)
val hasMore = MutableLiveData<Boolean>()//是否還有更多數(shù)據(jù)
private val articleList = Transformations.switchMap(page) {
api.articleList(it)
}
val articlePage = Transformations.map(articleList) {
refreshing.value = false
moreLoading.value = false
hasMore.value = !(it?.data?.over ?: false)
it.data
}
fun loadMore() {
page.value = (page.value ?: 0) + 1
moreLoading.value = true
}
fun refresh() {
loadBanner()
page.value = 0
refreshing.value = true
}
用SmartRefreshLayout作為分頁(yè)組件读规,來(lái)實(shí)現(xiàn)WanAndroid首頁(yè)文章列表數(shù)據(jù)的展示抓督。
綁定SmartRefreshLayout屬性和事件
通過(guò)@BindingAdapter
注解,將綁定SmartRefreshLayout屬性和事件封裝一樣束亏,便于我們?cè)诓季治募ㄟ^(guò)databinding控制它铃在。
新建一個(gè)CommonBinding.kt
文件,注意在gradle中引入kotlin-kapt
@BindingAdapter(value = ["refreshing", "moreLoading", "hasMore"], requireAll = false)
fun bindSmartRefreshLayout(
smartLayout: SmartRefreshLayout,
refreshing: Boolean,
moreLoading: Boolean,
hasMore: Boolean
) {
if (!refreshing) smartLayout.finishRefresh()
if (!moreLoading) smartLayout.finishLoadMore()
smartLayout.setEnableLoadMore(hasMore)
}
@BindingAdapter(value = ["onRefreshListener", "onLoadMoreListener"], requireAll = false)
fun bindListener(
smartLayout: SmartRefreshLayout,
refreshListener: OnRefreshListener?,
loadMoreListener: OnLoadMoreListener?
) {
smartLayout.setOnRefreshListener(refreshListener)
smartLayout.setOnLoadMoreListener(loadMoreListener)
}
然后在布局中使用
<layout 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">
<data>
<variable
name="vm"
type="io.github.iamyours.wandroid.ui.home.HomeVM"/>
</data>
<com.scwang.smartrefresh.layout.SmartRefreshLayout
android:id="@+id/refreshLayout"
android:layout_width="match_parent"
app:onRefreshListener="@{()->vm.refresh()}"
app:refreshing="@{vm.refreshing}"
app:moreLoading="@{vm.moreLoading}"
app:hasMore="@{vm.hasMore}"
app:onLoadMoreListener="@{()->vm.loadMore()}"
android:layout_height="match_parent">
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:orientation="vertical"
android:layout_height="wrap_content">
<cn.bingoogolapple.bgabanner.BGABanner
android:id="@+id/banner"
android:layout_width="match_parent"
android:layout_height="140dp"
app:banner_indicatorGravity="bottom|right"
app:banner_isNumberIndicator="true"
app:banner_pointContainerBackground="#0000"
app:banner_transitionEffect="zoom"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_marginTop="5dp"
tools:listitem="@layout/item_article"
android:layout_height="wrap_content"/>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</com.scwang.smartrefresh.layout.SmartRefreshLayout>
</layout>
分頁(yè)實(shí)現(xiàn)
然后在MainActivity
中完成RecyclerView的邏輯
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
private val adapter = ArticleAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
val vm = ViewModelProviders.of(this).get(HomeVM::class.java)
binding.lifecycleOwner = this
binding.vm = vm
binding.executePendingBindings()
initBanner()
initRecyclerView()
binding.refreshLayout.autoRefresh()
}
private fun initRecyclerView() {
binding.recyclerView.let {
it.adapter = adapter
it.layoutManager = LinearLayoutManager(this)
}
binding.vm?.articlePage?.observe(this, Observer {
it?.run {
if (curPage == 1) {
adapter.clearAddAll(datas)
} else {
adapter.addAll(datas)
}
}
})
}
private fun initBanner() {
...
}
}
最終效果: