Retrofit2实现图片文字上传

目录

 

前言

一、效果展示

二、基本配置

三、代码实战

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效果图,可以很清晰的看到上传的结果,我的网速太快了,我都没看清:

不过确实是上传成功了,来看一下日志:

Retrofit2实现图片文字上传_第1张图片

二、基本配置

既然是图文上传,这里重点讲的是上传,所以图片选择这一块我不打算多讲了,大家可以根据自己的实际业务去寻找对应的图片选择器(github上有很多),我这里使用的是MultiImageSelector,我把它的代码下载下来了,在本地项目中新建了一个Android Library,并给项目主Module添加上依赖,这样可以根据自己的需要进行定制,网络库这里肯定是选择Retrofit2.x版本了,直接添加对应的依赖到build.gradle文件中即可,结构如下图所示:

Retrofit2实现图片文字上传_第2张图片

另外给大家推荐几款我觉得还不错的图片选择器的开源库,我这里因为没那么多需求,所以选了上面这个体积比较小的:

GalleryFinal:https://github.com/pengjianbo/GalleryFinal

PhotoPicker:https://github.com/donglua/PhotoPicker

Matisse(知乎开源的小清新款):https://github.com/zhihu/Matisse

三、代码实战

3.1、创建RetrofitManager和APIService

在最初的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;
    }

}

3.2、准备好选择的图片

既然是图片文字上传,首先我们肯定得准备好图片和文字,上面已经大致说了怎么去实现图片的选择,这里不会详细的说,因为篇幅有限,所以只讲如何上传。首先来看一下服务端需要我们如何拼接参数去提交的呢?

可以看到输入的意见文字字段为content,它需要我们和其它两部分内容一起拼接成一个json串然后提交,图片部分需要我们按照key-value的形式进行提交,key就是file,value就是我们对应的图片,有几张图片就有几组file=?,然后这两大部分一起构造成最后需要提交的参数。在我的项目中我把最后需要上传的图片文件都存放到了一个File[]类型的数组中,那么根据服务端的要求也就会写成(file,file[0])这种成对的形式。

3.3、开始构造参数

有了上面的思路之后,就开始干吧。我在本地创建了一个UploadHelper类,初步的构想是将这个类采用单例模式的形式,然后对于参数这部分采用建造者模式,这样方便参数的添加,后面我会给出这个类的代码,这里就先截图了:

整个类采用单例模式:

Retrofit2实现图片文字上传_第3张图片

对于参数部分,采用建造者模式进行构建:

Retrofit2实现图片文字上传_第4张图片

我这里对于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();

3.4、实现上传

最后是发送我们的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());
            }
        });

以上就是整个图文上传的实现过程,如果有不对的地方,还请告知,谢谢大家!

附:UploadHelper.java源码

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);
    }

}

 

你可能感兴趣的:(Android开发框架)