Можно ли показать индикатор выполнения При загрузке изображения через Retrofit 2


Я сейчас использую Retrofit 2 и я хочу загрузить некоторые фотографии на моем сервере. Я знаю, что более старая версия использует TypedFile класс для загрузки. И если мы хотим использовать индикатор выполнения с ним мы должны переопределить writeTo метод TypedFile класса.

можно ли показать прогресс при использовании retrofit 2 библиотеки?

10 64

10 ответов:

прежде всего, вы должны использовать Retrofit 2 версии равной или выше 2.0 beta2. Во-вторых, создать новый класс, расширяющий RequestBody:

    public class ProgressRequestBody extends RequestBody {
    private File mFile;
    private String mPath;
    private UploadCallbacks mListener;

    private static final int DEFAULT_BUFFER_SIZE = 2048;

    public interface UploadCallbacks {
        void onProgressUpdate(int percentage);
        void onError();
        void onFinish();
    }

    public ProgressRequestBody(final File file, final  UploadCallbacks listener) {
        mFile = file;
        mListener = listener;            
    }

    @Override
    public MediaType contentType() {
        // i want to upload only images
        return MediaType.parse("image/*");
    }

    @Override
    public long contentLength() throws IOException {
      return mFile.length();
    }

    @Override
    public void writeTo(BufferedSink sink) throws IOException {
        long fileLength = mFile.length();
        byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
        FileInputStream in = new FileInputStream(mFile);
        long uploaded = 0;

        try {
            int read;
            Handler handler = new Handler(Looper.getMainLooper());
            while ((read = in.read(buffer)) != -1) {

                // update progress on UI thread
                handler.post(new ProgressUpdater(uploaded, fileLength));

                uploaded += read;
                sink.write(buffer, 0, read);
            }
        } finally {
            in.close();
        }
    }

    private class ProgressUpdater implements Runnable {
        private long mUploaded;
        private long mTotal;
        public ProgressUpdater(long uploaded, long total) {
            mUploaded = uploaded;
            mTotal = total;
        }

        @Override
        public void run() {
            mListener.onProgressUpdate((int)(100 * mUploaded / mTotal));            
        }
    }
}

в-третьих, создать интерфейс

@Multipart
    @POST("/upload")        
    Call<JsonObject> uploadImage(@Part MultipartBody.Part file);

теперь вы можете получить прогресс загрузки.

в своем activity (или fragment):

class MyActivity extends AppCompatActivity implements ProgressRequestBody.UploadCallbacks {
        ProgressBar progressBar;

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);

            progressBar = findViewById(R.id.progressBar);

            ProgressRequestBody fileBody = new ProgressRequestBody(file, this);
            MultipartBody.Part filePart = MultipartBody.Part.createFormData("image", file.getName(), fileBody);

            Call<JsonObject> request = RetrofitClient.uploadImage(filepart);
            request.enqueue(new Callback<JsonObject>{...});
        }

        @Override
        public void onProgressUpdate(int percentage) {
            // set current progress
            progressBar.setProgress(percentage);
        }

        @Override
        public void onError() {
            // do something on error
        }

        @Override
        public void onFinish() {
            // do something on upload finished
            // for example start next uploading at queue
            progressBar.setProgress(100);
        }


    }

модифицированный Юрий Колбасинский использовать rxjava и использовать Котлин. Добавлен обходной путь для использования HttpLoggingInterceptor в то же время

