《Android編程權(quán)威指南》第 24 章啦,本章又有個(gè)新應(yīng)用啦,叫 PhotoGallery,用來(lái)獲取 Flickr 網(wǎng)站的最新公共圖片「不限版權(quán)的圖片」。本章將學(xué)習(xí) Retrofit 網(wǎng)絡(luò)請(qǐng)求庫(kù),Json 數(shù)據(jù),Gson 解析 Json 等等。
一、創(chuàng)建 PhotoGallery 應(yīng)用
按照慣例,創(chuàng)建應(yīng)用,先寫(xiě)下 xml 文件,這里又是用 activity 嵌 fragment 的方式。
main_activity.xml:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/flayout_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
fragment 中放入列表:
fragment_photo_gallery.xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/recyclerview_photo"
android:layout_width="match_parent"
android:layout_height="match_parent" />
MainActivity.kt:
class MainActivity : AppCompatActivity() {
private lateinit var mBinding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(mBinding.root)
val isFragmentContainerEmpty = savedInstanceState == null
if (isFragmentContainerEmpty){
supportFragmentManager
.beginTransaction()
.add(R.id.flayout_container, PhotoGalleryFragment.newInstance())
.commit()
}
}
}
上面采用檢查 savedInstanceState 的方式判斷當(dāng)前 Activity 是不是重建或者第一次創(chuàng)建,再添加 fragment。
PhotoGalleryFragment.kt:
class PhotoGalleryFragment : Fragment() {
private lateinit var photoRecyclerView: RecyclerView
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_photo_gallery, container, false)
photoRecyclerView = view.findViewById(R.id.recyclerview_photo)
photoRecyclerView.layoutManager = GridLayoutManager(context, 3)
return super.onCreateView(inflater, container, savedInstanceState)
}
companion object {
fun newInstance() = PhotoGalleryFragment()
}
}
目前運(yùn)行起來(lái)還是個(gè)空頁(yè)面,因?yàn)闆](méi)有給 RecyclerView 綁定數(shù)據(jù)。
二、Retrofit 網(wǎng)絡(luò)連接基本
Retrofit 「https://square.github.io/retrofit/」是 Square 公司創(chuàng)建和維護(hù)的一個(gè)開(kāi)源庫(kù)。但本質(zhì)上,它的 HTTP 客戶端封裝使用的是 OkHttp 「https://square.github.io/okhttp/」 庫(kù)。
Retrofit 可創(chuàng)建 HTTP 網(wǎng)關(guān)類(lèi)。給 Retrofit 一個(gè)帶注解方法的接口,它會(huì)做接口實(shí)現(xiàn)。Retrofit 的接口實(shí)現(xiàn)能發(fā)起 HTTP 請(qǐng)求,收到 HTTP 響應(yīng)數(shù)據(jù)后會(huì)解析為一個(gè) OkHttp.ResponseBody。然而,OkHttp.ResponseBody 無(wú)法直接使用:你要將其轉(zhuǎn)換為自己應(yīng)用需要的數(shù)據(jù)類(lèi)型。為解決這個(gè)問(wèn)題,可以注冊(cè)一個(gè)響應(yīng)數(shù)據(jù)轉(zhuǎn)換器。隨后,在準(zhǔn)備網(wǎng)絡(luò)請(qǐng)求需要的數(shù)據(jù)以及從網(wǎng)絡(luò)響應(yīng)解析數(shù)據(jù)時(shí),Retrofit 就可以用這個(gè)轉(zhuǎn)換器進(jìn)行各種數(shù)據(jù)類(lèi)型的相互轉(zhuǎn)換了。
先在 build.gradle 文件添加 Retrofit 依賴:
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
- 定義 Retrofit API 接口
新建個(gè)包放接口 api,新建一個(gè)接口文件,F(xiàn)lickrApi.kt:
import retrofit2.Call
import retrofit2.http.GET
interface FlickrApi {
@GET("/")
fun fetchContents(): Call<String>
}
這里接口中的每一個(gè)函數(shù)都對(duì)應(yīng)著一個(gè)特定的 HTTP 請(qǐng)求,必須使用 HTTP 請(qǐng)求方法注解。
常見(jiàn)的 HTTP 請(qǐng)求類(lèi)型有 @GET、@POST、@PUT、@DELETE 和 @HEAD。
@GET("/") 注解的作用是把 fetchContents() 函數(shù)返回的 Call 配置成一個(gè) GET 請(qǐng)求。字符串"/"表示一個(gè)相對(duì)路徑 URL —— 針對(duì) Flickr API 端點(diǎn)基 URL 來(lái)說(shuō)的相對(duì)路徑。大多數(shù) HTTP 請(qǐng)求方法注解包括相對(duì)路徑。這里,"/" 相對(duì)路徑是指請(qǐng)求會(huì)發(fā)往我們稍后提供的基 URL。
所有 Retrofit 網(wǎng)絡(luò)請(qǐng)求默認(rèn)都會(huì)返回一個(gè) retrofit2.Call 對(duì)象(一個(gè)可執(zhí)行的網(wǎng)絡(luò)請(qǐng)求)。執(zhí)行 Call 網(wǎng)絡(luò)請(qǐng)求就會(huì)返回一個(gè)相應(yīng)的 HTTP 網(wǎng)絡(luò)響應(yīng)。(也可以配置 Retrofit 返回 RxJava Observable「目前主流方式」)
Call 的泛型參數(shù)是什么類(lèi)型,Retrofit 在反序列化 HTTP 響應(yīng)數(shù)據(jù)后就會(huì)生成同樣的數(shù)據(jù)類(lèi)型。Retrofit 默認(rèn)會(huì)把 HTTP 響應(yīng)數(shù)據(jù)反序列化為一個(gè) OkHttp.ResponseBody 對(duì)象。指定 Call<String> 就是告訴 Retrofit ,我們需要的是 String 對(duì)象,而不是 OkHttp.ResponseBody 對(duì)象。
- 構(gòu)建 Retrofit 對(duì)象并創(chuàng)建 API 實(shí)例
Retrofit 實(shí)例負(fù)責(zé)實(shí)現(xiàn)和創(chuàng)建 API 接口實(shí)例。為基于定義的 API 接口生成網(wǎng)絡(luò)請(qǐng)求。現(xiàn)在開(kāi)始構(gòu)建 Retrofit 實(shí)例。
val retrofit = Retrofit.Builder()
.baseUrl("https://www.flickr.com/")
.addConverterFactory(ScalarsConverterFactory.create())
.build()
val flickrApi = retrofit.create(FlickrApi::class.java)
Retrofit.Builder() 是一個(gè)流接口,用來(lái)配置并構(gòu)建 Retrofit 實(shí)例。
baseUrl(...) 提供要訪問(wèn)的基 URL 端點(diǎn)。
Retrofit.Builder() 進(jìn)行參數(shù)設(shè)定后調(diào)用 build() 函數(shù)會(huì)返回一個(gè)配置好的 Retrofit實(shí)例。
再添加個(gè)依賴包,做數(shù)據(jù)類(lèi)型轉(zhuǎn)換。
implementation 'com.squareup.retrofit2:converter-scalars:2.9.0'
利用 addConverterFactory(...) 函數(shù)添加特定的數(shù)據(jù)類(lèi)型轉(zhuǎn)換器實(shí)例。在返回 Call 結(jié)果之前,Retrofit對(duì)象就會(huì)使用這個(gè)字符串?dāng)?shù)據(jù)轉(zhuǎn)換器把 ResponseBody 對(duì)象轉(zhuǎn)換為 String 對(duì)象。當(dāng)然,Square 還為 Retrofit 提供了其他一些開(kāi)源數(shù)據(jù)類(lèi)型轉(zhuǎn)換器。
- 執(zhí)行網(wǎng)絡(luò)請(qǐng)求
創(chuàng)建 Call 請(qǐng)求:
val flickrHomePageRequest : Call<String> = flickrApi.fetchContents()
注意,調(diào)用 FlickrApi 的 fetchContents() 并不是執(zhí)行網(wǎng)絡(luò)請(qǐng)求,而是返回一個(gè)代表網(wǎng)絡(luò)請(qǐng)求的 Call<String> 對(duì)象。
然后,在 onCreate(savedInstanceState: Bundle?) 里調(diào)用 enqueue(...) 去執(zhí)行代表網(wǎng)絡(luò)請(qǐng)求的 Call 對(duì)象。
flickrHomePageRequest.enqueue(object : Callback<String> {
override fun onResponse(call: Call<String>, response: Response<String>) {
Log.d(TAG, "Response received : ${response.body()}")
}
override fun onFailure(call: Call<String>, t: Throwable) {
Log.e(TAG, "Failed to fetch photos", t)
}
})
Retrofit 天生就遵循兩個(gè)最重要的Android多線程規(guī)則。
(1) 僅在后臺(tái)線程上執(zhí)行耗時(shí)任務(wù)。
(2) 僅在主線程上做 UI 更新操作。
Call.enqueue(...) 函數(shù)執(zhí)行代表網(wǎng)絡(luò)請(qǐng)求的 Call 對(duì)象。最關(guān)鍵的是,它是在后臺(tái)線程上執(zhí)行網(wǎng)絡(luò)請(qǐng)求的。這一切都由 Retrofit 管理和調(diào)度的。
傳遞給 onResponse() 和 onFailure() 函數(shù)的 Call 對(duì)象就是最初發(fā)起網(wǎng)絡(luò)請(qǐng)求的 Call 對(duì)象。
- 獲取網(wǎng)絡(luò)使用權(quán)限
在 AndroidManifest.xml 中添加網(wǎng)絡(luò)權(quán)限:
<uses-permission android:name="android.permission.INTERNET" />
運(yùn)行可以看到打印日志「注意此 api 需要翻墻訪問(wèn),so,可以自行找個(gè)其他國(guó)內(nèi)公開(kāi)的 api 進(jìn)行訪問(wèn)」

