web-dev-qa-db-fra.com

Est-il possible d'afficher la barre de progression lors du téléchargement d'une image via Retrofit 2?

J'utilise actuellement Retrofit 2 et je veux télécharger des photos sur mon serveur. Je sais que cette version plus ancienne utilise la classe TypedFile pour le téléchargement. Et si nous voulons utiliser la barre de progression avec elle, nous devons surcharger la méthode writeTo dans la classe TypedFile.

Est-il possible de montrer les progrès en utilisant retrofit 2 bibliothèque?

69
Yuriy Kolbasinskiy

Tout d’abord, vous devriez utiliser Retrofit 2 version égale ou supérieure à 2.0 beta2. Deuxièmement, créer une nouvelle classe étend RequestBody:

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

  private static final int DEFAULT_BUFFER_SIZE = 2048;

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

Prenez note, j'ai ajouté le type de contenu afin qu'il puisse accueillir d'autres types à part image

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



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

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

Troisièmement, créer une interface

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

/ * JsonObject ci-dessus peut être remplacé par votre propre modèle, je veux juste que ce soit remarquable. * /

Maintenant, vous pouvez obtenir des progrès de votre téléchargement. Dans votre activity (ou 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 onResponse(Call<JsonObject> call,   Response<JsonObject> response) {
                if(response.isSuccessful()){
                    /* here we can equally assume the file has been downloaded successfully because for some reasons the onFinish method might not be called, I have tested it myself and it really not consistent, but the onProgressUpdate is efficient and we can use that to update out progress on the UIThread, and we can then set our progress to 100% right here because the file already downloaded finish. */
                  }
            }

            @Override
            public void onFailure(Call<JsonObject> call, Throwable t) {
                      /* we can also stop our progress update here, although I have not check if the onError is being called when the file could not be downloaded, so I will just use this as a backup plan just incase the onError did not get called. So I can stop the progress right here. */
            }
        });

      }

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


    }
149
Yuriy Kolbasinskiy

Yuriy Kolbasinskiy a été modifié pour utiliser rxjava et kotlin. Ajout d'une solution de contournement pour utiliser HttpLoggingInterceptor en même temps

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

Un exemple d'interface de téléchargement vidéo

public interface Api {

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

Un exemple de fonction pour poster une vidéo:

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();
            })
}
18
luca992

Voici comment gérer la progression du fichier téléchargé avec un simple POST plutôt que plusieurs parties. Pour une solution multipartie, consultez la solution de Yariy. De plus, cette solution utilise l'URI de contenu plutôt que des références directes à des fichiers.

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

Pour lancer le téléchargement:

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

Attention, si vous oubliez de remplacer la fonction contentLength (), vous risquez de recevoir quelques erreurs obscures:

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

Ou

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

Ou

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

Cela résulte du fait que RequestBody.writeTo () a été appelé plusieurs fois, car la valeur par défaut contentLength () est égale à -1.

Quoi qu'il en soit, cela a pris du temps à comprendre, espérons que cela aidera.

Liens utiles: https://github.com/square/retrofit/issues/1217

9
Justin Fiedler

@ luca992 Merci pour votre réponse. J'ai implémenté ceci dans Java et maintenant cela fonctionne bien.

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

    }
}
2
labhya sharma

J'ai essayé d'utiliser le code ci-dessus, mais j'ai trouvé que l'interface utilisateur était bloquée et j'ai donc essayé ce code cela fonctionne pour moi ou essayez d'utiliser ce code

1
Vivek Barai

Pour éviter deux problèmes en cours d'exécution. Nous pouvons définir l'indicateur à zéro initialement et l'indicateur à un après le premier appel du dialogue de progression.

 @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);
        }
    }
1
saneesh

Je met à jour progressbar onProgressUpdate. Ce code peut obtenir de meilleures performances.

@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();
    }
}
1
Troy Yuan-Ting Wu

Supprimez l'intercepteur de journalisation HTTP de httpbuilder. Sinon, il appellera writeTo() deux fois. Ou changez le niveau de journalisation de BODY.

1
Muhammad Haris

Vous pouvez utiliser FileUploader qui utilise Retrofit Library pour se connecter à l'API. Pour télécharger le fichier, le squelette de code est le suivant:

FileUploader fileUploader = new FileUploader();
fileUploader.uploadFiles("/", "file", filesToUpload, new FileUploader.FileUploaderCallback() {
    @Override
    public void onError() {
        // Hide progressbar
    }

    @Override
    public void onFinish(String[] responses) {
        // Hide progressbar

        for(int i=0; i< responses.length; i++){
            String str = responses[i];
            Log.e("RESPONSE "+i, responses[i]);
        }
    }

    @Override
    public void onProgressUpdate(int currentpercent, int totalpercent, int filenumber) {
        // Update Progressbar
        Log.e("Progress Status", currentpercent+" "+totalpercent+" "+filenumber);
    }
});

Les étapes complètes sont disponibles chez Medium:

Mise à niveau de plusieurs fichiers avec la progression dans Android

0

D'après ce que je peux voir dans this post, aucune mise à jour concernant la réponse à la progression du téléchargement des images n'a été effectuée et vous devez toujours utiliser la méthode override de writeTo, comme indiqué dans this SO répondez en créant une interface ProgressListener et en utilisant une sous-classe de TypedFile à override le writeTo méthode.

Ainsi, il n'y a pas de moyen intégré pour afficher les progrès réalisés lors de l'utilisation de la bibliothèque de mise à niveau 2.

0
Shubham A.