📱 Android | 网络请求的使用

这次在安卓上通过网络请求实现一个哔哩哔哩用户视频获取软件以及获取 GitHub 用户 Repos 和 Issues 的应用,主要使用到的库有RxJavaRetrofit2以及okHttp

实现内容

实现一个 bilibili 的用户视频信息获取软件

  • 搜索框只允许正整数 int 类型,不符合的需要弹 Toast 提示
  • 当手机处于飞行模式或关闭 wifi 和移动数据的网络连接时,需要弹 Toast 提示
  • 由于 bilibili 的 API 返回状态有很多,这次我们特别的限制在以下几点
    • 基础信息 API 接口为: https://space.bilibili.com/ajax/top/showTop?mid=<user_id>
    • 图片信息 API 接口为基础信息 API 返回的 URL,cover 字段
    • 只针对前 40 的用户 id 进行处理,即user_id <= 40
    • [2,7,10,19,20,24,32]都存在数据,需要正确显示
  • 在图片加载出来前需要有一个加载条,不要求与加载进度同步
  • 布局和样式没有强制要求,只需要展示图片/播放数/评论/时长/创建时间/标题/简介的内容即可,可以自由发挥
  • 布局需要使用到 CardView 和 RecyclerView
  • 每个 item 最少使用 2 个 CardView,布局怎样好看可以自由发挥,不发挥也行
  • 不完成加分项的同学可以不显示 SeekBar
  • 输入框以及按钮需要一直处于顶部

实现一个 github 用户 repos 以及 issues 应用

  • 教程位于./manual/tutorial_retrofit.md
  • 每次点击搜索按钮都会清空上次搜索结果再进行新一轮的搜索
  • 获取 repos 时需要处理以下异常:HTTP 404 以及 用户没有任何 repo
  • 只显示 has_issues = true 的 repo(即 fork 的他人的项目不会显示)
  • repo 显示的样式自由发挥,显示的内容可以自由增加(不能减少)
  • repo 的 item 可以点击跳转至下一界面
  • 该 repo 不存在任何 issue 时需要弹 Toast 提示
  • 不完成加分项的同学只需要显示所有 issues 即可,样式自由发挥,内容可以增

应用截图

  • BiliBili
Screenshot_20181207-233438_HttpAPI应用主界面Screenshot_20181207-233451_HttpAPI正常搜索
Screenshot_20181207-233513_HttpAPI拖动进度条
  • Github
Screenshot_20181217-150559_HttpAPI首页Screenshot_20181217-150627_HttpAPI搜索 Repo
1545030696670添加 Issue1545030707860添加成功
1545030727467没有 Issue

界面

主界面由一个EditTextMaterialButtonRecyclerView组成,使用ConstraintLayout进行布局,输入框和按钮在顶部,而列表则填充剩下的部分。

列表中的布局由两个MaterialCardView组成,第一个卡片显示图片、标题、进度条等信息,第二个卡片显示主要内容。

在使用MaterialCardView之前,还需要先设置好依赖implementation 'com.android.support:design:28.0.0'

这些界面的布局都是非常基本的东西,简单地写一下就可以了,这里就不详细说明了。

网络请求

这里使用了HttpURLConnectionRxJava实现网络请求。

首先声明我们需要的两个 API

1
2
private final String baseURI = "https://space.bilibili.com/ajax/top/showTop?mid=%d";
private final String pVideoURI = "https://api.bilibili.com/pvideo?aid=%d";

然后编写一个根据用户 ID 获取基本信息的接口

HttpsURLConnection

这个使用起来十分简单,只需要使用 URL 的openConnection方法,就可以获取一个HttpsURLConnection

然后设置好一系列的参数(如 Get 方法、其实可以不用)

最后调用getInputStream方法就可以获得这次请求的回应数据主体。

使用Gson.fromJson方法就可以将回应的Json转化为一个类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
try {
URL api = new URL(String.format(Locale.getDefault(), baseURI, id));
HttpsURLConnection urlConnection = (HttpsURLConnection) api.openConnection();
urlConnection.setRequestMethod("GET");
InputStream in = new BufferedInputStream(urlConnection.getInputStream());
byte[] res = getBytesByInputStream(in);
in.close();
urlConnection.disconnect();
try {
BaseInfoObj baseInfoObj = new Gson().fromJson(new String(res), BaseInfoObj.class);
emitter.onNext(baseInfoObj);
} catch (JsonSyntaxException e) {
e.printStackTrace();
emitter.onError(e);
}
} catch (MalformedURLException e) {
e.printStackTrace();
emitter.onError(e);
}

RxJava

因为网络请求是异步的,因此我们可以使用RxJava来优雅地实现异步请求。