class ProgressRequestBody : RequestBody {

val mFile: File
val ignoreFirstNumberOfWriteToCalls : Int


constructor(mFile: File) : super(){
    this.mFile = mFile
    ignoreFirstNumberOfWriteToCalls = 0
}

constructor(mFile: File, ignoreFirstNumberOfWriteToCalls : Int) : super(){
    this.mFile = mFile
    this.ignoreFirstNumberOfWriteToCalls = ignoreFirstNumberOfWriteToCalls
}


var numWriteToCalls = 0

protected val getProgressSubject: PublishSubject<Float> = PublishSubject.create<Float>()

fun getProgressSubject(): Observable<Float> {
    return getProgressSubject
}


override fun contentType(): MediaType {
    return MediaType.parse("video/mp4")
}

@Throws(IOException::class)
override fun contentLength(): Long {
    return mFile.length()
}

@Throws(IOException::class)
override fun writeTo(sink: BufferedSink) {
    numWriteToCalls++

    val fileLength = mFile.length()
    val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
    val `in` = FileInputStream(mFile)
    var uploaded: Long = 0

    try {
        var read: Int
        var lastProgressPercentUpdate = 0.0f
        read = `in`.read(buffer)
        while (read != -1) {

            uploaded += read.toLong()
            sink.write(buffer, 0, read)
            read = `in`.read(buffer)

            // when using HttpLoggingInterceptor it calls writeTo and passes data into a local buffer just for logging purposes.
            // the second call to write to is the progress we actually want to track
            if (numWriteToCalls > ignoreFirstNumberOfWriteToCalls ) {
                val progress = (uploaded.toFloat() / fileLength.toFloat()) * 100f
                //prevent publishing too many updates, which slows upload, by checking if the upload has progressed by at least 1 percent
                if (progress - lastProgressPercentUpdate > 1 || progress == 100f) {
                    // publish progress
                    getProgressSubject.onNext(progress)
                    lastProgressPercentUpdate = progress
                }
            }
        }
    } finally {
        `in`.close()
    }
}


companion object {

    private val DEFAULT_BUFFER_SIZE = 2048
}

}

пример интерфейса загрузки видео

public interface Api {

    @Multipart
    @POST("/upload")        
    Observable<ResponseBody> uploadVideo(@Body MultipartBody requestBody);
}

пример функции для публикации видео:

fun postVideo(){
            val api : Api = Retrofit.Builder()
            .client(OkHttpClient.Builder()
                    //.addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
                    .build())
            .baseUrl("BASE_URL")
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .build()
            .create(Api::class.java)

    val videoPart = ProgressRequestBody(File(VIDEO_URI))
    //val videoPart = ProgressRequestBody(File(VIDEO_URI), 1) //HttpLoggingInterceptor workaround
    val requestBody = MultipartBody.Builder()
            .setType(MultipartBody.FORM)
            .addFormDataPart("example[name]", place.providerId)
            .addFormDataPart("example[video]","video.mp4", videoPart)
            .build()

    videoPart.getProgressSubject()
            .subscribeOn(Schedulers.io())
            .subscribe { percentage ->
                Log.i("PROGRESS", "${percentage}%")
            }

    var postSub : Disposable?= null
    postSub = api.postVideo(requestBody)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe({ r ->
            },{e->
                e.printStackTrace()
                postSub?.dispose();

            }, {
                Toast.makeText(this,"Upload SUCCESS!!",Toast.LENGTH_LONG).show()
                postSub?.dispose();
            })
}

вот как обрабатывать прогресс загрузки файлов с помощью простого сообщения, а не нескольких частей. Для multipart проверьте решение @Yariy. Кроме того, это решение использует URI содержимого вместо прямых ссылок на файлы.

RestClient

@Headers({
    "Accept: application/json",
    "Content-Type: application/octet-stream"
})
@POST("api/v1/upload")
Call<FileDTO> uploadFile(@Body RequestBody file);

ProgressRequestBody

public class ProgressRequestBody extends RequestBody {
    private static final String LOG_TAG = ProgressRequestBody.class.getSimpleName();

    public interface ProgressCallback {
        public void onProgress(long progress, long total);
    }

    public static class UploadInfo {
        //Content uri for the file
        public Uri contentUri;

        // File size in bytes
        public long contentLength;
    }

    private WeakReference<Context> mContextRef;
    private UploadInfo mUploadInfo;
    private ProgressCallback mListener;

    private static final int UPLOAD_PROGRESS_BUFFER_SIZE = 8192;

    public ProgressRequestBody(Context context, UploadInfo uploadInfo, ProgressCallback listener) {
        mContextRef = new WeakReference<>(context);
        mUploadInfo =  uploadInfo;
        mListener = listener;
    }

