概述
本次快速开发Android应用系列,是基于课工场的公开课高效Android工程师6周培养计划,记录微服私访APP的整个开发过程以及当中碰到的问题,供日后学习参考。
上一篇我们主要实现android客户端的用户登录及验证。其中,使用TextInputLayout
和TextInputEditText
,代替传统的EditText
来输入用户名和密码。使用LitePal
代替android原生的DatabaseHelper
来实现本地数据库用户校验。
还没看过前一篇文章的朋友可以先去参考快速开发android应用2-使用TextInputLayout实现用户登录及验证
这是本系列的第三篇,主要实现基于okhttp解析服务端数据,并且以json格式返回给客户端,从而完成用户登录远程验证的功能。
效果图:
使用okhttp获取数据
在获取数据前,首先需要检查服务端接口是否正常,若不正常,请先检查服务器搭建过程。具体如何搭建请参考快速开发android应用1-服务器搭建
1. 打开mysql、tomcat服务
2. 在浏览器中输入http://localhost:8080/visitshop/login?userid=num01&password=123456
3. 若得到结果
{"code":0,"msg":"登录成功","body":{"userid":"num01","job":" 经理 ","nickname":"张华","phonenum":"18913145210","sex":0,"img":"visitshop/img/user/head.png","registdate":"2016-10-20","area":" 华中地区 "}}
则服务端接口正常
检查服务正常后,接下来就要使用okhttp来获取服务端数据了。
第一步,添加okhttp依赖库,sync project
compile 'com.squareup.okhttp3:okhttp:3.7.0'
第二步,使用okhttp发起登录post请求
public static void doLogin(Callback callback) {
//创建一个OkHttp实例
OkHttpClient httpClient = new OkHttpClient();
//创建一个body,包含params请求
RequestBody requestBody = new FormBody.Builder()
.add("userid", "num01")
.add("password", "123456")
.build();
//创建一个request对象
String loginUrl = "http://192.168.50.131:8080/visitshop/login";
Request request = new Request.Builder()
.url(loginUrl)
.post(requestBody)
.build();
//发起请求
httpClient.newCall(request).enqueue(callback);
}
有以下几点需要注意:
http get和post请求的区别是
get请求直接将请求参数放在url里,用于获取服务资源,如前面测试服务端注册接口http://localhost:8080/visitshop/login?userid=num01&password=123456
时,?后面的字符串就是请求参数;
而post请求则是将请求参数字符串放在了请求体中,用于获取或者修改服务资源;使用真机测试服务请求时,要使电脑和手机处在同一个网络(通常通过连接同一个wifi来实现),并修改ip地址为本机电脑的IP。
例子中的
httpClient.newCall(request).enqueue(callback)
,是异步的post请求,要通过callback
来完成请求成功或者失败的回调。
第三步,增加callback回调处理,打印服务端返回结果
OkHttpHelper.doLogin(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
//验证失败
mHandler.post(new Runnable() {
@Override
public void run() {
Toast.makeText(LoginActivity.this, "登录失败", Toast.LENGTH_SHORT).show();
mLoadingLayout.setVisibility(View.INVISIBLE);
}
});
}
@Override
public void onResponse(Call call, final Response response) throws IOException {
//验证成功
mHandler.post(new Runnable() {
@Override
public void run() {
Toast.makeText(LoginActivity.this, "登录成功", Toast.LENGTH_SHORT).show();
if(response.isSuccessful()) {
try {
String result = response.body().string();
Log.d(TAG, result);
...
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
mLoadingLayout.setVisibility(View.INVISIBLE);
}
});
}
});
这里的回调不在主线程,所以要操作UI或者弹出toast提示,都要通过handler
先切换到主线程。
观察log,若返回以下信息,则通过okhttp获取登录接口数据成功。
D/LoginActivity: {"code":0,"msg":"登录成功","body":{"userid":"num01","job":" 经理 ","nickname":"张华","phonenum":"18913145210","sex":0,"img":"visitshop/img/user/head.png","registdate":"2016-10-20","area":" 华中地区 "}}
第四步,检查服务端返回的json格式串是否正确。
可通过在线生成json网站来验证
若想了解更多关于okhttp的相关知识,可自行前往Android OkHttp相关解析 实践篇
使用Gson解析数据
当app获取到json字符串时,我们需要把json字符串解析成java中的对象,再进行具体业务的处理,Gson就提供这样的功能。
第一步,配置gson依赖库
compile 'com.google.code.gson:gson:2.8.0'
第二步,安装GsonFormat插件(此步骤主要用于方便自动生成json实体类)
找到setting->Plugins
搜索 gsonformat 安装
重启android studio
第三步,新建一个json实体类LoginResult
,插入代码(快捷键Alt+Insert
)
第四步,完成json串到LoginResult的转换
//解析json数据
Gson gson = new Gson();
LoginResult loginResult = gson.fromJson(result, LoginResult.class);
保存User到数据库
当用户发出登录请求,得到后台返回的用户信息后,需要把用户信息保存到数据库中。
这样当下次进入app后,就不需要重新登录,直接进入主界面了。
要实现这个功能,首先需要根据user表
创建一个User类
/**
* desc: 用户实体类
* author: youyutorch
* date: 2017/7/12 0012 23:47
*/
public class User extends DataSupport {
private int id;
private String userId;
private String passWord;
private String job;
private String nickName;
private int sex;// 性别 1:男,0:女
private String img;
private String phoneNum;
private String area;
public User() {
}
...
}
然后,新建一个User对象,并保存到数据库中
//利用loginResult对象,构造user对象
if (loginResult != null) {
if (loginResult.getCode() == 0) {
//成功获取到用户信息
LoginResult.BodyBean bodyBean = loginResult.getBody();
User user = new User();
user.setUserId(bodyBean.getUserid());
user.setArea(bodyBean.getArea());
user.setImg(bodyBean.getImg());
user.setNickName(bodyBean.getNickname());
user.setSex(bodyBean.getSex());
user.setPhoneNum(bodyBean.getPhonenum());
user.setJob(bodyBean.getJob());
//保存到本地数据库
Log.d(TAG, "保存前:" + user.toString());
boolean saveFlag = user.save();
Log.d(TAG, "保存后:" + user.toString());
//保存到sharepreference中,下次进入应用默认登入
if (saveFlag) {
SharePreUtil.SetShareString(LoginActivity.this, "userId", bodyBean.getUserid());
}
//跳转到主页面
Intent intent = new Intent(LoginActivity.this, MainActivity.class);
startActivity(intent);
finish();
}
最后,在LoginActivity
的onCreate()
方法中判断是否需要登录
//判断是否已登录
String userId = SharePreUtil.GetShareString(this, "userId");
if (!TextUtils.isEmpty(userId)) {
//跳转到主页面
Intent intent = new Intent(LoginActivity.this, MainActivity.class);
startActivity(intent);
finish();
}
okhttp网络请求封装
前面我们发起的http登录请求,虽然能得到正确的结果,但也存在一些问题:
- 登录请求的url写死在方法
doLogin()
中,不方便修改 - 其他的请求接口如注册、广播轮播、任务获取等,整个请求过程很相似,但不能复用
- http请求回调逻辑复杂,没有进行剥离,和
LoginActivity
耦合在一起
那如何才能解决以上这些问题呢?可以考虑用封装的思路去优化
我对封装的理解是
结合项目,从面向对象的角度,在完成业务功能的基础上,对代码进行抽象、整合,尽可能实现一个独立可复用的功能模块。
比如说,我们现在要对当前项目的网络请求进行封装。
第一步,结合项目
当前网络请求包括注册、登录、任务获取等接口,发现这些接口:基础url是一样的;访问路径不同;请求类型不同(Get请求或者post请求);返回结果都是json格式。
第二步,面向对象
根据前面的描述,我们可以抽象出一个RequestUrl
常量类,专门存储各接口url,每个url都由base url
+ relative url
拼接而成
public class RequestUrl {
public static final int HttpOk = 7000;//成功
public static final int HttpFail = 7001;//失败
public static final String BaseUrl = "http://192.168.43.232:8080";//模拟器根接口
// public static final String BaseUrl = "http://192.168.9.232:8080";//根接口
public static final String Login = BaseUrl + "/visitshop/login";//登录get
public static final String FeedBack = BaseUrl + "/visitshop/feedback";//意见反馈post
public static final String Announcement = BaseUrl + "/visitshop/announcement";//公告获取get
public static final String Task = BaseUrl + "/visitshop/task";//任务获取get
public static final String Info = BaseUrl + "/visitshop/info";//咨询获取get
public static final String AppUpdate = BaseUrl + "/visitshop/appinfo";//app更新get
public static final String HistroyShop = BaseUrl + "/visitshop/history";//历史巡店get
public static final String ShopSelect = BaseUrl + "/visitshop/shop";//店面选择get
public static final String VisitShopSubmit = BaseUrl + "/visitshop/visitupload";//巡店数据提交post
public static final String Train = BaseUrl + "/visitshop/historytrain";//培训列表接口get
public static final String TrainDetail = BaseUrl + "/visitshop/triandetail";//培训详情get
public static final String TrainSubmit = BaseUrl + "/visitshop/trainupload";//培训数据提交post
public static final String InterviewSubmit = BaseUrl + "/visitshop/interviewsubmit";//拜访提交post
public static final String HistoryInterview = BaseUrl + "/visitshop/historyinterview";//历史拜访post
public static final String UpdateUser = BaseUrl + "/visitshop/updateuser";//更新用户资料
public static final String UpdateHead = BaseUrl + "/visitshop/uploadhead";//更新用户资料
}
抽象出一个OkHttpHelper类,用于发起网络请求,将请求逻辑放在这个类里,降低和LoginActivity的耦合。因为访问网络可以使用相同的httpclient
对象,故可以使用单例模式来获取OkHttpHelper
对象。
private OkHttpHelper() {
mMainHandler = new Handler(Looper.getMainLooper());
mOkHttpClient = new OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.build();
}
public static synchronized OkHttpHelper getInstance() {
if (mOkHttpHelper == null) {
mOkHttpHelper = new OkHttpHelper();
}
return mOkHttpHelper;
}
第三步,实现可复用模块
抽象出相同的地方,将剥离出来的不同地方用接口方式暴露给调用方。
当前项目可以抽象出doGet()
和doPost()
方法,同时调用时,传入不同的url
以及请求参数
,并将请求参数抽象成一个params
实体类
/**
* desc: 请求参数实体类
* author: tianyouyu
* date: 2017/7/13 0013 15:41
*/
public class Params {
public static final String KEY_USERID = "userid";
public static final String KEY_PASSWORD = "password";
private Map<String, String> requestMap = new HashMap<>();
public Params() {
}
public Params(Map<String, String> map) {
if (map == null) {
requestMap = new HashMap<>();
} else {
requestMap = map;
}
}
public void putParam(String key, String value) {
if (TextUtils.isEmpty(key) || TextUtils.isEmpty(value)) {
return;
}
requestMap.put(key, value);
}
public Map<String, String> getRequestMap() {
return requestMap;
}
}
//get请求
public void doGet(String url, final RequestCallback callback) {
if (TextUtils.isEmpty(url)) {
if (callback != null) {
callback.onFailure(new IOException("请求地址不合法"));
}
return;
}
doRequest(url, null, callback);
}
//post请求
public void doPost(String url, Params params, final RequestCallback callback) {
if (TextUtils.isEmpty(url)) {
if (callback != null) {
callback.onFailure(new IOException("请求地址不合法"));
}
return;
}
//创建一个RequestBody对象
FormBody.Builder builder = new FormBody.Builder();
if (params != null) {
for (Map.Entry entry : params.getRequestMap().entrySet()) {
builder.add((String) entry.getKey(), (String) entry.getValue());
}
}
doRequest(url, builder.build(), callback);
}
//实际请求发起的地方
private void doRequest(String url, RequestBody body, final RequestCallback callback) {
//创建一个request对象
Request.Builder builder = new Request.Builder();
builder.url(url);
if (body != null) {
builder.post(body);
}
Request request = builder.build();
//发起请求
mOkHttpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, final IOException e) {
postFailure(callback, e);
}
@Override
public void onResponse(Call call, final Response response) throws IOException {
if (response.isSuccessful()) {
String result = null;
try {
result = response.body().string();
} catch (IOException e) {
e.printStackTrace();
postFailure(callback, e);
return;
}
postSuccess(callback, result);
} else {
postFailure(callback, new IOException("获取response出错"));
}
}
});
}
自定义接口,将回调跳转回到主线程处理,并简化回调接口
public interface RequestCallback {
void onSuccess(String result);
void onFailure(IOException e);
}
第四步,调用封装后的网络请求
/**
* 用户名验证
* @param name
* @param pwd
*/
private void verifyUser(String name, String pwd) {
//显示loading
mLoadingLayout.setVisibility(View.VISIBLE);
//新建请求params对象
Params params = new Params();
params.putParam(Params.KEY_USERID, name);
params.putParam(Params.KEY_PASSWORD, pwd);
OkHttpHelper.getInstance().doPost(RequestUrl.Login, params, new OkHttpHelper.RequestCallback() {
@Override
public void onSuccess(String result) {
//解析json串,得到user对象
User user = GsonUtil.parseLoginJson(result);
if (user == null) {
String msg = "登录失败:";
if (TextUtils.isEmpty(GsonUtil.getReason())) {
msg += "json解析出错";
} else {
msg += GsonUtil.getReason();
}
showToast(msg);
} else {
boolean saveFlag = user.save();
Log.d(TAG, "保存User到数据库:" + user.toString());
//保存到sharepreference中,下次进入应用默认登入
if (saveFlag) {
SharePreUtil.SetShareString(LoginActivity.this, "userId", user.getUserId());
}
//跳转到主页面
Intent intent = new Intent(LoginActivity.this, MainActivity.class);
startActivity(intent);
finish();
}
mLoadingLayout.setVisibility(View.INVISIBLE);
}
@Override
public void onFailure(IOException e) {
e.printStackTrace();
showToast("登录失败:" + e.getMessage());
mLoadingLayout.setVisibility(View.INVISIBLE);
}
});
}
你会发现,所有和网络请求相关的代码都放在OkHttpHelper
中,同时,OkHttpHelper
作为独立的一个网络请求模块(功能),供其他类调用。
总结
- 尽量自己去实现项目的功能以及业务的逻辑,碰到不懂的地方,再参考去老师给的源码,学习的过程在于实践。
- 开发项目代码过程中,要注意编码的规范,好的习惯都是在平时形成的
- 尽量尝试自己去解决问题、优化项目代码
- 多多学习参考优秀源码(这点我自己也做的不好,需要加强)
附录
快速开发android应用相关的代码都会更新在我的github上,大家可以通过star来跟进项目代码的变动https://github.com/youyutorch/RapidDevAndroid。