《Android編程權(quán)威指南》第 24 章啦舍悯,本章又有個新應(yīng)用啦,叫 PhotoGallery盒发,用來獲取 Flickr 網(wǎng)站的最新公共圖片「不限版權(quán)的圖片」兔沃。本章將學(xué)習(xí) Retrofit 網(wǎng)絡(luò)請求庫,Json 數(shù)據(jù)卵沉,Gson 解析 Json 等等颠锉。
一、創(chuàng)建 PhotoGallery 應(yīng)用
按照慣例史汗,創(chuàng)建應(yīng)用琼掠,先寫下 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()
}
}
目前運行起來還是個空頁面怜森,因為沒有給 RecyclerView 綁定數(shù)據(jù)速挑。
二、Retrofit 網(wǎng)絡(luò)連接基本
Retrofit 「https://square.github.io/retrofit/」是 Square 公司創(chuàng)建和維護的一個開源庫副硅。但本質(zhì)上姥宝,它的 HTTP 客戶端封裝使用的是 OkHttp 「https://square.github.io/okhttp/」 庫。
Retrofit 可創(chuàng)建 HTTP 網(wǎng)關(guān)類恐疲。給 Retrofit 一個帶注解方法的接口腊满,它會做接口實現(xiàn)。Retrofit 的接口實現(xiàn)能發(fā)起 HTTP 請求培己,收到 HTTP 響應(yīng)數(shù)據(jù)后會解析為一個 OkHttp.ResponseBody碳蛋。然而,OkHttp.ResponseBody 無法直接使用:你要將其轉(zhuǎn)換為自己應(yīng)用需要的數(shù)據(jù)類型省咨。為解決這個問題肃弟,可以注冊一個響應(yīng)數(shù)據(jù)轉(zhuǎn)換器。隨后零蓉,在準(zhǔn)備網(wǎng)絡(luò)請求需要的數(shù)據(jù)以及從網(wǎng)絡(luò)響應(yīng)解析數(shù)據(jù)時笤受,Retrofit 就可以用這個轉(zhuǎn)換器進行各種數(shù)據(jù)類型的相互轉(zhuǎn)換了。
先在 build.gradle 文件添加 Retrofit 依賴:
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
- 定義 Retrofit API 接口
新建個包放接口 api敌蜂,新建一個接口文件箩兽,F(xiàn)lickrApi.kt:
import retrofit2.Call
import retrofit2.http.GET
interface FlickrApi {
@GET("/")
fun fetchContents(): Call<String>
}
這里接口中的每一個函數(shù)都對應(yīng)著一個特定的 HTTP 請求,必須使用 HTTP 請求方法注解章喉。
常見的 HTTP 請求類型有 @GET汗贫、@POST身坐、@PUT、@DELETE 和 @HEAD芳绩。
@GET("/") 注解的作用是把 fetchContents() 函數(shù)返回的 Call 配置成一個 GET 請求掀亥。字符串"/"表示一個相對路徑 URL —— 針對 Flickr API 端點基 URL 來說的相對路徑。大多數(shù) HTTP 請求方法注解包括相對路徑妥色。這里搪花,"/" 相對路徑是指請求會發(fā)往我們稍后提供的基 URL。
所有 Retrofit 網(wǎng)絡(luò)請求默認(rèn)都會返回一個 retrofit2.Call 對象(一個可執(zhí)行的網(wǎng)絡(luò)請求)嘹害。執(zhí)行 Call 網(wǎng)絡(luò)請求就會返回一個相應(yīng)的 HTTP 網(wǎng)絡(luò)響應(yīng)撮竿。(也可以配置 Retrofit 返回 RxJava Observable「目前主流方式」)
Call 的泛型參數(shù)是什么類型,Retrofit 在反序列化 HTTP 響應(yīng)數(shù)據(jù)后就會生成同樣的數(shù)據(jù)類型笔呀。Retrofit 默認(rèn)會把 HTTP 響應(yīng)數(shù)據(jù)反序列化為一個 OkHttp.ResponseBody 對象幢踏。指定 Call<String> 就是告訴 Retrofit ,我們需要的是 String 對象许师,而不是 OkHttp.ResponseBody 對象房蝉。
- 構(gòu)建 Retrofit 對象并創(chuàng)建 API 實例
Retrofit 實例負(fù)責(zé)實現(xiàn)和創(chuàng)建 API 接口實例。為基于定義的 API 接口生成網(wǎng)絡(luò)請求∥⑶現(xiàn)在開始構(gòu)建 Retrofit 實例搭幻。
val retrofit = Retrofit.Builder()
.baseUrl("https://www.flickr.com/")
.addConverterFactory(ScalarsConverterFactory.create())
.build()
val flickrApi = retrofit.create(FlickrApi::class.java)
Retrofit.Builder() 是一個流接口,用來配置并構(gòu)建 Retrofit 實例逞盆。
baseUrl(...) 提供要訪問的基 URL 端點檀蹋。
Retrofit.Builder() 進行參數(shù)設(shè)定后調(diào)用 build() 函數(shù)會返回一個配置好的 Retrofit實例。
再添加個依賴包云芦,做數(shù)據(jù)類型轉(zhuǎn)換俯逾。
implementation 'com.squareup.retrofit2:converter-scalars:2.9.0'
利用 addConverterFactory(...) 函數(shù)添加特定的數(shù)據(jù)類型轉(zhuǎn)換器實例。在返回 Call 結(jié)果之前舅逸,Retrofit對象就會使用這個字符串?dāng)?shù)據(jù)轉(zhuǎn)換器把 ResponseBody 對象轉(zhuǎn)換為 String 對象桌肴。當(dāng)然,Square 還為 Retrofit 提供了其他一些開源數(shù)據(jù)類型轉(zhuǎn)換器琉历。
- 執(zhí)行網(wǎng)絡(luò)請求
創(chuàng)建 Call 請求:
val flickrHomePageRequest : Call<String> = flickrApi.fetchContents()
注意识脆,調(diào)用 FlickrApi 的 fetchContents() 并不是執(zhí)行網(wǎng)絡(luò)請求,而是返回一個代表網(wǎng)絡(luò)請求的 Call<String> 對象善已。
然后,在 onCreate(savedInstanceState: Bundle?) 里調(diào)用 enqueue(...) 去執(zhí)行代表網(wǎng)絡(luò)請求的 Call 對象离例。
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 天生就遵循兩個最重要的Android多線程規(guī)則换团。
(1) 僅在后臺線程上執(zhí)行耗時任務(wù)。
(2) 僅在主線程上做 UI 更新操作宫蛆。
Call.enqueue(...) 函數(shù)執(zhí)行代表網(wǎng)絡(luò)請求的 Call 對象艘包。最關(guān)鍵的是的猛,它是在后臺線程上執(zhí)行網(wǎng)絡(luò)請求的。這一切都由 Retrofit 管理和調(diào)度的想虎。
傳遞給 onResponse() 和 onFailure() 函數(shù)的 Call 對象就是最初發(fā)起網(wǎng)絡(luò)請求的 Call 對象卦尊。
- 獲取網(wǎng)絡(luò)使用權(quán)限
在 AndroidManifest.xml 中添加網(wǎng)絡(luò)權(quán)限:
<uses-permission android:name="android.permission.INTERNET" />
運行可以看到打印日志「注意此 api 需要翻墻訪問,so舌厨,可以自行找個其他國內(nèi)公開的 api 進行訪問」
- 使用倉庫模式聯(lián)網(wǎng)
這里把 Retrofit 配置代碼和 API 聯(lián)網(wǎng)代碼抽出來岂却,移到一個新類中。
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ù)返回的是個無法修改的 LiveData<String>躏哩。可修改的 LiveData 對象盡量不要對外暴露揉燃,以防被其他外部代碼篡改扫尺。LiveData 里的數(shù)據(jù)流動應(yīng)保持一個方向。
然后修改 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)推崇的倉庫模式正驻。FlickrFetchr 充當(dāng)基本倉庫的角色。這種倉庫類封裝了從一個或多個數(shù)據(jù)源獲取數(shù)據(jù)的邏輯抢腐。不管是本地數(shù)據(jù)庫姑曙,還是遠(yuǎn)程服務(wù)器,它都知道該如何獲取或保存各種數(shù)據(jù)氓栈。UI 代碼不關(guān)心數(shù)據(jù)的獲取和保存(倉庫類自己的內(nèi)部實現(xiàn))渣磷,需要數(shù)據(jù)時,找倉庫類就行了授瘦。
運行程序醋界,可以看到日志打印跟上述一樣,是 Flickr 主頁內(nèi)容提完。
三形纺、從 Flickr 獲取 JSON 數(shù)據(jù)
JSON(JavaScript Object Notation)是由道格拉斯·克羅克福特構(gòu)想和設(shè)計的一種輕量級資料交換格式。
Flickr 提供了方便而強大的 JSON API⊥叫溃現(xiàn)在逐样,我們也根據(jù)書中推薦,注冊個Flickr賬戶打肝,打開它的開發(fā)文檔脂新。
Flickr 開發(fā)人員指南:https://www.flickr.com/services/developer/
然后我們根據(jù)指南,先申請個非商用 API Key粗梭,再將我們的示例應(yīng)用程式放入 App Garden 中争便。
然后我們會得到一個 API key,這個 key 比較長就不貼代碼了断医,把它定義在一個單例中滞乙,我們繼續(xù)在 FlickrApi 中新增接口方法奏纪,書中此接口指南地址:
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ù)。然后再去更新下 FlickrFetchr 類斩启,這里我們就不像書中 Demo 一樣修改方法了序调,我們新增一個方法,取名為 fetchPhotos()兔簇,然后調(diào)用 fetchPhotos 的 api发绢,將我們原來 PhotoGalleryFragment 類中調(diào)用 fetchContent 的地方修改為調(diào)用 fetchPhotos。
最終運行項目男韧,得到Log日志:
這里真是調(diào)了半天朴摊,去官網(wǎng)看了下 api ,跟書上提供的略有不同此虑,還是需要參考最新的文檔甚纲,不管怎么樣,總算是有數(shù)據(jù)了朦前,具體代碼還是參考我的Github上的個人 Demo 啦介杆。
- 接下來,新建 GalleryItem.kt 數(shù)據(jù)類進行接收請求數(shù)據(jù):
data class GalleryItem(
var title:String="",
var id:String = "",
@SerializedName("url_s")
var url:String=""
)
然后就是對數(shù)據(jù)進行解析啦韭寸,將要用到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 類:
class PhotoResponse {
@SerializedName("photo")
lateinit var galleryItems:List<GalleryItem>
}
- 新建 FlickrResponse 類:
class FlickrResponse {
lateinit var photos: PhotoResponse
}
- 更新 fetchPhoto() 的返回類型:
@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")
})
運行日志:
此小節(jié)關(guān)于接口問題可能會遇到不少坑,關(guān)鍵還是在于恩伺,多斷點調(diào)試一下赴背,仔細(xì)看看官方文檔,分析下報錯內(nèi)容晶渠,還是可以解決的凰荚。可以調(diào)試下網(wǎng)頁版的接口褒脯,看看網(wǎng)頁是怎么調(diào)用具體的接口的便瑟。
參考:https://www.flickr.com/services/api/explore/flickr.interestingness.getList
四、應(yīng)對設(shè)備配置改變
五番川、在 RecyclerView 里顯示結(jié)果
其他
PhotoGallery 項目 Demo 地址:
https://github.com/visiongem/AndroidGuideApp/tree/master/PhotoGallery