    @Override
    public MediaType contentType() {
        // NOTE: We are posting the upload as binary data so we don't need the true mimeType
        return MediaType.parse("application/octet-stream");
    }

    @Override
    public void writeTo(BufferedSink sink) throws IOException {
        long fileLength = mUploadInfo.contentLength;
        byte[] buffer = new byte[UPLOAD_PROGRESS_BUFFER_SIZE];
        InputStream in = in();
        long uploaded = 0;

        try {
            int read;
            while ((read = in.read(buffer)) != -1) {
                mListener.onProgress(uploaded, fileLength);

                uploaded += read;

                sink.write(buffer, 0, read);
            }
        } finally {
            in.close();
        }
    }

    /**
     * WARNING: You must override this function and return the file size or you will get errors
     */
    @Override
    public long contentLength() throws IOException {
        return mUploadInfo.contentLength;
    }

    private InputStream in() throws IOException {
        InputStream stream = null;
        try {
            stream = getContentResolver().openInputStream(mUploadInfo.contentUri);            
        } catch (Exception ex) {
            Log.e(LOG_TAG, "Error getting input stream for upload", ex);
        }

        return stream;
    }

    private ContentResolver getContentResolver() {
        if (mContextRef.get() != null) {
            return mContextRef.get().getContentResolver();
        }
        return null;
    }
}

начать загрузку:

// Create a ProgressRequestBody for the file
ProgressRequestBody requestBody = new ProgressRequestBody(
    getContext(),
    new UploadInfo(myUri, fileSize),
    new ProgressRequestBody.ProgressCallback() {
        public void onProgress(long progress, long total) {
            //Update your progress UI here
            //You'll probably want to use a handler to run on UI thread
        }
    }
);

// Upload
mRestClient.uploadFile(requestBody);

внимание, если вы забыли переопределить функцию contentLength (), вы можете получить несколько неясных ошибки:

retrofit2.adapter.rxjava.HttpException: HTTP 503 client read error

или

Write error: ssl=0xb7e83110: I/O error during system call, Broken pipe

или

javax.net.ssl.SSLException: Read error: ssl=0x9524b800: I/O error during system call, Connection reset by peer

это результат RequestBody.writeTo() вызывается несколько раз, поскольку по умолчанию contentLength () равно -1.

в любом случае это заняло много времени, чтобы выяснить, надеюсь, это поможет.

Полезные ссылки: https://github.com/square/retrofit/issues/1217

Я обновляю progressbar onProgressUpdate. Этот код может получить лучшую производительность.

@Override
public void writeTo(BufferedSink sink) throws IOException {
    long fileLength = mFile.length();
    byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
    FileInputStream in = new FileInputStream(mFile);
    long uploaded = 0;

    try {
        int read;
        Handler handler = new Handler(Looper.getMainLooper());
        int num = 0;
        while ((read = in.read(buffer)) != -1) {

            int progress = (int) (100 * uploaded / fileLength);
            if( progress > num + 1 ){
                // update progress on UI thread
                handler.post(new ProgressUpdater(uploaded, fileLength));
                num = progress;
            }

            uploaded += read;
            sink.write(buffer, 0, read);
        }
    } finally {
        in.close();
    }
}

@luca992 Спасибо за ваш ответ. Я реализовал это в Java, и теперь он работает нормально.

public class ProgressRequestBodyObservable extends RequestBody {

    File file;
    int ignoreFirstNumberOfWriteToCalls;
    int numWriteToCalls;`enter code here`

    public ProgressRequestBodyObservable(File file) {
        this.file = file;

        ignoreFirstNumberOfWriteToCalls =0;
    }

    public ProgressRequestBodyObservable(File file, int ignoreFirstNumberOfWriteToCalls) {
        this.file = file;
        this.ignoreFirstNumberOfWriteToCalls = ignoreFirstNumberOfWriteToCalls;
    }


    PublishSubject<Float> floatPublishSubject = PublishSubject.create();

