《Android编程权威指南》之HTTP与后台任务

《Android编程权威指南》第 24 章啦,本章又有个新应用啦,叫 PhotoGallery,用来获取 Flickr 网站的最新公共图片「不限版权的图片」。本章将学习 Retrofit 网络请求库,Json 数据,Gson 解析 Json 等等。

一、创建 PhotoGallery 应用

按照惯例,创建应用,先写下 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 的方式判断当前 Activity 是不是重建或者第一次创建,再添加 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 绑定数据。

二、Retrofit 网络连接基本

Retrofit 「https://square.github.io/retrofit/」是 Square 公司创建和维护的一个开源库。但本质上,它的 HTTP 客户端封装使用的是 OkHttp 「https://square.github.io/okhttp/」 库。

Retrofit 可创建 HTTP 网关类。给 Retrofit 一个带注解方法的接口,它会做接口实现。Retrofit 的接口实现能发起 HTTP 请求,收到 HTTP 响应数据后会解析为一个 OkHttp.ResponseBody。然而,OkHttp.ResponseBody 无法直接使用:你要将其转换为自己应用需要的数据类型。为解决这个问题,可以注册一个响应数据转换器。随后,在准备网络请求需要的数据以及从网络响应解析数据时,Retrofit 就可以用这个转换器进行各种数据类型的相互转换了。

先在 build.gradle 文件添加 Retrofit 依赖:

 implementation 'com.squareup.retrofit2:retrofit:2.9.0'
  • 定义 Retrofit API 接口

新建个包放接口 api,新建一个接口文件,FlickrApi.kt:

import retrofit2.Call
import retrofit2.http.GET

interface FlickrApi {
    @GET("/")
    fun fetchContents(): Call<String>
}

这里接口中的每一个函数都对应着一个特定的 HTTP 请求,必须使用 HTTP 请求方法注解。

常见的 HTTP 请求类型有 @GET、@POST、@PUT、@DELETE 和 @HEAD。

@GET("/") 注解的作用是把 fetchContents() 函数返回的 Call 配置成一个 GET 请求。字符串"/"表示一个相对路径 URL —— 针对 Flickr API 端点基 URL 来说的相对路径。大多数 HTTP 请求方法注解包括相对路径。这里,"/" 相对路径是指请求会发往我们稍后提供的基 URL。

所有 Retrofit 网络请求默认都会返回一个 retrofit2.Call 对象(一个可执行的网络请求)。执行 Call 网络请求就会返回一个相应的 HTTP 网络响应。(也可以配置 Retrofit 返回 RxJava Observable「目前主流方式」)

Call 的泛型参数是什么类型,Retrofit 在反序列化 HTTP 响应数据后就会生成同样的数据类型。Retrofit 默认会把 HTTP 响应数据反序列化为一个 OkHttp.ResponseBody 对象。指定 Call<String> 就是告诉 Retrofit ,我们需要的是 String 对象,而不是 OkHttp.ResponseBody 对象。

  • 构建 Retrofit 对象并创建 API 实例

Retrofit 实例负责实现和创建 API 接口实例。为基于定义的 API 接口生成网络请求。现在开始构建 Retrofit 实例。

        val retrofit = Retrofit.Builder()
            .baseUrl("https://www.flickr.com/")
            .addConverterFactory(ScalarsConverterFactory.create())
            .build()
        
        val flickrApi = retrofit.create(FlickrApi::class.java)

Retrofit.Builder() 是一个流接口,用来配置并构建 Retrofit 实例。
baseUrl(...) 提供要访问的基 URL 端点。
Retrofit.Builder() 进行参数设定后调用 build() 函数会返回一个配置好的 Retrofit实例。

再添加个依赖包,做数据类型转换。

implementation 'com.squareup.retrofit2:converter-scalars:2.9.0'

利用 addConverterFactory(...) 函数添加特定的数据类型转换器实例。在返回 Call 结果之前,Retrofit对象就会使用这个字符串数据转换器把 ResponseBody 对象转换为 String 对象。当然,Square 还为 Retrofit 提供了其他一些开源数据类型转换器。

  • 执行网络请求

创建 Call 请求:

 val flickrHomePageRequest : Call<String> = flickrApi.fetchContents()

