目录
前言
一、效果展示
二、基本配置
三、代码实战
3.1、创建RetrofitManager和APIService
3.2、准备好选择的图片
3.3、开始构造参数
3.4、实现上传
附:UploadHelper.java源码
距离上一篇文章到现在已经有将近半年的时间了,因为换了一座城市,到现在才算是刚刚熟悉起来吧,所以这段时间一直没能静下心来去总结,今天是周末,首先祝大家都能度过一个开心快乐的周末时光,我这个屌丝只能坐在家里码码文字了。今天总结一下关于在Android开发中使用Retrofit2实现类似于Web端表单文件上传的技术实现。
工作了也有将近三年的时间了,这期间关于这一块的实现方法也是换了几波,从最开始使用的基于Volley开发的MultipartRequest,到后来的OkHttp3,再到现在使用的Retrofit2,总结下来,其实都是大同小异。客户端这边主要就是构造请求参数,不同的参数需要指定其对应的参数类型,具体体现就是在构造参数时,需要指定Content-Type的类型(如果你平时观察过http请求,肯定会在Request Headers就是请求头中看到过它,我下面的日志截图里也有),然后构造完参数以后,剩下的就是和发送普通的Http请求一样了。服务端会有一个叫做enctype的东西,它的作用是告知服务器请求正文的MIME类型(和请求消息头的headers作用一样),然后服务端就根据对应的类型去解析,比如普通文字类型直接根据对应实体中的字段获取对应的值,文件类型的通过IO流读取写入文件,这样就完成了整个的上传流程。那么接下来,就来具体说说Android客户端Retrofit2的实现过程,至于服务端因为我懂得不多,所以就不在这里丢人了,简单点的大家可以尝试着写个Servlet去测试测试。
这里录了一个gif效果图,可以很清晰的看到上传的结果,我的网速太快了,我都没看清:
不过确实是上传成功了,来看一下日志:
既然是图文上传,这里重点讲的是上传,所以图片选择这一块我不打算多讲了,大家可以根据自己的实际业务去寻找对应的图片选择器(github上有很多),我这里使用的是MultiImageSelector,我把它的代码下载下来了,在本地项目中新建了一个Android Library,并给项目主Module添加上依赖,这样可以根据自己的需要进行定制,网络库这里肯定是选择Retrofit2.x版本了,直接添加对应的依赖到build.gradle文件中即可,结构如下图所示:
另外给大家推荐几款我觉得还不错的图片选择器的开源库,我这里因为没那么多需求,所以选了上面这个体积比较小的:
GalleryFinal:https://github.com/pengjianbo/GalleryFinal
PhotoPicker:https://github.com/donglua/PhotoPicker
Matisse(知乎开源的小清新款):https://github.com/zhihu/Matisse
在最初的http协议中,没有定义上传文件的Method,为了实现这个功能,http协议组改造了post请求,添加了一种post规范,设定这种规范的Content-Type为multipart/form-data;boundary=bound,其中bound,其中{bound}是定义的分隔符,用于分割各项内容(文件,key-value对),不然服务器无法正确识别各项内容。post body里需要用到,尽量保证随机唯一。Retrofit是个网络代理框架,负责封装请求,然后把请求分发给http协议具体实现者-httpclient。retrofit默认的httpclient是okhttp。Retrofit会根据注解封装网络请求,待httpclient请求完成后,把原始response内容通过转化器(converter)转化成我们需要的对象(object)。那么Retrofit和okhttp怎么封装这些multipart/form-data上传数据呢?答案如下:
@retrofit2.http.Multipart: 标记一个请求是multipart/form-data类型,需要和@retrofit2.http.POST一同使用,并且方法参数必须是@retrofit2.http.Part注解。
@retrofit2.http.Part: 代表Multipart里的一项数据,即用${bound}分隔的内容块。
@retrofit2.http.PartMap:用于表单字段,默认接受的类型是Map
了解了上面这些,就可以开始定义我们的ApiService了,注意这是个接口类,定义如下:
返回值类型这里Retrofit返回的是Call类型,如果你是使用的RxJava,这里直接返回Observable就行了,泛型这里传入的是服务端返回内容的一个实体类,这样最后直接根据实体类中的字段去进行结果的处理就OK了!
定义完了接口,接下来我们再去定义一个Retrofit的处理类,当然你也可以不定义,直接根据Retrofit的官方api在用到的地方调用也行,但是实际项目中肯定是要封装一个类,不然要写多少遍啊,不符合我们CV工程师的特点了!关于Retrofit的用法不再细说了,你既然能看到这里了,肯定不是个小白了,直接上代码:
package com.nari.yihui.api;
import android.support.annotation.NonNull;
import android.util.Log;
import com.nari.yihui.BaseApplication;
import com.nari.yihui.commonlib.ui.smartshow.SmartToast;
import com.nari.yihui.constants.ApiConstant;
import com.nari.yihui.helper.UserManager;
import com.nari.yihui.utils.CommonUtil;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
import retrofit2.converter.gson.GsonConverterFactory;
/**
* Created by 纪安奇 on 2018/6/11.
* Retrofit的管理类
*/
@SuppressWarnings("FieldCanBeLocal")
public class RetrofitManager {
//读超时时长,单位:毫秒
public static final int READ_TIME_OUT = 15 * 1000;
//写超时时长,单位:毫秒
public static final int WRITE_TIME_OUT = 15 * 1000;
//连接超时时长,单位:毫秒
public static final int CONNECT_TIME_OUT = 15 * 1000;
private static Retrofit mRetrofit; //Retrofit对象
private static ApiService mApiService; //接口类实例对象
private static OkHttpClient mClient; //OKHttp对象
//获取ApiService对象
public static ApiService getInstance() {
if (CommonUtil.isNetworkAvailable(BaseApplication.getAppContext())) {
//配置OkHttpClient
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.readTimeout(READ_TIME_OUT, TimeUnit.MILLISECONDS)
.writeTimeout(WRITE_TIME_OUT, TimeUnit.MILLISECONDS)
.connectTimeout(CONNECT_TIME_OUT, TimeUnit.MILLISECONDS)
.addInterceptor(getLoggerInterceptor());
//添加请求头Header
if (UserManager.getInstance().getUser() != null) {
builder.addInterceptor(new Interceptor() {
@Override
public Response intercept(@NonNull Chain chain) throws IOException {
Request request = chain.request()
.newBuilder()
.addHeader("Cookie", "sid=" + UserManager.getInstance().getUser().getObj().getSid())
.addHeader("Content-Type", "text/html;charset:utf-8")
.build();
return chain.proceed(request);
}
});
}
mClient = builder.build();
//配置Retrofit
mRetrofit = new Retrofit.Builder()
.baseUrl(ApiConstant.baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.client(mClient)
.build();
//创建ApiService对象
mApiService = mRetrofit.create(ApiService.class);
} else {
SmartToast.showInCenter("当前网络连接失败");
}
return mApiService;
}
//设置日志拦截器,打印返回的数据
private static HttpLoggingInterceptor getLoggerInterceptor() {
//日志显示级别
HttpLoggingInterceptor.Level level = HttpLoggingInterceptor.Level.BODY;
//新建log拦截器
HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
@Override
public void log(@NonNull String message) {
Log.e("ApiUrl------>", message);
}
});
loggingInterceptor.setLevel(level);
return loggingInterceptor;
}
}
既然是图片文字上传,首先我们肯定得准备好图片和文字,上面已经大致说了怎么去实现图片的选择,这里不会详细的说,因为篇幅有限,所以只讲如何上传。首先来看一下服务端需要我们如何拼接参数去提交的呢?
可以看到输入的意见文字字段为content,它需要我们和其它两部分内容一起拼接成一个json串然后提交,图片部分需要我们按照key-value的形式进行提交,key就是file,value就是我们对应的图片,有几张图片就有几组file=?,然后这两大部分一起构造成最后需要提交的参数。在我的项目中我把最后需要上传的图片文件都存放到了一个File[]类型的数组中,那么根据服务端的要求也就会写成(file,file[0])这种成对的形式。
有了上面的思路之后,就开始干吧。我在本地创建了一个UploadHelper类,初步的构想是将这个类采用单例模式的形式,然后对于参数这部分采用建造者模式,这样方便参数的添加,后面我会给出这个类的代码,这里就先截图了:
整个类采用单例模式:
对于参数部分,采用建造者模式进行构建:
我这里对于Map的value类型做了一个判断,文本和图片会根据对应的类型添加到Map中,是不是很方便,现在可以去真正的构造参数了,请看代码:
首先构造json串:
String jsonString = "{\"name\":\"" + name + "\",\"content\":\"" + content +
"\",\"province\":\"" +
BaseApplication.getSpUtil().getString(SPConstant.SP_PROVINCE) + "\"}";
接着构造图片,并且合并上面的json串:
helper = UploadHelper.getInstance();
if (bmp.size() == 1) {
helper.addParameter("file", files[0]);
} else if (bmp.size() == 2) {
helper.addParameter("file", files[0]);
helper.addParameter("file", files[1]);
} else if (bmp.size() == 3) {
helper.addParameter("file", files[0]);
helper.addParameter("file", files[1]);
helper.addParameter("file", files[2]);
}
Map params = helper.addParameter("param", json).builder();
最后是发送我们的Http请求,实现上传的功能,这里根据处理结果做一些UI上的更新:
LoadingDialog.show(this, "正在上传,请稍候...", false);
ApiConstant.baseUrl = ApiConstant.getBaseUrl(BaseUrlCode.FEEDBACK_UPLOAD);
RetrofitManager.getInstance().uploadFeedback(params).enqueue(new Callback() {
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) {
if (response.isSuccessful()) {
LoadingDialog.canceled();
if (response.body() != null) {
if (response.body().getCode() == 1000) {
SmartToast.showInCenter("上传成功");
finish();
overridePendingTransition(R.anim.in_from_right, R.anim.finish_to_right);
} else if (response.body().getCode() == 2004 || response.body().getCode() == 2006) {
SmartToast.showInCenter("登录失效,请重新登录");
CommonUtil.loginOut(FeedBackActivity.this);
} else {
SmartToast.showInCenter(response.body().getMsg());
}
}
}
}
@Override
public void onFailure(@NonNull Call call, @NonNull Throwable t) {
LoadingDialog.canceled();
LogUtil.e("意见反馈上传图文异常------" + t.getMessage());
}
});
以上就是整个图文上传的实现过程,如果有不对的地方,还请告知,谢谢大家!
package com.nari.yihui.helper;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Environment;
import android.provider.MediaStore;
import com.nari.yihui.commonlib.imageselector.MultiImageSelectorActivity;
import com.nari.yihui.constants.Constant;
import com.nari.yihui.utils.FileUtils;
import com.nari.yihui.utils.LogUtil;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import okhttp3.MediaType;
import okhttp3.RequestBody;
/**
* 包名:com.nari.yihui.helper
* 文件名: UploadHelper
* 创建时间: 2018/7/19 18:50
* 作者: 纪安奇
* 作用:图文上传工具类
*/
public class UploadHelper {
private volatile static UploadHelper mInstance;
public static Map params;
private Uri photoUri;
private UploadHelper() {}
//单例模式
public static UploadHelper getInstance() {
if (mInstance == null) {
synchronized (UploadHelper.class) {
if (mInstance == null)
mInstance = new UploadHelper();
params = new HashMap<>();
}
}
return mInstance;
}
//根据传进来的Object对象来判断是String还是File类型的参数
public UploadHelper addParameter(String key, Object o) {
RequestBody body = null;
if (o instanceof String) {
body = RequestBody.create(MediaType.parse("text/plain;charset=UTF-8"), (String) o);
} else if (o instanceof File) {
body = RequestBody.create(MediaType.parse("multipart/form-data;charset=UTF-8"), (File) o);
}
params.put(key, body);
return this;
}
//建造者模式
public Map builder() {
return params;
}
//清除参数
public void clear(){
params.clear();
}
//最终上传的Bitmap保存为File对象
public void saveBitmapFile(List mList,File[] files) {
String root = Environment.getExternalStorageDirectory().toString();
File myDir = new File(root + "/suggestionUpload");
if (myDir.exists()) {
myDir.delete();
}
myDir.mkdirs();
for (int i = 0; i < mList.size(); i++) {
files[i] = new File(myDir, "ims" + i + ".JPEG");
try {
if (files[i].exists()) {
files[i].delete();
}
files[i].createNewFile();
FileOutputStream out = new FileOutputStream(files[i]);
mList.get(i).compress(Bitmap.CompressFormat.JPEG, 100, out);
out.flush();
out.close();
LogUtil.e("最终上传图片的路径------>" + files[i].getAbsolutePath());
} catch (Exception e) {
e.printStackTrace();
}
}
}
//启用裁剪
public void startPhotoZoom(Activity mContext, List drr) {
try {
// 获取系统时间 然后将裁剪后的图片保存至指定的文件夹
@SuppressLint("SimpleDateFormat")
SimpleDateFormat sDateFormat = new SimpleDateFormat("yyyyMMddhhmmss");
String address = sDateFormat.format(new Date());
if (!FileUtils.isFileExist("")) {
FileUtils.createSDDir("");
}
drr.add(FileUtils.SDPATH + address + ".JPEG");
@SuppressLint("SdCardPath")
Uri imageUri = Uri.parse("file:///sdcard/formats/" + address + ".JPEG");
final Intent intent = new Intent("com.android.camera.action.CROP");
// 照片URL地址
intent.setDataAndType(photoUri, "image/*");
intent.putExtra("crop", "true");
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1);
intent.putExtra("outputX", 480);
intent.putExtra("outputY", 480);
// 输出路径
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
// 输出格式
intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
// 不启用人脸识别
intent.putExtra("noFaceDetection", false);
intent.putExtra("return-data", false);
mContext.startActivityForResult(intent, Constant.REQUEST_CUTTING_CODE);
} catch (IOException e) {
e.printStackTrace();
}
}
//拍照
public void takePhoto(Activity mContext) {
try {
Intent openCameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
String sdcardState = Environment.getExternalStorageState();
String sdcardPathDir = Environment.getExternalStorageDirectory().getPath() + "/myImage/";
File file = null;
if (Environment.MEDIA_MOUNTED.equals(sdcardState)) {
// 有sd卡,是否有myImage文件夹
File fileDir = new File(sdcardPathDir);
if (!fileDir.exists()) {
fileDir.mkdirs();
}
// 是否有headImg文件
file = new File(sdcardPathDir + System.currentTimeMillis() + ".JPEG");
}
if (file != null) {
String path = file.getPath();
photoUri = Uri.fromFile(file);
openCameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
mContext.startActivityForResult(openCameraIntent, Constant.REQUEST_CAMERA_CODE);
}
} catch (Exception e) {
e.printStackTrace();
}
}
//从相册选择
public void albmSelect(Activity mContext,int maxCanSelectNum,List drr) {
int selectedMode = MultiImageSelectorActivity.MODE_MULTI;
int maxNum = maxCanSelectNum - drr.size();
Intent intent = new Intent(mContext, MultiImageSelectorActivity.class);
// 是否显示拍摄图片
intent.putExtra(MultiImageSelectorActivity.EXTRA_SHOW_CAMERA, false);
// 最大可选择图片数量
intent.putExtra(MultiImageSelectorActivity.EXTRA_SELECT_COUNT, maxNum);
// 选择模式
intent.putExtra(MultiImageSelectorActivity.EXTRA_SELECT_MODE, selectedMode);
mContext.startActivityForResult(intent, Constant.REQUEST_ALBUM_CODE);
}
}