- 使用倉(cāng)庫(kù)模式聯(lián)網(wǎng)
這里把 Retrofit 配置代碼和 API 聯(lián)網(wǎng)代碼抽出來(lái),移到一個(gè)新類(lèi)中。
private const val TAG = "FlickrFetchr"
class FlickrFetchr {
private val flickrApi :FlickrApi
init {
val retrofit = Retrofit.Builder()
.baseUrl("https://www.flickr.com/")
.addConverterFactory(ScalarsConverterFactory.create())
.build()
flickrApi = retrofit.create(FlickrApi::class.java)
}
fun fetchContents():LiveData<String>{
val responseLiveData : MutableLiveData<String> = MutableLiveData()
val flickrHomePageRequest: Call<String> = flickrApi.fetchContents()
flickrHomePageRequest.enqueue(object : Callback<String> {
override fun onResponse(call: Call<String>, response: Response<String>) {
Log.d(TAG, "Response received : ${response.body()}")
responseLiveData.value = response.body()
}
override fun onFailure(call: Call<String>, t: Throwable) {
Log.e(TAG, "Failed to fetch photos", t)
}
})
return responseLiveData
}
}
注意,fetchContents() 函數(shù)返回的是個(gè)無(wú)法修改的 LiveData<String>??尚薷牡?LiveData 對(duì)象盡量不要對(duì)外暴露,以防被其他外部代碼篡改。LiveData 里的數(shù)據(jù)流動(dòng)應(yīng)保持一個(gè)方向。
然后修改 PhotoGalleryFragment 中的 onCreate() 方法。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val flickrLiveData: LiveData<String> = FlickrFetchr().fetchContents()
flickrLiveData.observe(this,
Observer { responseString ->
Log.d(TAG, "Response received:$responseString")
})
}
這里借鑒了 Google 應(yīng)用架構(gòu)指導(dǎo)推崇的倉(cāng)庫(kù)模式。FlickrFetchr 充當(dāng)基本倉(cāng)庫(kù)的角色。這種倉(cāng)庫(kù)類(lèi)封裝了從一個(gè)或多個(gè)數(shù)據(jù)源獲取數(shù)據(jù)的邏輯。不管是本地?cái)?shù)據(jù)庫(kù),還是遠(yuǎn)程服務(wù)器,它都知道該如何獲取或保存各種數(shù)據(jù)。UI 代碼不關(guān)心數(shù)據(jù)的獲取和保存(倉(cāng)庫(kù)類(lèi)自己的內(nèi)部實(shí)現(xiàn)),需要數(shù)據(jù)時(shí),找倉(cāng)庫(kù)類(lèi)就行了。
運(yùn)行程序,可以看到日志打印跟上述一樣,是 Flickr 主頁(yè)內(nèi)容。
三、從 Flickr 獲取 JSON 數(shù)據(jù)
JSON(JavaScript Object Notation)是由道格拉斯·克羅克福特構(gòu)想和設(shè)計(jì)的一種輕量級(jí)資料交換格式。
Flickr 提供了方便而強(qiáng)大的 JSON API?,F(xiàn)在,我們也根據(jù)書(shū)中推薦,注冊(cè)個(gè)Flickr賬戶,打開(kāi)它的開(kāi)發(fā)文檔。