首先将上面的方法封装一下,返回一个Observable对象,在得到结果的时候调用mitter.onNext(baseInfoObj),发生错误的时候调用emitter.onError(e)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Observable<BaseInfoObj> getBaseInfo(final int id) {
return Observable.create(new ObservableOnSubscribe<BaseInfoObj>() {
@Override
public void subscribe(ObservableEmitter<BaseInfoObj> emitter) throws Exception {
try {
URL api = new URL(String.format(Locale.getDefault(), baseURI, id));
HttpsURLConnection urlConnection = (HttpsURLConnection) api.openConnection();
urlConnection.setRequestMethod("GET");
InputStream in = new BufferedInputStream(urlConnection.getInputStream());
byte[] res = getBytesByInputStream(in);
in.close();
urlConnection.disconnect();
try {
BaseInfoObj baseInfoObj = new Gson().fromJson(new String(res), BaseInfoObj.class);
emitter.onNext(baseInfoObj);
} catch (JsonSyntaxException e) {
e.printStackTrace();
emitter.onError(e);
}
} catch (MalformedURLException e) {
e.printStackTrace();
emitter.onError(e);
}
}
});
}

然后写一个Observer观察者,订阅这个网络事件,当数据返回的时候,就可以在Observer里面的onNext或者onError处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
 Observer<BaseInfoObj> baseInfoObserver = new Observer<BaseInfoObj>() {
@Override
public void onSubscribe(Disposable d) {

}

@Override
public void onNext(BaseInfoObj obj) {
if (obj.getStatus()) {
adapter.addData(obj);
binding.mainList.scrollToPosition(0);
}
}

@Override
public void onError(Throwable e) {
if (e instanceof JsonSyntaxException) {
Toast.makeText(activity, "数据库中不存在记录", Toast.LENGTH_SHORT).show();
} else if (e instanceof MalformedURLException) {
Toast.makeText(activity, "网络连接失败", Toast.LENGTH_SHORT).show();
}
}

@Override
public void onComplete() {

}
};

httpRequest.getBaseInfo(id)
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(baseInfoObserver);

而加载图片的请求和上面的基本差不多,也是使用RxJava调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Load Image
httpRequest.getImage(obj.getData().getCover())
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<Bitmap>() {
@Override
public void onSubscribe(Disposable d) {

}

@Override
public void onNext(Bitmap bitmap) {
binding.itemImage.setImageBitmap(bitmap);
binding.getModel().setImage(bitmap);
binding.itemProgressBar.setVisibility(View.GONE);
}

@Override
public void onError(Throwable e) {

}

@Override
public void onComplete() {

}
});

检测网络连接

这里使用ConnectivityManager来获取网络状态

1
2
3
4
5
6
7
8
9
10
11
private boolean isNetworkConnected() {
ConnectivityManager mConnectivityManager = (ConnectivityManager) activity
.getSystemService(Context.CONNECTIVITY_SERVICE);
if (mConnectivityManager != null) {
NetworkInfo mNetworkInfo = mConnectivityManager.getActiveNetworkInfo();
if (mNetworkInfo != null) {
return mNetworkInfo.isAvailable();
}
}
return false;
}

进度条

拖动进度条可以显示预览的图片,首先我们要获取预览的数据和图片。

我这里把他们放在了AdapterViewHolder进行bind的时候实现。

链式请求

这里涉及到两个连续的请求,就是需要先获取数据,然后根据数据中的地址获取图片。

这里就可以用到RxJavaflatMap实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
httpRequest.getPVideo(obj.getData().getAid())
.subscribeOn(Schedulers.newThread())
.flatMap(new Function<PVideoObj, Observable<List<Bitmap>>>() {
@Override
public Observable<List<Bitmap>> apply(PVideoObj pVideoObj) {
binding.getModel().setInfo(pVideoObj);
return httpRequest.getImages(pVideoObj.getData().getImage());
}
})
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<List<Bitmap>>() {
@Override
public void onSubscribe(Disposable d) {

}

@Override
public void onNext(List<Bitmap> bitmap) {
binding.getModel().setPvImage(bitmap);
}

@Override
public void onError(Throwable e) {

}

@Override
public void onComplete() {

}
});

这里看上去只有一个请求,而实际上是一个链式请求。

首先通过getPVideo获取 Json 信息,然后使用flatMap,当上一个请求的onNext被调用,结果就会被传递到apply函数里,在其apply函数中再次发起网络请求,并且将结果传递给下一个Observer

两个请求可以使用RxJava合二为一,链式调用完成。

图片切割

当进度条拖动的时候,就更新预览图,而预览图是一张 100 张图合成的大图。

首先计算出当前进度条说对应的图片的索引,然后根据 API 获取到的图片的信息(每行多少块、每列多少块、每块多长、每块多高),找出图片中预览图的起始xy以及heightwidth

