创建PhotoGallery应用
- 新建继承自AppCompatActivity类的SingleFragmentActivity抽象类,使PhotoGalleryActivity类继承它;
- 新建布局文件fragment_photo_gallery.xml,布置RecyclerView视图;
- 新建PhotoGalleryFragment类,使其继承Fragment,初始化RecyclerView视图,并设置此fragment为保留fragment。
PhotoGalleryFragment.java
private RecyclerView mPhotoRecyclerView;
public static PhotoGalleryFragment newInstance() {
return new PhotoGalleryFragment();
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
new FetchItemTask().execute();
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_photo_gallery, container, false);
mPhotoRecyclerView = view.findViewById(R.id.fragment_photo_gallery_recycler_view);
mPhotoRecyclerView.setLayoutManager(new GridLayoutManager(getActivity(), 1));
setAdapter();
return view;
}
网络连接基本
PhotoGallery应用中,需要一个网络连接专用类。
FlickrFetcher.java
public byte[] getUrlBytes(String urlSpec) throws IOException {
URL url = new URL(urlSpec);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
//真正连接到指定的URL地址
InputStream in = connection.getInputStream();
if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
throw new IOException(connection.getResponseMessage() + ": with " + urlSpec);
}
int bytesRead = 0;
byte[] buffer = new byte[1024];
while ((bytesRead = in.read(buffer)) > 0) {
out.write(buffer, 0, bytesRead);
}
out.close();
return out.toByteArray();
}
finally {
connection.disconnect();
}
}
public String getUrlString(String urlSpec) throws IOException {
return new String(getUrlBytes(urlSpec));
}
在 getUrlBytes(String) 方法中,首先根据传入的字符串参数,如https://www.bignerdranch.com,创建一个 URL 对象。然后调用 openConnection() 方法创建一个指向要访问URL的连接对象。URL.openConnection() 方法默认返回的是 URLConnection 对象,但要连接的是 httpURL,因此需将其强制类型转换为 HttpURLConnection 对象 。 这让我们得以调用它的 getInputStream() 、getResponseCode() 等方法。
虽然 HttpURLConnection 对象提供了一个连接,但只有在调用 getInputStream() 方法时
(如果是POST请求,则调用 getOutputStream() 方法),它才会真正连接到指定的URL地址。
创建了URL并打开网络连接之后,我们便可循环调用 read() 方法读取网络数据,直到取完为止。只要还有数据,InputStream 类就会不断地输出字节流数据。数据全部取回后,关闭网络连接,并将读取的数据写入 ByteArrayOutputStream 字节数组中。
获取网络使用权限
AndroidManifest.xml
...
<uses-permission android:name="android.permission.INTERNET" />
<application
...
</application>
...
使用AsyncTask在后台线程上运行代码
不要直接在 PhotoGalleryFragment 类中调用 FlickrFetchr.getURLString(String) 方法。正确的做法是,创建一个后台线程,然后在该线程中运行代码。
使用后台线程最简便的方式是使用 AsyncTask 工具类。 AsyncTask 创建后台线程后,我们便可在该线程上调用 doInBackground(…) 方法运行代码。
在PhotoGalleryFragment.java中,添加一个名为 FetchItemsTask 的内部类。覆盖 AsyncTask.doInBackground(…) 方法,从目标网站获取数据并记录日志:
PhotoGalleryFragment.java
private class FetchItemTask extends AsyncTask<Void, Void, List<Image>> {
@Override
protected List<Image> doInBackground(Void... voids) {
Log.d(TAG, "FetchItemTask -> doInBackground...");
List<Image> imgUrlList = new ArrayList<>();
try {
Image image = new Image();
new BaiduFetcher().getJumpUrlImage("https://api.blogbig.cn/random/api.php", image);
imgUrlList.add(image);
Log.i(TAG, "获取到URL的内容:" + image.getImgUrl());
}
catch (IOException ioe) {
Log.e(TAG, "获取URL内容失败!", ioe);
}
return imgUrlList;
}
@Override
protected void onPostExecute(List<Image> imgUrlList) {
Log.d(TAG, "FetchItemTask -> onPostExecute...");
mImgUrlList = imgUrlList;
setAdapter();
}
}
BaiduFetcher.java
public void getJumpUrlImage(String urlSpec, Image image) throws IOException {
URL url = new URL(urlSpec);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
switch (connection.getResponseCode()) {
case HttpURLConnection.HTTP_MOVED_TEMP:
String imgUrl = connection.getHeaderField("location");
image.setImgUrl(imgUrl);
getJumpUrlImage(imgUrl, image);
break;
case HttpURLConnection.HTTP_OK:
ByteArrayOutputStream out = new ByteArrayOutputStream();
//真正连接到指定的URL地址
InputStream in = connection.getInputStream();
int bytesRead = 0;
byte[] buffer = new byte[1024];
while ((bytesRead = in.read(buffer)) > 0) {
out.write(buffer, 0, bytesRead);
}
out.close();
byte[] imgBytes = out.toByteArray();
Bitmap bmpImg = BitmapFactory.decodeByteArray(imgBytes, 0, imgBytes.length);
image.setImg(bmpImg);
break;
default:
throw new IOException(connection.getResponseMessage() + ": with " + urlSpec);
}
}
然后,在 PhotoGalleryFragment.onCreate(…) 方法中,调用 FetchItemsTask 新实例的execute() 方法。(见创建PhotoGallery应用 => onCreate方法)
线程与主线程
网络连接需要时间。Web服务器可能需要1~2秒的时间来响应访问请求,文件下载则耗时更久。考虑到这个因素,Android禁止任何主线程网络连接行为。即使强行为之,Android也会抛出NetworkOnMainThreadException 异常。
线程是个单一执行序列。所有Android应用的运行都是从主线程开始的。然而,主线程不是线程那样的预定执行序列。相反,它处于一个无限循环的运行状态,等待着用户或系统触发事件的发生。事件触发后,主线程便负责执行代码,以响应这些事件。
从Flickr获取JSON数据
添加fetchItems()方法
登录Flickr网站获取API key,并添加fetchItems()方法:
FlickrFetcher.java
private static final String TAG = "FlickrFetcher";
private static final String API_KEY = "6feaae933e5e5fea2b3250027be7b5bd";
//获取并解析JSON数据
public List<GalleryItem> fetchItems() {
List<GalleryItem> itemList = new ArrayList<>();
try {
String url = Uri.parse("https://api.flickr.com/services/rest/")
.buildUpon()
.appendQueryParameter("method", "flickr.photos.getRecent")
.appendQueryParameter("api_key", API_KEY)
.appendQueryParameter("format", "json")
.appendQueryParameter("nojsoncallback", "1")
.appendQueryParameter("extras", "url_s")
.build().toString();
String jsonString = getUrlString(url);
Log.i(TAG, "获取到的JSON数据:" + jsonString);
JSONObject jsonBody = new JSONObject(jsonString);
parseItems(itemList, jsonBody);
}
catch (JSONException je) {
Log.e(TAG, "解析JSON数据失败!");
}
catch (IOException ioe) {
Log.e(TAG, "获取网站数据失败!");
}
return itemList;
}
在后台线程调用fetchItems()方法
PhotoGalleryFragment.java
private class FetchItemsTask extends AsyncTask<Void, Void, List<GalleryItem>> {
@Override
protected List<GalleryItem> doInBackground(Void... voids) {
return new FlickrFetcher().fetchItems();
}
//...
}
创建模型类GalleryItem
GalleryItem.java
public class GalleryItem {
private String mCaption;
private String mId;
private String mUrl;
//...
@NonNull
@Override
public String toString() {
return mCaption;
}
}
解析JSON数据
JSON对象是一系列包含在 { } 中的名值对。JSON数组是包含在 [ ] 中用逗号隔开的JSON对象列表。对象彼此嵌套形成层级关系。
json.org API提供有对应 JSON 数据的Java对象,如 JSONObject 和 JSONArray 。使用JSONObject(String) 构造函数,可以很方便地把 JSON 数据解析进相应的 Java对象 。
解析Flickr图片
FlickrFetcher.java
//解析JSON数据
private void parseItems(List<GalleryItem> itemList, JSONObject jsonBody) throws JSONException {
JSONObject photosJsonObject = jsonBody.getJSONObject("photos");
JSONArray photoJsonArray = photosJsonObject.getJSONArray("photo");
for (int i = 0; i < photoJsonArray.length(); i++) {
JSONObject photoJsonObject = photoJsonArray.getJSONObject(i);
GalleryItem item = new GalleryItem();
item.setId(photoJsonObject.getString("id"));
item.setCaption(photoJsonObject.getString("title"));
//并不是每张图片都有对应的url_s链接,需要每一个进行检查
//url_s这个参数告诉Flickr,如果有小尺寸图片,也一并返回其URL
if (!photoJsonObject.has("url_s")) continue;
item.setUrl(photoJsonObject.getString("url_s"));
itemList.add(item);
}
}
从AsyncTask回到主线程
添加ViewHolder实现
PhotoGalleryFragment.java
private class PhotoHolder extends RecyclerView.ViewHolder {
private TextView mTitleTextView;
public PhotoHolder(@NonNull View itemView) {
super(itemView);
mTitleTextView = (TextView) itemView;
}
public void bindGalleryItem(GalleryItem item) {
mTitleTextView.setText(item.toString());
}
}
添加Adapter实现
PhotoGalleryFragment.java
private class PhotoAdapter extends RecyclerView.Adapter<PhotoHolder> {
private List<GalleryItem> mGalleryItemList;
public PhotoAdapter(List<GalleryItem> galleryItemList) {
mGalleryItemList = galleryItemList;
}
@NonNull
@Override
public PhotoHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
LayoutInflater layoutInflater = LayoutInflater.from(getActivity());
View view = layoutInflater.inflate(android.R.layout.simple_list_item_1, parent, false);
return new PhotoHolder(view);
}
@Override
public void onBindViewHolder(@NonNull PhotoHolder holder, int position) {
GalleryItem galleryItem = mGalleryItemList.get(position);
holder.bindGalleryItem(galleryItem);
}
@Override
public int getItemCount() {
return mGalleryItemList.size();
}
}
添加setupAdapter()方法
PhotoGalleryFragment.java
private List<GalleryItem> mGalleryItemList = new ArrayList<>();
private void setupAdapter() {
//检查fragment是否与activity相关联
if (isAdded()) {
mPhotoRecyclerView.setAdapter(new PhotoAdapter(mGalleryItemList));
}
}
应在 onCreateView(…) 方法中调用该方法,这样每次因设备旋转重新生成 RecyclerView 时,可重新为其配置对应的adapter。另外,每次模型层对象发生改变时,也应及时调用该方法。
注意,配置adapter前,应检查 isAdded() 的返回值是否为 true 。该检查确认fragment已与目标activity相关联,进而保证 getActivity() 方法返回结果不为空。fragment可脱离任何activity而独立存在。在这之前,所有的方法调用都是由系统框架的回调方法驱动的,所以不会出现这种情况。如果fragment在接收回调指令,则它必然关联着某个activity;如它单独存在,就会收不到回调指令。
既然在用 AsyncTask ,我们就正在从后台进程触发回调指令。因而不能确定fragment是否关联着activity。必须检查确认fragment是否仍与activity关联。如果没有关联,依赖于activity的操作(如创建PhotoAdapter,进而还会使用托管activity作为context来创建 TextView )就会失败。
添加adapter更新代码
AsyncTask 提供有另一个可覆盖的 onPostExecute(…) 方法。 onPostExecute(…) 方法在 doInBackground(…) 方法执行完毕后才会运行。更为重要的是,它是在主线程而非后台线程上运行的。因此,在该方法中更新UI比较安全。
PhotoGalleryFragment.java
private class FetchItemsTask extends AsyncTask<Void, Void, List<GalleryItem>> {
@Override
protected List<GalleryItem> doInBackground(Void... voids) {
return new FlickrFetcher().fetchItems();
}
@Override
protected void onPostExecute(List<GalleryItem> galleryItemList) {
mGalleryItemList = galleryItemList;
setupAdapter();
}
}