Flickr 開(kāi)發(fā)人員指南:https://www.flickr.com/services/developer/

然后我們根據(jù)指南,先申請(qǐng)個(gè)非商用 API Key,再將我們的示例應(yīng)用程式放入 App Garden 中。

然后我們會(huì)得到一個(gè) API key,這個(gè) key 比較長(zhǎng)就不貼代碼了,把它定義在一個(gè)單例中,我們繼續(xù)在 FlickrApi 中新增接口方法,書(shū)中此接口指南地址:
https://www.flickr.com/services/api/flickr.interestingness.getList.html
@GET(
"services/rest/?method=flickr.interestingness.getList"
+ "&api_key=${FlickrConstants.FLICKR_KEY}"
+ "&format=json&nojsoncallback=1"
+ "&extras=url_s"
)
fun fetchPhotos(): Call<String>
這里根據(jù)同書(shū)中賦值參數(shù)。然后再去更新下 FlickrFetchr 類(lèi),這里我們就不像書(shū)中 Demo 一樣修改方法了,我們新增一個(gè)方法,取名為 fetchPhotos(),然后調(diào)用 fetchPhotos 的 api,將我們?cè)瓉?lái) PhotoGalleryFragment 類(lèi)中調(diào)用 fetchContent 的地方修改為調(diào)用 fetchPhotos。
最終運(yùn)行項(xiàng)目,得到Log日志:

