Android开发——网络请求
网络请求可以说是Android开发的核心,也同样是Android开发中的难点。常用的是
retrofit2
和
okhttp3
,它们都是对最基础的网络请求的一层封装。
1.依赖配置
//引入retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
//顺便引入gson方便对请求中传递的JSON字符串进行对象转化处理
implementation 'com.squareup.okhttp3:logging-interceptor:3.12.2'
api 'com.squareup.retrofit2:converter-gson:2.3.0'
implementation 'com.google.code.gson:gson:2.9.1'
2.post登录验证举例
我们以比较基础的登录注册为例来说明:
首先需要创建一个UploadService
,目的是为了统一管理各种请求接口,通知利用retrofit
提供的路由注解方便进行接口请求与规范化。
package com.example.mqttledmenu;
import okhttp3.MultipartBody;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.http.Field;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.GET;
import retrofit2.http.Multipart;
import retrofit2.http.POST;
import retrofit2.http.Part;
import retrofit2.http.Path;
import retrofit2.http.Query;
public interface UploadService {
//文件上传使用Multipart注解传递上传的文件流信息
@POST("upload")
@Multipart
Call<ResponseBody> upload(@Part MultipartBody.Part file);
//post请求中传递的表单信息使用@Field注解
//@Field("userName")中的userName为表单名称,
//String username分别为调用此接口的请求参数类型和参数名(get方式也是一样的)
@FormUrlEncoded//表单注解,用于传递key-value表单信息的post请求
@POST("login")
Call<ResponseBody> login(@Field("userName") String username, @Field("userPassword") String password);
//get请求中传递的请求参数使用@Query注解
@GET("getNews")
Call<ResponseBody> getnews(@Query("userId") String user_id);
//@Path注解,其实是和springboot配套的一种路由检索方式
@GET("detail/{id}")
Call<ResponseBody> getDetail(@Path("id") String news_id);
}
使用如下:
Retrofit retrofit = new Retrofit.Builder().baseUrl(Config.serverUrl).build();
UploadService uploadService = retrofit.create(UploadService.class);
Call<ResponseBody> call = uploadService.login(usernameStr, passwordStr);
call.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse (Call < ResponseBody > call, Response < ResponseBody > response){
String result = null;
try {
result = response.body().string();
//请求对象转化成json对象,然后利用get方法获取对象里面的详细信息
JsonObject returnData = new JsonParser().parse(result).getAsJsonObject();
if (!returnData.get("code").toString().equals("200")) {
Toast.makeText(LoginActivity.this, "用户名或密码错误~", Toast.LENGTH_SHORT).show();
return;
}
JsonObject temp = new JsonParser().parse(returnData.get("data").toString()).getAsJsonObject();
Config.getInstance().setUserId(temp.get("userId").toString());//存储用户的id
Config.getInstance().setUserAvatar(temp.get("userAvatar").toString());//存储用户的头像
if (isRemember.isChecked()) {
//存储用户用户名和密码
SharedPreferences.Editor edit = sp.edit();
edit.putString("username", usernameStr);
edit.putString("password", passwordStr);
edit.putBoolean("remember", true);
edit.apply();
}
Intent intent = new Intent();
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setClass(getApplicationContext(), MainActivity.class);
startActivity(intent);
} catch (IOException e) {
Toast.makeText(LoginActivity.this, "服务端异常~", Toast.LENGTH_SHORT).show();
e.printStackTrace();
}
}
@Override
public void onFailure (Call < ResponseBody > call, Throwable t){
}
});
3.网络请求列表处理
通常情况下,我们不仅会获取基础的成功和错误的响应,更多的是返回一个包含JSON数组信息,对于这种响应需要使用gson
依赖里面的JSONArray
。
Retrofit retrofit = new Retrofit.Builder().baseUrl(Config.serverUrl).build();
UploadService uploadService = retrofit.create(UploadService.class);
Call<ResponseBody> call = uploadService.getnews("");
//与后端约定,不传入id直接获取全部新闻
call.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
String result = null;
try {
result = response.body().string();
JsonObject returnData = new JsonParser().parse(result).getAsJsonObject();
if (!returnData.get("code").toString().equals("200")) {
Toast.makeText(getActivity(), "服务端错误~", Toast.LENGTH_SHORT).show();
return;
}
try {
//直接将得到的JSON字符串数组转换成数组对象
JSONArray myjsonArray = new JSONArray(returnData.get("data").toString());
for (int i = 0; i < myjsonArray.length(); i++) {
JSONObject jsonObject = myjsonArray.getJSONObject(i);
//利用得到的信息构造需要使用的自定义NewsItem对象
newsList.add(new NewsItem(jsonObject.get("authorAvatar").toString().replace("\"", ""),
(String) jsonObject.get("newsAuthor"), (String) jsonObject.get("newsDepartment"),
(String) jsonObject.get("newsTitle"), (String) jsonObject.get("newsContent"), (String) jsonObject.get("newsPublishTime"),
jsonObject.get("newsId").toString(), 1));
}
adapter = new SimpleAdapter(getActivity(), getData(newsList),
R.layout.news_adapter_item, new String[]{"avatar", "publisher", "title", "brief", "time"},
new int[]{R.id.avatar, R.id.publisher, R.id.title, R.id.brief, R.id.time});
//设置glide占位符
RequestOptions requestOptions = new RequestOptions()
.placeholder(R.drawable.loading)
.error(R.drawable.unknow_person)
.fallback(R.drawable.unknow_person);
//设置头像切换效果(其实比较耗性能)
DrawableCrossFadeFactory factory = new DrawableCrossFadeFactory.Builder().setCrossFadeEnabled(true).build();
adapter.setViewBinder(new SimpleAdapter.ViewBinder() {
@Override
public boolean setViewValue(View view, Object data, String textRepresentation) {
if (view instanceof ImageView) {
ImageView iv = (ImageView) view;
Glide.with(FragmentHome.this)
.load(data.toString())
.apply(requestOptions)
.transition(DrawableTransitionOptions.with(factory)).transform(new CircleCrop()).into(iv);
return true;
}
if (view.getId() == R.id.brief) {
TextView tv = (TextView) view;
String str = data.toString();
if (str.length() > 18) {
tv.setText(str.substring(0, 16) + "...");
return true;
}
}
return false;
}
});
setListAdapter(adapter);
} catch (JSONException e) {
Log.d("nightowl", "onResponse: 接口调用异常~");
e.printStackTrace();
}
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
}
});
为了方便大家理解,加上NewsItem对象代码:
public class NewsItem {
public String avatar;
public String publisher;
public String apartment;
public String title;
public String brief;
public String time;
public String news_id;
public Boolean isRead;
NewsItem(String avatar, String publisher, String apartment, String title, String brief, String time, String tag, int isRead) {
this.avatar = avatar;
this.publisher = publisher + "·" + apartment;
this.apartment = apartment;
this.title = title;
this.brief = brief;
this.time = time;
this.news_id = tag;
this.isRead = isRead == 1;
}
}
下面对具体的关键逻辑代码详细说明:
adapter =new SimpleAdapter(getActivity(),getData(newsList),
R.layout.news_adapter_item,new String[]{"avatar","publisher","title","brief","time"},
new int[]{R.id.avatar,R.id.publisher,R.id.title,R.id.brief,R.id.time});
这里其实调用了Android里面的滑动列表SimpleAdapter
构造器,这里的news_adapter_item
其实就是滑动列表的一个小组件块UI,然后利用实际需要被渲染进入的数组和R.id.*
数组写入数据:
下面给出news_adapter_item
单位UI块的实现代码:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="70dp"
android:orientation="horizontal"
android:padding="10dp"
tools:ignore="MissingConstraints">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="horizontal">
<ImageView
android:id="@+id/avatar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:layout_weight="6"
android:src="@drawable/loading" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="10dp"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/publisher"
android:layout_width="match_parent"
android:layout_height="18dp"
android:layout_gravity="left"
android:text="发布者"
android:textColor="@color/black"
android:textFontWeight="@integer/material_motion_duration_medium_1"
android:textSize="14dp" />
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="16dp"
android:layout_gravity="left"
android:text="通知标题"
android:textColor="@color/title"
android:textSize="14dp" />
<TextView
android:id="@+id/brief"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="left"
android:text="通知内容简介"
android:textColor="@color/brief" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="7"
android:orientation="vertical">
<TextView
android:id="@+id/time"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="9"
android:gravity="center"
android:text="消息时间"
android:textSize="10dp" />
<ImageView
android:id="@+id/read"
android:layout_width="match_parent"
android:layout_height="24dp"
android:layout_gravity="center"
android:layout_weight="1"
android:src="@drawable/has_read" />
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
一般这里的数组需要进一步处理,设计成map对象:
//对数据进行加载
private List<? extends Map<String, ?>> getData(ArrayList<NewsItem> newsList) {
List<Map<String, Object>> list = new ArrayList<Map<String, Object>>();
for (NewsItem item : newsList) {
Map<String, Object> map = new HashMap<String, Object>();
map.put("avatar", item.avatar);
map.put("publisher", item.publisher);
map.put("title", item.title);
map.put("brief", item.brief);
map.put("time", item.time);
list.add(map);
}
return list;
}
另外对于头像的处理,需要使用到glide
优化头像的显示:
//设置glide占位符
RequestOptions requestOptions = new RequestOptions()
.placeholder(R.drawable.loading)
.error(R.drawable.unknow_person)
.fallback(R.drawable.unknow_person);
//设置头像切换效果(其实比较耗性能)
DrawableCrossFadeFactory factory = new DrawableCrossFadeFactory.Builder().setCrossFadeEnabled(true).build();
adapter.setViewBinder(new SimpleAdapter.ViewBinder() {
@Override
public boolean setViewValue(View view, Object data, String textRepresentation) {
//由于这里使用的是网络路径的图片,因此需要使用glide加载图片资源(当然还有其他的方法),
//如果想更加友好的话,可以使用缓存进一步优化
if (view instanceof ImageView) {
ImageView iv = (ImageView) view;
Glide.with(FragmentHome.this)
.load(data.toString())
.apply(requestOptions)
.transition(DrawableTransitionOptions.with(factory))
.transform(new CircleCrop()).into(iv);
return true;
}
//对新闻的展示作长度截取处理,后面显示...号,使界面更加友好
if (view.getId() == R.id.brief) {
TextView tv = (TextView) view;
String str = data.toString();
if (str.length() > 18) {
tv.setText(str.substring(0, 16) + "...");
return true;
}
}
return false;
}
});
4.开发上的建议
设计到网络请求往往需要涉及到生产环境和开发环境的切换,因此建议使用一手单例模式,通过set
和get
函数来获取和修改全局使用的信息,例如用户id
等多个页面都需要使用到的信息:
public class Config {//使用单例模式进行全局配置
public static String serverUrl = "https://android.nightowl.top/";
public String user_id = "1";
public String avatar = "";
private static Config ApplicationConfig = new Config();
//让构造函数为 private,这样该类就不会被实例化
private Config() {
}
public static Config getInstance() {
return ApplicationConfig;
}
public void setUserAvatar(String avatar) {
this.avatar = avatar.replace("\"", "");
}
public void setUserId(String user_id) {
this.user_id = user_id;
}
public String getUserId() {
return user_id;
}
}