《Android編程權(quán)威指南》之HTTP與后臺任務(wù)

《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 進行訪問」

Log
  • 使用倉庫模式聯(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

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

開發(fā)人員指南

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

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日志:

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")
            })

運行日志:

log

此小節(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末到涂,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子颁督,更是在濱河造成了極大的恐慌践啄,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,820評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件沉御,死亡現(xiàn)場離奇詭異往核,居然都是意外死亡,警方通過查閱死者的電腦和手機嚷节,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評論 3 399
  • 文/潘曉璐 我一進店門聂儒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人硫痰,你說我怎么就攤上這事衩婚。” “怎么了效斑?”我有些...
    開封第一講書人閱讀 168,324評論 0 360
  • 文/不壞的土叔 我叫張陵非春,是天一觀的道長。 經(jīng)常有香客問我缓屠,道長奇昙,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,714評論 1 297
  • 正文 為了忘掉前任敌完,我火速辦了婚禮储耐,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘滨溉。我一直安慰自己什湘,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,724評論 6 397
  • 文/花漫 我一把揭開白布晦攒。 她就那樣靜靜地躺著闽撤,像睡著了一般。 火紅的嫁衣襯著肌膚如雪脯颜。 梳的紋絲不亂的頭發(fā)上哟旗,一...
    開封第一講書人閱讀 52,328評論 1 310
  • 那天,我揣著相機與錄音栋操,去河邊找鬼闸餐。 笑死,一個胖子當(dāng)著我的面吹牛讼庇,可吹牛的內(nèi)容都是我干的绎巨。 我是一名探鬼主播,決...
    沈念sama閱讀 40,897評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼蠕啄,長吁一口氣:“原來是場噩夢啊……” “哼场勤!你這毒婦竟也來了再来?” 一聲冷哼從身側(cè)響起个粱,我...
    開封第一講書人閱讀 39,804評論 0 276
  • 序言:老撾萬榮一對情侶失蹤茅信,失蹤者是張志新(化名)和其女友劉穎笼平,沒想到半個月后茶行,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體夺饲,經(jīng)...
    沈念sama閱讀 46,345評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡胡陪,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,431評論 3 340
  • 正文 我和宋清朗相戀三年件舵,在試婚紗的時候發(fā)現(xiàn)自己被綠了骚秦。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片她倘。...
    茶點故事閱讀 40,561評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡璧微,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出硬梁,到底是詐尸還是另有隱情前硫,我是刑警寧澤,帶...
    沈念sama閱讀 36,238評論 5 350
  • 正文 年R本政府宣布荧止,位于F島的核電站屹电,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏跃巡。R本人自食惡果不足惜危号,卻給世界環(huán)境...
    茶點故事閱讀 41,928評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望素邪。 院中可真熱鬧外莲,春花似錦、人聲如沸娘香。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,417評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽烘绽。三九已至淋昭,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間安接,已是汗流浹背翔忽。 一陣腳步聲響...
    開封第一講書人閱讀 33,528評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留盏檐,地道東北人歇式。 一個月前我還...
    沈念sama閱讀 48,983評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像胡野,于是被迫代替她去往敵國和親材失。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,573評論 2 359

推薦閱讀更多精彩內(nèi)容