retrofit content-length为0_LiveData+Retrofit 网络请求实战
作者:星星y
鏈接:https://www.jianshu.com/p/34fb6ffaa684
聲明:本文已獲星星y授權發表,轉發等請聯系原作者授權
RxJava與Retrofit
在出現LiveData之前,Android上實現網絡請求最常用的方式是使用Retrofit+Rxjava。通常是RxJavaCallAdapterFactory將請求轉成Observable(或者Flowable等)被觀察者對象,調用時通過subscribe方式實現最終的請求。為了實現線程切換,需要將訂閱時的線程切換成io線程,請求完成通知被觀察者時切換成ui線程。代碼通常如下:
observable.subscribeOn(Schedulers.io())??????????.observeOn(AndroidSchedulers.mainThread())
??????????.subscribe(subscriber)
為了能夠讓請求監聽到生命周期變化,onDestroy時不至于發生view空指針,要需要使用RxLifecycle或AutoDispose讓Observable能夠監聽到Activity和Fragment的生命周期,在適當的生命周期下取消訂閱。
LiveData與Retrofit
LiveData和Rxjava中的Observable類似,是一個被觀察者的數據持有類。但是不同的是LiveData具有生命周期感知,相當于RxJava+RxLifecycle。LiveData使用起來相對簡單輕便,所以當它加入到項目中后,再使用RxJava便顯得重復臃腫了(RxJava包1~2M容量)。為了移除RxJava,我們將Retrofit的Call請求適配成LiveData,因此我們需要自定義CallAdapterFactory。根據接口響應格式不同,對應的適配器工廠會有所區別。本次便以廣為人知的wanandroid的api為例子,來完成LiveData網絡請求實戰。
首先根據它的響應格式:
{????data:[],//或者{}
????errorCode:0,
????errorMsg:""
}
定義一個通用的響應實體ApiResponse
class?ApiResponse<T>(????var?data:?T?,
????var?errorCode:?Int,
????var?errorMsg:?String
)
然后我們定義對應的LiveDataCallAdapterFactory
class?LiveDataCallAdapterFactory?:?Factory()?{????override?fun?get(returnType:?Type,?annotations:?Array,?retrofit:?Retrofit):?CallAdapter??{
????????if?(getRawType(returnType)?!=?LiveData::class.java)?return?null
????????//獲取第一個泛型類型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(observableType)
????}
}
然后在LiveDataCallAdapter將Retrofit的Call對象適配成LiveData
class?LiveDataCallAdapter<T>(private?val?responseType:?Type)?:?CallAdapter>?{override?fun?adapt(call:?Call<T>):?LiveData?{return?object?:?LiveData()?{private?val?started?=?AtomicBoolean(false)override?fun?onActive()?{super.onActive()if?(started.compareAndSet(false,?true))?{//確保執行一次????????????????????call.enqueue(object?:?Callback?{override?fun?onFailure(call:?Call<T>,?t:?Throwable)?{val?value?=?ApiResponse(null,?-1,?t.message??:?"")?as?T
????????????????????????????postValue(value)
????????????????????????}override?fun?onResponse(call:?Call<T>,?response:?Response<T>)?{
????????????????????????????postValue(response.body())
????????????????????????}
????????????????????})
????????????????}
????????????}
????????}
????}override?fun?responseType()?=?responseType
}
第一個請求
以首頁banner接口(https://www.wanandroid.com/banner/json)為例,完成第一個請求。
新建一個WanApi接口,加入Banner列表api,以及Retrofit初始化方法,為方便查看http請求和響應,加入了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)
????????}
????}
????/**
?????*?首頁banner
?????*/
????@GET("banner/json")
????fun?bannerList():?LiveData>>
}
BannerVO實體
data?class?BannerVO(????var?id:?Int,
????var?title:?String,
????var?desc:?String,
????var?type:?Int,
????var?url:?String,
????var?imagePath:String
)\
我們在MainActivity中發起請求
?private?fun?loadData()?{????val?bannerList?=?WanApi.get().bannerList()
????bannerList.observe(this,?Observer?{
????????Log.e("main",?"res:$it")
????})
?}
調試結果如下:
banner請求結果LiveData的map與switchMap操作
LiveData可以通過Transformations的map和switchMap操作,將一個LiveData轉成另一種類型的LiveData,效果與RxJava的map/switchMap操作符類似。可以看看兩個函數的聲明
public?static??LiveData?map(@NonNull?LiveData?source,@NonNull?final?Function?mapFunction)public?static??LiveData?switchMap(@NonNull?LiveData?source,@NonNull?final?Function>?switchMapFunction)根據以上代碼,我們可以知道,對應的變換函數返回的類型是不一樣的:map是基于泛型類型的變換,而switchMap則返回一個新的LiveData。
還是以banner請求為例,我們將map和switchMap應用到實際場景中:
1: 為了能夠手動控制請求,我們需要一個refreshTrigger觸發變量,當這個變量被設置為true時,通過switchMap生成一個新的LiveData用作請求banner
private?val?api?=?WanApi.get()
private?val?bannerLis:LiveData>>?=?Transformations.switchMap(refreshTrigger)?{//當refreshTrigger的值被設置時,bannerList
????api.bannerList()
}
2: 為了展示banner,我們通過map將ApiResponse轉換成最終關心的數據是List
val banners: LiveData> = Transformations.map(bannerList) {
? ?it.data ?: ArrayList()
}
LiveData與ViewModel結合
為了將LiveData與Activity解耦,我們通過ViewModel來管理這些LiveData。
class?HomeVM?:?ViewModel()?{????private?val?refreshTrigger?=?MutableLiveData<Boolean>()
????private?val?api?=?WanApi.get()
????private?val?bannerList:?LiveData>>?=?Transformations.switchMap(refreshTrigger)?{//當refreshTrigger的值被設置時,bannerList
????????api.bannerList()
????}val?banners:?LiveData>?=?Transformations.map(bannerList)?{
????????it.data??:?ArrayList()
????}fun?loadData()?{
????????refreshTrigger.value?=?true
????}
}
在activity_main.xml中加入banner布局,這里使用BGABanner-Android來顯示圖片
<?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>
????????<variablename="vm"type="io.github.iamyours.wandroid.ui.home.HomeVM"/>
????data>
????<LinearLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical">
????????<cn.bingoogolapple.bgabanner.BGABannerandroid: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"/>
????????<TextViewandroid: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初始化,通過監聽ViewModel中的banners實現輪播圖片的展示。
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?{?_,?image,?model,?_?->
????????????????image.displayWithUrl(model?.imagePath)
????????????}
????????????banner.setAdapter(bannerAdapter)
????????????vm?.banners?.observe(this@MainActivity,?Observer?{
????????????????banner.setData(it,?null)
????????????})
????????}
????}
}
最終效果如下:
banner加載進度顯示
SwipeRefreshLayout
請求網絡過程中,必不可少的是加載進度的展示。這里我們列舉兩種常用的的加載方式,一種在布局中的進度條(如SwipeRefreshLayout),另一種是加載對話框。
為了控制加載進度條顯示隱藏,我們在HomeVM中添加loading變量,在調用loadData時通過loading.value=true控制進度條的顯示,在map中的轉換函數中控制進度的隱藏
val?loading?=?MutableLiveData<Boolean>()val?banners:?LiveData>?=?Transformations.map(bannerList)?{
????loading.value?=?false
????it.data??:?ArrayList()
}fun?loadData()?{
????refreshTrigger.value?=?true
????loading.value?=?true
}
我們在activity_main.xml的外層嵌套一個SwipeRefreshLayout,通過databinding設置加載狀態,添加刷新事件
<androidx.swiperefreshlayout.widget.SwipeRefreshLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"app:onRefreshListener="@{()?->?vm.loadData()}"app:refreshing="@{vm.loading}">????????...
androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
然后我們再看下效果:
SwipeRefreshLayout進度控制加載對話框KProgressHUD
為了能和ViewModel解藕,我們將加載對話框封裝到一個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添加這個Observer
vm.loading.observe(this,?LoadingObserver(this))效果:
加載對話框顯示我們還可以將LoadingObserver注冊到BaseActivity
class?BaseActivity?:?AppCompatActivity()?{????val?loadingState?=?MutableLiveData<Boolean>()
????override?fun?onCreate(savedInstanceState:?Bundle?)?{
????????super.onCreate(savedInstanceState)
????????loadingState.observe(this,?LoadingObserver(this))
????}
}
然后在HomeVM中添加一個attachLoading方法
class?HomeVM:ViewModel{?????fun?attachLoading(otherLoadingState:?MutableLiveData<Boolean>)?{
????????loading.observeForever?{
????????????otherLoadingState.value?=?it
????????}
????}
}
最終如果想要顯示進度對話框,在BaseActivity到子類中,只需調用vm.attachLoading(loadingState)即可。
分頁請求
分頁請求是另個一常用請求,它的請求狀態就比刷新數據多了幾種。以wanandroid首頁文章列表api為例,我們在HomeVM中加入page,refreshing,moreLoading,hasMore變量控制分頁請求
private?val?page?=?MutableLiveData<Int>()?//分頁數據val?refreshing?=?MutableLiveData<Boolean>()//下拉刷新狀態
val?moreLoading?=?MutableLiveData<Boolean>()//上拉加載更多狀態
val?hasMore?=?MutableLiveData<Boolean>()//是否還有更多數據
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作為分頁組件,來實現WanAndroid首頁文章列表數據的展示。
綁定SmartRefreshLayout屬性和事件
通過@BindingAdapter注解,將綁定SmartRefreshLayout屬性和事件封裝一樣,便于我們在布局文件通過databinding控制它。
新建一個CommonBinding.kt文件,注意在gradle中引入kotlin-kapt
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)
}
然后在布局中使用
"http://schemas.android.com/apk/res/android"????????xmlns:app="http://schemas.android.com/apk/res-auto"
????????xmlns:tools="http://schemas.android.com/tools">
????<data>????????????????name="vm"
????????????????type="io.github.iamyours.wandroid.ui.home.HomeVM"/>data>????????????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">????????????????android:layout_width="match_parent"
????????????????android:layout_height="match_parent">????????????????????android:layout_width="match_parent"
????????????????????android:orientation="vertical"
????????????????????android:layout_height="wrap_content">????????????????????????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"/>????????????????????????android:id="@+id/recyclerView"
????????????????????????android:layout_width="match_parent"
????????????????????????android:layout_marginTop="5dp"
????????????????????????tools:listitem="@layout/item_article"
????????????????????????android:layout_height="wrap_content"/>
分頁實現
然后在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()?{
???????...
????}
}
最終效果:
wanandroid首頁數據項目地址https://github.com/iamyours/Wandroid
推薦閱讀
Android 官方架構組件(二)——LiveData
LiveData 源碼分析之事件總線 LiveBus 實現
掃一掃 關注我的公眾號
如果你想要跟大家分享你的文章,歡迎投稿~
總結
以上是生活随笔為你收集整理的retrofit content-length为0_LiveData+Retrofit 网络请求实战的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux如何查看jupyter日志_M
- 下一篇: if语句 power query_Pow