最后使用Bitmap.createBitmap(pvImages.get(page),x,y,width,height)方法切割图片并且显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
List<Bitmap> pvImages = binding.getModel().getPvImage();
int currentIndex = binding.getModel().getCurrentIndex();
int totalTime = binding.getModel().getTotalTime();
PVideoObj data = binding.getModel().getInfo();
int newIndex = 0;
if (pvImages != null && pvImages.size() != 0 && data != null) {
int[] index = data.getData().getIndex();
progress = (int)(((double) progress / (double) totalTime) * index[index.length - 1]);
if (progress < index[index.length - 1]) {
for (int i = 1; i < index.length; i++) {
if (progress >= index[i - 1] && progress <= index[i]) {
newIndex = i;
break;
}
}
} else {
newIndex = index.length - 1;
}
if (newIndex != currentIndex) {
binding.getModel().setCurrentIndex(newIndex);
int row = newIndex % data.getData().getImg_x_len();
int col = (newIndex / data.getData().getImg_x_len()) % data.getData().getImg_y_len();
int page = newIndex / (data.getData().getImg_x_len() * data.getData().getImg_y_len());
if (pvImages.size() > page && pvImages.get(page) != null &&
(row + 1) * data.getData().getImg_x_size() <= pvImages.get(page).getWidth() &&
(col + 1) * data.getData().getImg_y_size() <= pvImages.get(page).getHeight()) {
binding.itemImage.setImageBitmap(Bitmap.createBitmap(pvImages.get(page),
row * data.getData().getImg_x_size(),
col * data.getData().getImg_y_size(),
data.getData().getImg_x_size(),
data.getData().getImg_y_size()));
}
}
}
}

当释放进度条时候,需要重置为原来的图片

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
binding.getModel().setCurrentIndex(-1);
}

@Override
public void onStopTrackingTouch(SeekBar seekBar) {
Bitmap bitmap = binding.getModel().getImage();
binding.getModel().setCurrentIndex(-1);
if (bitmap != null) {
binding.itemImage.setImageBitmap(bitmap);
}
}

Retrofit2 & okHttp

在 Github 请求部分,用到了Retrofit2okHttp

声明 API

Retrofit2 使用起来非常简单,他是基于注释实现 RESTful 风格的 API 请求,只需要写一个简单的接口,就可以实现请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface GitHubService {
@GET("users/{user}/repos")
@Headers("Authorization: token xxx")
Observable<List<RepoItem>> listRepos(@Path("user") String user);

@GET("repos/{user}/{repo}/issues")
@Headers("Authorization: token xxx")
Observable<List<IssueItem>> listIssues(@Path("user") String user, @Path("repo") String repo);

@POST("repos/{user}/{repo}/issues")
@Headers("Authorization: token xxx")
Observable<IssueItem> addIssue(@Path("user") String user, @Path("repo") String repo, @Body RequestBody body);
}

使用注释表明请求方法GET或者POST,然后还可以使用Headers给请求加上请求头

请求中动态变化的部分,使用{}标识出来,然后再参数中使用Path表明这个参数的值

对于POST请求,可以使用Body标识请求体

声明返回结构

对于返回的结果,需要声明一个类来解析

1
2
3
4
5
6
7
8
9
10
11
public class IssueItem {
private String title;

@SerializedName("created_at")
@Expose
private String createdAt;

private String body;

...// Get and Set
}

为了保持变量的驼峰命名法,可以使用@SerializedName表示序列化时候所对应的变量名

使用方法

1
2
3
4
5
6
7
8
9
10
11
12
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(2, TimeUnit.SECONDS)
.readTimeout(2, TimeUnit.SECONDS)
.writeTimeout(2, TimeUnit.SECONDS)
.build();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.client(client)
.build();
service = retrofit.create(GitHubService.class);

使用OkHttp承载Retrofit的请求,只需要声明一些参数然后使用.client()传给Retrofit即可

然后使用create创建服务

因为这里用了RxJava,因此返回的是Observable,和上面提到的用法一样,指定观察者就可以接收这次请求的数据,然后再做出处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
service.listRepos(username).observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.newThread())
.subscribe(new Observer<List<RepoItem>>() {
@Override
public void onSubscribe(Disposable d) {

}

@Override
public void onNext(List<RepoItem> repoItems) {
if (repoItems.size() != 0) {
adapter.refreshData(repoItems);
adapter.setUser(username);
} else {
Toast.makeText(activity,"该用户没有任何Repo", Toast.LENGTH_SHORT).show();
}
}

@Override
public void onError(Throwable e) {
e.printStackTrace();
Toast.makeText(activity, e.getMessage(), Toast.LENGTH_SHORT).show();
}

@Override
public void onComplete() {

}
});

POST 请求

POST 请求和 Get 有些不同,对于自定义的类型,需要提供一个结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
JSONObject result = new JSONObject();
try {
result.put("title", Objects.requireNonNull(binding.issueTitleInput.getEditText()).getText().toString());
result.put("body", Objects.requireNonNull(binding.issueBodyInput.getEditText()).getText().toString());
} catch (JSONException e) {
e.printStackTrace();
}
RequestBody body = RequestBody.create(MediaType.parse("application/vnd.github.symmetra-preview+json"), result.toString());
service.addIssue(user, repo, body).observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.newThread())
.subscribe(new Observer<IssueItem>() {
....
}

新建一个JSONObject对象,然后把数据体(Issue 的标题和内容)通过put方法传进去,然后使用RequestBody创建一个application/vnd.github.symmetra-preview+json类型的请求对象

然后调用Retrofit就可以发起 POST 请求了

总结

这次实验运用了网络请求,虽然说在期中项目上我们已经使用过RetrofitLiveData实现网络请求和数据库请求的结合,但是这次实验也进一步加深了对一些有认证的请求的用法。Github的 API 就是一套非常标准的Restful API非常适合我们进行学习。

土豪通道
0%