Можно ли показать индикатор выполнения При загрузке изображения через Retrofit 2
Я сейчас использую Retrofit 2
и я хочу загрузить некоторые фотографии на моем сервере.
Я знаю, что более старая версия использует TypedFile
класс для загрузки. И если мы хотим использовать индикатор выполнения с ним мы должны переопределить writeTo
метод TypedFile
класса.
можно ли показать прогресс при использовании retrofit 2
библиотеки?
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
thewriteTo
метод.так,не любой встроенный способ показать прогресс при использовании библиотеки 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/*");
или что-то еще вариант