中山大学数据科学与计算机学院本科生实验报告
传送门:项目源码
(2018年秋季学期)
一、实验题目
第十五周实验目的
- 理解Restful接口
- 学会使用Retrofit2
- 复习使用RxJava
- 学会使用OkHttp
二、实现内容
实现一个github用户repos以及issues应用
主界面有两个跳转按钮分别对应两次作业 | github界面,输入用户名搜索该用户所有可提交issue的repo,每个item可点击 |
---|---|
repo详情界面,显示该repo所有的issues | 加分项:在该用户的该repo下增加一条issue,输入title和body即可 |
- 教程位于
./manual/tutorial_retrofit.md
- 每次点击搜索按钮都会清空上次搜索结果再进行新一轮的搜索
- 获取repos时需要处理以下异常:HTTP 404 以及 用户没有任何repo
- 只显示 has_issues = true 的repo(即fork的他人的项目不会显示)
- repo显示的样式自由发挥,显示的内容可以自由增加(不能减少)
- repo的item可以点击跳转至下一界面
- 该repo不存在任何issue时需要弹Toast提示
- 不完成加分项的同学只需要显示所有issues即可,样式自由发挥,内容可以增加
三、实验结果
(1)实验截图
1.新增初始页面
2.进入GITHUB API初始页面
3 搜索dick20的github项目
4.点击进入其中一个项目,查看Issue
5.新建一个Issue
6.点击进入一个没有Issue的repo
(2)实验步骤以及关键代码
a.页面的设计
新增的内容也可以复用之前的页面架构,都是使用RecyclerView来显示列表的内容。具体的边距也没有很大的调整,只是单纯改变其中的Text。这里不再叙述。
b.通过用户名获取Github的Repo
首先,要设计获取过来Repo的内容要显示些什么。下面包括5个属性,name名字,description描述,id仓库的号码,has_issues表示该仓库是否包含Issue,open_issues表示开放issue的数量
public class Repo {
String name;
String description;
String id;
Boolean has_issues;
int open_issues;
···
}
创建RecyclerView就不再重复放置,这里重点说一下使用Retrofit2+RxJava如何实现get到用户的仓库。首先,我们先定义一个接口,使用GET参数,并且传入一个用户名。
public interface GitHubService {
@GET("users/{user_name}/repos")
Observable<List<Repo>> getRepo(@Path("user_name") String user_name);
}
第二步,分别定义OkHttpClient,设置其的超时时间限制。retrofit设置网络请求的Url基地址,添加支持RxJava的转换工厂。
private void request_repo(String userName){
OkHttpClient build = 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/") // 设置网络请求的Url地址
.addConverterFactory(GsonConverterFactory.create()) // 设置数据解析器
.addCallAdapterFactory(RxJavaCallAdapterFactory.create()) // 支持RxJava平台
.client(build)
.build();
第三步,根据之前的接口定义来创建repoObservable,然后就像对RxJava对象一样操作,在主线程观察其改变,在io线程订阅。其意义在于在UI线程来改变UI,而在io线程来进行网络访问
接着,填写onError函数来处理无法找到用户的情况。填写onNext函数来处理拿回来的List,将它一个个加入显示的列表中,最后利用adapter的notifyDataSetChanged,这样实现UI的变化。
GitHubService service = retrofit.create(GitHubService.class);
Observable<List<Repo>> repoObservable = service.getRepo(userName);
repoObservable
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe(new Observer<List<Repo>>() {
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
Toast.makeText(GithubActivity.this,
"无法找到该用户",Toast.LENGTH_SHORT).show();
}
@Override
public void onNext(List<Repo> repos) {
for (int i = 0; i < repos.size(); i++){
Log.i("list",repos.get(i).getName());
list.add(repos.get(i));
}
adapter.notifyDataSetChanged();
}
});
}
c.为每个仓库条目设置跳转事件
在获取完仓库列表后,要跳转到某特定仓库的里面,查看Issue的情况。这里利用的是我在Adapter定义的接口来重构点击事件,为其创建特定监听事件。
这里仅仅需要处理单击事件即可,长按事件可以不填忽略。
传递参数包括仓库的名字,用户名字,该仓库是否有Issue,这三个参数都是在Issue页面所要用到,通过API获取Issue必须的参数。
adapter.setOnItemClickListener(new
GithubRecyclerViewAdapter.OnItemClickListener() {
@Override
public void onClick(int position) {
Intent intent = new Intent();
Bundle bundle = new Bundle();
// 传递的三个参数
bundle.putString("repoName",
list.get(position).getName());
bundle.putString("userName",userName);
bundle.putBoolean("hasIssue",
list.get(position).getHas_issues());
intent.setClass(GithubActivity.this,IssueActivity.class);
intent.putExtras(bundle);
startActivity(intent);
}
@Override
public void onLongClick(int position) {
}
});
d.通过用户名,仓库名获取Issue
Issue类的设计,需要显示的内容包含title名称,body内容,created_at创建的时间,state状态表示该Issue是open还是close。
public class Issue {
String title;
String body;
String created_at;
String state;
···
}
与获取仓库类似,获取Issue这里还是需要GET参数,以及这次需要传入用户名以及仓库名
public interface IssueService {
@GET("repos/{user_name}/{repo_name}/issues")
Observable<List<Issue>> getIssue(@Path("user_name")
String user_name, @Path("repo_name") String repo_name);
}
RxJava的操作也类似,这里就不再重复说明,获取过来的issue也是添加到列表,然后adpter来刷新显示。
c.处理没有Issue的提示
跳转过来后,要先判断has_issue是否为真,如果不是则证明该仓库是fork来的,故没有issue这一功能,需要把显示与添加功能屏蔽掉,并显示toast提醒。
Boolean hasIssue = bundle.getBoolean("hasIssue");
if (hasIssue){
request_issue(userName,repoName);
// 加分项,绑定按键监听器,post一个issue
bind_post_issue();
}
// 显示提醒Toast
else{
Toast.makeText(IssueActivity.this,
"该repo不存在issue",Toast.LENGTH_SHORT).show();
}
(3)实验遇到的困难以及解决思路
a.POST操作时候,返回数据不正确,不能正常post
这个问题困扰了我很久,我post的结果是返回了一个json的数组,而在postman软件测试api的时候明明只返回了一个json,而且我得到的返回数据与get回来的数据是一样的,即post失败。
这个问题我首先是试着从token方面来找问题,修改了Headers以及Header写法都没有改变这个情况。紧接着,我试着将@Field改成@RequestBody来装载post过去的数据,并通过log来查看post的json内容,结果都是正确无误。这使我一度陷入怀疑人生的状态,在这个debug过程学会了多种post的方法却无一成功。
debug不成,就开始从头开始重构代码,结果被我反复查看,发现了我的api基地址填写的是http://api.github.com/而不是https://开头,这个错误导致了一直post不成功,甚至返回get正确的结果。这样的错误难以发现,但是经历这次错误后,以后使用api都会再三比对网址的准确性。
b.token的读取错误
这是一个比较搞笑的错误,但还是要记录一下,避免下次的犯错。本来是应该Authorization,我却将其拼写成了Authentication。这两个单词傻傻分不清楚,下次不会再犯,这样的错误是会有错误提示,比较好找,第一个错误就没有这么幸运了。
@Headers("Authorization:
token xxxx34ddbcb0xxxxxx1ce6xxxx0d9xxxx2fbxxxx")
c.RecyclerView显示丢失
在通过api获取完仓库列表后,我直接将获取后的list复制给adapter的list,结果UI显示不出来,输出list的数据却存在。
这是因为改变了list的地址,使到adapter所传入的list直接丢失掉。正确的做法是将新数据一个个从列表中拿出来,放置在初始化adapter的list中,这样就可以正常显示了。
// 错误
list = issues;
// 正确
for (int i = 0; i < issues.size(); i++){
Log.i("list",issues.get(i).getTitle());
list.add(issues.get(i));
}
四、实验思考及感想
a.加分项:实现POST一个评论
惯例,先写出接口函数,由于POST需要用户的一些权限。所以这里通过Headers传入了权限的token,该token可以从github的设置中获取。
POST参数,所使用的地址与GET一样,但是需要一个变量存放传入的Json。
@Headers("Authorization:
token xxxx34ddbcb0xxxxxx1ce6xxxx0d9xxxx2fbxxxx")
@POST("/repos/{user_name}/{repo_name}/issues")
Observable<Issue> postIssue(@Path("user_name") String user_name,
@Path("repo_name") String repo_name, @Body RequestBody requestBody);
创建OkHttpClient与Retrofit跟GET一样,不重复放代码。这里讲述新建一个JSONObject,将传入的数据按照键值对的形式put进去。然后RequestBody转换成json。接着我们就可以利用之前定义的post接口来获取单个Issue皆可。
JSONObject root = new JSONObject();
try {
root.put("title", title);
root.put("body", body);
} catch (JSONException e) {
e.printStackTrace();
}
RequestBody requestBody = RequestBody.create
(MediaType.parse("application/json"), root.toString());
Log.i("requestBody",root.toString());
IssueService service = retrofit.create(IssueService.class);
Observable<Issue> IssueObservable2 = service.postIssue
(userName,repoName,requestBody);
获取了Issue后,将它加入队列显示就完成了。
b.感想
这次Retrofit实验是在之前HttpConnection的基础下做的,其实除了用了原有的界面也没有太多的可复用性。这次实验还是加强了RecyclerView的使用技巧,复习了RxJava的多线程处理,学习到了新的访问网络获取数据的方式。相比较下来,使用Retrofit来获取网络的数据更加简便,而且接口简单,可读性强,配合RxJava更是完美解决获取数据与更新UI的矛盾。这次实验遇到了不少bug,网址写不正确使我学习到了更多的Retrofit参数使用,更学会使用了postman先测试使用接口,再来编写代码。