這里真是調(diào)了半天,去官網(wǎng)看了下 api ,跟書(shū)上提供的略有不同,還是需要參考最新的文檔,不管怎么樣,總算是有數(shù)據(jù)了,具體代碼還是參考我的Github上的個(gè)人 Demo 啦。
- 接下來(lái),新建 GalleryItem.kt 數(shù)據(jù)類(lèi)進(jìn)行接收請(qǐng)求數(shù)據(jù):
data class GalleryItem(
var title:String="",
var id:String = "",
@SerializedName("url_s")
var url:String=""
)
然后就是對(duì)數(shù)據(jù)進(jìn)行解析啦,將要用到Gson了。
可別忘記了在 build.gradle 中添加依賴?yán)玻?/p>
implementation 'com.google.code.gson:gson:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
- 新建 PhotoResponse 類(lèi):
class PhotoResponse {
@SerializedName("photo")
lateinit var galleryItems:List<GalleryItem>
}
- 新建 FlickrResponse 類(lèi):
class FlickrResponse {
lateinit var photos: PhotoResponse
}
- 更新 fetchPhoto() 的返回類(lèi)型:
@GET(
"services/rest/?method=flickr.interestingness.getList"
+ "&api_key=${FlickrConstants.FLICKR_KEY}"
+ "&format=json&nojsoncallback=1"
+ "&extras=url_s"
)
fun fetchPhotos(): Call<FlickrResponse>
更新 FlickrFetchr 中初始化 retrofit 的 addConverterFactory 為addConverterFactory(GsonConverterFactory.create(GsonBuilder().setLenient().create()))
更新 fetchPhotos() 方法:
fun fetchPhotos(): LiveData<List<GalleryItem>> {
val responseLiveData: MutableLiveData<List<GalleryItem>> = MutableLiveData()
val flickrHomePageRequest: Call<FlickrResponse> = flickrApi.fetchPhotos()
flickrHomePageRequest.enqueue(object : Callback<FlickrResponse> {
override fun onResponse(call: Call<FlickrResponse>, response: Response<FlickrResponse>) {
Log.d(TAG, "Response received : ${response.body()}")
val flickrResponse: FlickrResponse? = response.body()
val photoResponse: PhotoResponse? = flickrResponse?.photos
var galleryItems: List<GalleryItem> = photoResponse?.galleryItems ?: mutableListOf()
galleryItems = galleryItems.filterNot { it.url.isBlank() }
responseLiveData.value = galleryItems
}
override fun onFailure(call: Call<FlickrResponse>, t: Throwable) {
Log.e(TAG, "Failed to fetch photos", t)
}
})
return responseLiveData
}
- 更新 PhotoGalleryFragment 的 onCreate() 的內(nèi)容為:
val flickrLiveData: LiveData<List<GalleryItem>> = FlickrFetchr().fetchPhotos()
flickrLiveData.observe(this,
Observer { gallerayItems ->
Log.d(TAG, "Response received:$gallerayItems")
})
運(yùn)行日志:

此小節(jié)關(guān)于接口問(wèn)題可能會(huì)遇到不少坑,關(guān)鍵還是在于,多斷點(diǎn)調(diào)試一下,仔細(xì)看看官方文檔,分析下報(bào)錯(cuò)內(nèi)容,還是可以解決的。可以調(diào)試下網(wǎng)頁(yè)版的接口,看看網(wǎng)頁(yè)是怎么調(diào)用具體的接口的。
參考:https://www.flickr.com/services/api/explore/flickr.interestingness.getList
四、應(yīng)對(duì)設(shè)備配置改變
五、在 RecyclerView 里顯示結(jié)果
其他
PhotoGallery 項(xiàng)目 Demo 地址:
https://github.com/visiongem/AndroidGuideApp/tree/master/PhotoGallery