Можно ли показать индикатор выполнения При загрузке изображения через 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доoverridethewriteToметод.так,не любой встроенный способ показать прогресс при использовании библиотеки 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/*");или что-то еще вариант