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

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

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

Прежде всего, вы должны использовать версию Retrofit 2 равную или выше 2.0 beta2. Во-вторых, создание нового classа расширяет 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 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 request = RetrofitClient.uploadImage(filepart); request.enqueue(new Callback{...}); } @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); } } в 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 request = RetrofitClient.uploadImage(filepart); request.enqueue(new Callback{...}); } @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); } } 

Вот как обрабатывать процесс загрузки файлов с помощью простого POST, а не Multipart. Для многопроходной проверки решения @ Яри. Кроме того, это решение использует Content URI вместо прямых ссылок на файлы.

RestClient

 @Headers({ "Accept: application/json", "Content-Type: application/octet-stream" }) @POST("api/v1/upload") Call 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 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

Изменен Юрий Колбасинский использовать 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 = PublishSubject.create() fun getProgressSubject(): Observable { 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 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(); }) } 

Я обновляю 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 floatPublishSubject = PublishSubject.create(); public Observable 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(); } } } 

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

  @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 как показано в этом ответе SO, путем создания интерфейса ProgressListener и использования подclassа TypedFile для override метод writeTo .

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

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

Удалите Http Logging interceptor из httpbuilder . writeTo() он вызовет 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()); 

и нет возможности добавлять имя файла по annotations

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

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

 @Multipart @POST("some/method") Observable> 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/*"); 

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

Давайте будем гением компьютера.