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

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

一、创建 PhotoGallery 应用

按照惯例,创建应用,先写下 xml 文件,这里又是用 activity 嵌 fragment 的方式。
main_activity.xml:



fragment 中放入列表:
fragment_photo_gallery.xml:



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
}

这里接口中的每一个函数都对应着一个特定的 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 就是告诉 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 = flickrApi.fetchContents()

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

然后,在 onCreate(savedInstanceState: Bundle?) 里调用 enqueue(...) 去执行代表网络请求的 Call 对象。

        flickrHomePageRequest.enqueue(object : Callback {
            override fun onResponse(call: Call, response: Response) {
                Log.d(TAG, "Response received : ${response.body()}")
            }

            override fun onFailure(call: Call, 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 中添加网络权限:

 

运行可以看到打印日志「注意此 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{

        val responseLiveData : MutableLiveData = MutableLiveData()

        val flickrHomePageRequest: Call = flickrApi.fetchContents()

        flickrHomePageRequest.enqueue(object : Callback {
            override fun onResponse(call: Call, response: Response) {
                Log.d(TAG, "Response received : ${response.body()}")
                responseLiveData.value = response.body()
            }

            override fun onFailure(call: Call, t: Throwable) {
                Log.e(TAG, "Failed to fetch photos", t)
            }
        })
        return responseLiveData
    }
}

注意,fetchContents() 函数返回的是个无法修改的 LiveData。可修改的 LiveData 对象尽量不要对外暴露,以防被其他外部代码篡改。LiveData 里的数据流动应保持一个方向。

然后修改 PhotoGalleryFragment 中的 onCreate() 方法。

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val flickrLiveData: LiveData = 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

这里根据同书中赋值参数。然后再去更新下 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
}
  • 新建 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
  • 更新 FlickrFetchr 中初始化 retrofit 的 addConverterFactory 为addConverterFactory(GsonConverterFactory.create(GsonBuilder().setLenient().create()))

  • 更新 fetchPhotos() 方法:

fun fetchPhotos(): LiveData> {
        val responseLiveData: MutableLiveData> = MutableLiveData()
        val flickrHomePageRequest: Call = flickrApi.fetchPhotos()

        flickrHomePageRequest.enqueue(object : Callback {
            override fun onResponse(call: Call, response: Response) {
                Log.d(TAG, "Response received : ${response.body()}")
                val flickrResponse: FlickrResponse? = response.body()
                val photoResponse: PhotoResponse? = flickrResponse?.photos
                var galleryItems: List = photoResponse?.galleryItems ?: mutableListOf()
                galleryItems = galleryItems.filterNot { it.url.isBlank() }
                responseLiveData.value = galleryItems
            }

            override fun onFailure(call: Call, t: Throwable) {
                Log.e(TAG, "Failed to fetch photos", t)
            }
        })
        return responseLiveData
    }
  • 更新 PhotoGalleryFragment 的 onCreate() 的内容为:
        val flickrLiveData: LiveData> = 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

你可能感兴趣的:(《Android编程权威指南》之HTTP与后台任务)