《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 对象并创建 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 进行访问」
- 使用仓库模式联网
这里把 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
然后修改 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 开发人员指南:https://www.flickr.com/services/developer/
然后我们根据指南,先申请个非商用 API Key,再将我们的示例应用程式放入 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日志:
这里真是调了半天,去官网看了下 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")
})
运行日志:
此小节关于接口问题可能会遇到不少坑,关键还是在于,多断点调试一下,仔细看看官方文档,分析下报错内容,还是可以解决的。可以调试下网页版的接口,看看网页是怎么调用具体的接口的。
参考:https://www.flickr.com/services/api/explore/flickr.interestingness.getList
四、应对设备配置改变
五、在 RecyclerView 里显示结果
其他
PhotoGallery 项目 Demo 地址:
https://github.com/visiongem/AndroidGuideApp/tree/master/PhotoGallery