安卓开发笔记(十)—— Retrofit2+Rxjava网络访问,多线程(利用GitHub的api接口获取数据)

中山大学数据科学与计算机学院本科生实验报告

传送门:项目源码

(2018年秋季学期)

一、实验题目

第十五周实验目的

  1. 理解Restful接口
  2. 学会使用Retrofit2
  3. 复习使用RxJava
  4. 学会使用OkHttp

二、实现内容

实现一个github用户repos以及issues应用
img主界面有两个跳转按钮分别对应两次作业imggithub界面,输入用户名搜索该用户所有可提交issue的repo,每个item可点击
imgrepo详情界面,显示该repo所有的issuesimg加分项:在该用户的该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.新增初始页面

1

2.进入GITHUB API初始页面

2

3 搜索dick20的github项目

3

4.点击进入其中一个项目,查看Issue

4

5.新建一个Issue

5

6.点击进入一个没有Issue的repo

6

(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先测试使用接口,再来编写代码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值