注意,调用 FlickrApi 的 fetchContents() 并不是执行网络请求,而是返回一个代表网络请求的 Call<String> 对象。

然后,在 onCreate(savedInstanceState: Bundle?) 里调用 enqueue(...) 去执行代表网络请求的 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多线程规则。

(1) 仅在后台线程上执行耗时任务。

(2) 仅在主线程上做 UI 更新操作。

Call.enqueue(...) 函数执行代表网络请求的 Call 对象。最关键的是,它是在后台线程上执行网络请求的。这一切都由 Retrofit 管理和调度的。

传递给 onResponse() 和 onFailure() 函数的 Call 对象就是最初发起网络请求的 Call 对象。

  • 获取网络使用权限

在 AndroidManifest.xml 中添加网络权限:

 <uses-permission android:name="android.permission.INTERNET" />

运行可以看到打印日志「注意此 api 需要翻墙访问,so,可以自行找个其他国内公开的 api 进行访问」

Log
  • 使用仓库模式联网

这里把 Retrofit 配置代码和 API 联网代码抽出来,移到一个新类中。

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() 函数返回的是个无法修改的 LiveData<String>。可修改的 LiveData 对象尽量不要对外暴露,以防被其他外部代码篡改。LiveData 里的数据流动应保持一个方向。

然后修改 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 应用架构指导推崇的仓库模式。FlickrFetchr 充当基本仓库的角色。这种仓库类封装了从一个或多个数据源获取数据的逻辑。不管是本地数据库,还是远程服务器,它都知道该如何获取或保存各种数据。UI 代码不关心数据的获取和保存(仓库类自己的内部实现),需要数据时,找仓库类就行了。

运行程序,可以看到日志打印跟上述一样,是 Flickr 主页内容。

三、从 Flickr 获取 JSON 数据

JSON(JavaScript Object Notation)是由道格拉斯·克罗克福特构想和设计的一种轻量级资料交换格式

Flickr 提供了方便而强大的 JSON API。现在,我们也根据书中推荐,注册个Flickr账户,打开它的开发文档。

注册Flickr

Flickr 开发人员指南:https://www.flickr.com/services/developer/

开发人员指南

然后我们根据指南,先申请个非商用 API Key,再将我们的示例应用程式放入 App Garden 中。

App Garden

然后我们会得到一个 API key,这个 key 比较长就不贴代码了,把它定义在一个单例中,我们继续在 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>

这里根据同书中赋值参数。然后再去更新下 FlickrFetchr 类,这里我们就不像书中 Demo 一样修改方法了,我们新增一个方法,取名为 fetchPhotos(),然后调用 fetchPhotos 的 api,将我们原来 PhotoGalleryFragment 类中调用 fetchContent 的地方修改为调用 fetchPhotos。

最终运行项目,得到Log日志:

log

这里真是调了半天,去官网看了下 api ,跟书上提供的略有不同,还是需要参考最新的文档,不管怎么样,总算是有数据了,具体代码还是参考我的Github上的个人 Demo 啦。

  • 接下来,新建 GalleryItem.kt 数据类进行接收请求数据:
data class GalleryItem(
    var title:String="",
    var id:String = "",
    @SerializedName("url_s")
    var url:String=""
)

然后就是对数据进行解析啦,将要用到Gson了。

可别忘记了在 build.gradle 中添加依赖啦:

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() 的内容为:
        val flickrLiveData: LiveData<List<GalleryItem>> = FlickrFetchr().fetchPhotos()
        flickrLiveData.observe(this,
            Observer { gallerayItems ->
                Log.d(TAG, "Response received:$gallerayItems")
            })

运行日志:

log

此小节关于接口问题可能会遇到不少坑,关键还是在于,多断点调试一下,仔细看看官方文档,分析下报错内容,还是可以解决的。可以调试下网页版的接口,看看网页是怎么调用具体的接口的。

参考:https://www.flickr.com/services/api/explore/flickr.interestingness.getList

四、应对设备配置改变

五、在 RecyclerView 里显示结果

其他

PhotoGallery 项目 Demo 地址:

https://github.com/visiongem/AndroidGuideApp/tree/master/PhotoGallery

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容