   public Observable<Float> getProgressSubject(){
        return floatPublishSubject;
    }

    @Override
    public MediaType contentType() {
        return MediaType.parse("image/*");
    }

    @Override
    public long contentLength() throws IOException {
        return file.length();
    }



    @Override
    public void writeTo(BufferedSink sink) throws IOException {
        numWriteToCalls++;


        float fileLength = file.length();
        byte[] buffer = new byte[2048];
        FileInputStream in = new  FileInputStream(file);
        float uploaded = 0;

        try {
            int read;
            read = in.read(buffer);
            float lastProgressPercentUpdate = 0;
            while (read != -1) {

                uploaded += read;
                sink.write(buffer, 0, read);
                read = in.read(buffer);

                // when using HttpLoggingInterceptor it calls writeTo and passes data into a local buffer just for logging purposes.
                // the second call to write to is the progress we actually want to track
                if (numWriteToCalls > ignoreFirstNumberOfWriteToCalls ) {
                    float progress = (uploaded / fileLength) * 100;
                    //prevent publishing too many updates, which slows upload, by checking if the upload has progressed by at least 1 percent
                    if (progress - lastProgressPercentUpdate > 1 || progress == 100f) {
                        // publish progress
                        floatPublishSubject.onNext(progress);
                        lastProgressPercentUpdate = progress;
                    }
                }
            }
        } finally {
        in.close();
        }

    }
}

чтобы избежать дважды запущенной проблемы. Мы можем установить флаг как ноль изначально .И установите флаг как один после первого вызова диалога прогресса.

 @Override
    public void writeTo(BufferedSink sink) throws IOException {

        Source source = null;
        try {
            source = Okio.source(mFile);
            total = 0;
            long read;

            Handler handler = new Handler(Looper.getMainLooper());

            while ((read = source.read(sink.buffer(), DEFAULT_BUFFER_SIZE)) != -1) {

                total += read;
                sink.flush();

                // flag for avoiding first progress bar .
                if (flag != 0) {
                    handler.post(() -> mListener.onProgressUpdate((int) (100 * total / mFile.length())));

                }
            }

            flag = 1;

        } finally {
            Util.closeQuietly(source);
        }
    }

насколько я могу видеть в этой сообщение, никаких обновлений относительно ответа о ходе загрузки изображения не было сделано, и вам все еще нужно override в writeTo метод, как показано в этой так ответьте, сделав ProgressListener интерфейс и использование подкласса TypedFile до override the writeTo метод.

так,не любой встроенный способ показать прогресс при использовании библиотеки retrofit 2.

Я пытался использовать выше код, но я обнаружил, что пользовательский интерфейс застревает, поэтому я попытался код это работает для меня, или, возможно, попробуйте использовать код

удалите перехватчик ведения журнала Http из httpbuilder. Иначе он вызовет writeTo() два раза. Или измените уровень ведения журнала с BODY.

для дооснащения 2.0.0-beta4 загрузка файлов не реализована корректно

парсер исходного кода на данный момент

@Documented
@Target(PARAMETER)
@Retention(RUNTIME)
public @interface Part {
    String value();
    String encoding() default "binary";
}

// #####

okhttp3.Headers headers = okhttp3.Headers.of(
"Content-Disposition", "form-data; name=\"" + part.value() + "\"",
        "Content-Transfer-Encoding", part.encoding());

и нет никакого способа, чтобы добавить имя файла аннотации

поэтому мы используем этот хак, чтобы вставить имя файла

на данный момент интерфейс должен быть

@Multipart
@POST("some/method")
Observable<Response<SomeClass>> UpdateUserPhoto( // RxJava          
    @Part("token") RequestBody token,
    @Part("avatar\"; filename=\"avatar.png") RequestBody photo
);

и после сборки запроса мы берем

Content-Disposition: form-data; name="avatar"; filename="avatar.png"
Content-Transfer-Encoding: binary

тип носителя RequestBody для файла (изображения) должен быть

MediaType MEDIA_TYPE_IMAGE = MediaType.parse("image/*");

или что-то еще вариант