Bekerja dengan Permintaan Multipart POST dengan Volley dan tanpa HttpEntity

106

Ini sebenarnya bukan pertanyaan, namun, saya ingin membagikan beberapa kode kerja saya di sini untuk referensi Anda saat Anda membutuhkannya.

Seperti yang kita ketahui, itu HttpEntitysudah usang dari API22 dan benar-benar dihapus sejak API23. Saat ini, kami tidak dapat mengakses Referensi HttpEntity di Pengembang Android lagi (404). Jadi, berikut ini adalah kode contoh kerja saya untuk Permintaan Multipart POST dengan Volley dan tanpa HttpEntity . Ini bekerja, diuji dengan Asp.Net Web API. Tentu saja, kode tersebut mungkin hanya contoh dasar yang memposting dua file drawable yang ada, juga bukan solusi terbaik untuk semua kasus, dan bukan penyetelan yang baik.

MultipartActivity.java:

package com.example.multipartvolley;

import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.support.v4.content.ContextCompat;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;

import com.android.volley.NetworkResponse;
import com.android.volley.Response;
import com.android.volley.VolleyError;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;


public class MultipartActivity extends Activity {

    private final Context context = this;
    private final String twoHyphens = "--";
    private final String lineEnd = "\r\n";
    private final String boundary = "apiclient-" + System.currentTimeMillis();
    private final String mimeType = "multipart/form-data;boundary=" + boundary;
    private byte[] multipartBody;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_multipart);

        byte[] fileData1 = getFileDataFromDrawable(context, R.drawable.ic_action_android);
        byte[] fileData2 = getFileDataFromDrawable(context, R.drawable.ic_action_book);

        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        DataOutputStream dos = new DataOutputStream(bos);
        try {
            // the first file
            buildPart(dos, fileData1, "ic_action_android.png");
            // the second file
            buildPart(dos, fileData2, "ic_action_book.png");
            // send multipart form data necesssary after file data
            dos.writeBytes(twoHyphens + boundary + twoHyphens + lineEnd);
            // pass to multipart body
            multipartBody = bos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }

        String url = "http://192.168.1.100/api/postfile";
        MultipartRequest multipartRequest = new MultipartRequest(url, null, mimeType, multipartBody, new Response.Listener<NetworkResponse>() {
            @Override
            public void onResponse(NetworkResponse response) {
                Toast.makeText(context, "Upload successfully!", Toast.LENGTH_SHORT).show();
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                Toast.makeText(context, "Upload failed!\r\n" + error.toString(), Toast.LENGTH_SHORT).show();
            }
        });

        VolleySingleton.getInstance(context).addToRequestQueue(multipartRequest);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_multipart, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    private void buildPart(DataOutputStream dataOutputStream, byte[] fileData, String fileName) throws IOException {
        dataOutputStream.writeBytes(twoHyphens + boundary + lineEnd);
        dataOutputStream.writeBytes("Content-Disposition: form-data; name=\"uploaded_file\"; filename=\""
                + fileName + "\"" + lineEnd);
        dataOutputStream.writeBytes(lineEnd);

        ByteArrayInputStream fileInputStream = new ByteArrayInputStream(fileData);
        int bytesAvailable = fileInputStream.available();

        int maxBufferSize = 1024 * 1024;
        int bufferSize = Math.min(bytesAvailable, maxBufferSize);
        byte[] buffer = new byte[bufferSize];

        // read file and write it into form...
        int bytesRead = fileInputStream.read(buffer, 0, bufferSize);

        while (bytesRead > 0) {
            dataOutputStream.write(buffer, 0, bufferSize);
            bytesAvailable = fileInputStream.available();
            bufferSize = Math.min(bytesAvailable, maxBufferSize);
            bytesRead = fileInputStream.read(buffer, 0, bufferSize);
        }

        dataOutputStream.writeBytes(lineEnd);
    }

    private byte[] getFileDataFromDrawable(Context context, int id) {
        Drawable drawable = ContextCompat.getDrawable(context, id);
        Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        bitmap.compress(Bitmap.CompressFormat.PNG, 0, byteArrayOutputStream);
        return byteArrayOutputStream.toByteArray();
    }
}

MultipartRequest.java:

package com.example.multipartvolley;

import com.android.volley.AuthFailureError;
import com.android.volley.NetworkResponse;
import com.android.volley.ParseError;
import com.android.volley.Request;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.HttpHeaderParser;

import java.util.Map;

class MultipartRequest extends Request<NetworkResponse> {
    private final Response.Listener<NetworkResponse> mListener;
    private final Response.ErrorListener mErrorListener;
    private final Map<String, String> mHeaders;
    private final String mMimeType;
    private final byte[] mMultipartBody;

    public MultipartRequest(String url, Map<String, String> headers, String mimeType, byte[] multipartBody, Response.Listener<NetworkResponse> listener, Response.ErrorListener errorListener) {
        super(Method.POST, url, errorListener);
        this.mListener = listener;
        this.mErrorListener = errorListener;
        this.mHeaders = headers;
        this.mMimeType = mimeType;
        this.mMultipartBody = multipartBody;
    }

    @Override
    public Map<String, String> getHeaders() throws AuthFailureError {
        return (mHeaders != null) ? mHeaders : super.getHeaders();
    }

    @Override
    public String getBodyContentType() {
        return mMimeType;
    }

    @Override
    public byte[] getBody() throws AuthFailureError {
        return mMultipartBody;
    }

    @Override
    protected Response<NetworkResponse> parseNetworkResponse(NetworkResponse response) {
        try {
            return Response.success(
                    response,
                    HttpHeaderParser.parseCacheHeaders(response));
        } catch (Exception e) {
            return Response.error(new ParseError(e));
        }
    }

    @Override
    protected void deliverResponse(NetworkResponse response) {
        mListener.onResponse(response);
    }

    @Override
    public void deliverError(VolleyError error) {
        mErrorListener.onErrorResponse(error);
    }
}

MEMPERBARUI:

Untuk bagian teks, lihat jawaban @ Oscar di bawah ini.

BNK
sumber
1
Saya baru saja menyalin komentar @Kevin pada pertanyaan berikut : Beberapa server SANGAT pilih-pilih. Jika Anda mengalami masalah, tambahkan SPASI antara ";" dan "filename =" saat membuat Content-Disposition dan "multipart / form-data; boundary =" + boundary; :)
BNK
1
jika Anda ingin menambahkan mimtype: dataOutputStream.writeBytes ("Content-Type: image / jpeg" + lineEnd);
Maor Hadad
1
@MaorHadad: terima kasih atas komentar Anda :)
BNK
1
Terima kasih atas solusi hebat ini. setelah memperbarui ke appcompat 23, masalah ini menyakitkan di **
Maor Hadad
1
BNK yang terhormat, apakah ini berfungsi untuk mengupload video?
mok

Jawaban:

63

Saya menulis ulang kode Anda @RacZo dan @BNK lebih modular dan mudah digunakan seperti

VolleyMultipartRequest multipartRequest = new VolleyMultipartRequest(Request.Method.POST, url, new Response.Listener<NetworkResponse>() {
    @Override
    public void onResponse(NetworkResponse response) {
        String resultResponse = new String(response.data);
        // parse success output
    }
}, new Response.ErrorListener() {
    @Override
    public void onErrorResponse(VolleyError error) {                
        error.printStackTrace();
    }
}) {
    @Override
    protected Map<String, String> getParams() {
        Map<String, String> params = new HashMap<>();
        params.put("api_token", "gh659gjhvdyudo973823tt9gvjf7i6ric75r76");
        params.put("name", "Angga");
        params.put("location", "Indonesia");
        params.put("about", "UI/UX Designer");
        params.put("contact", "[email protected]");
        return params;
    }

    @Override
    protected Map<String, DataPart> getByteData() {
        Map<String, DataPart> params = new HashMap<>();
        // file name could found file base or direct access from real path
        // for now just get bitmap data from ImageView
        params.put("avatar", new DataPart("file_avatar.jpg", AppHelper.getFileDataFromDrawable(getBaseContext(), mAvatarImage.getDrawable()), "image/jpeg"));
        params.put("cover", new DataPart("file_cover.jpg", AppHelper.getFileDataFromDrawable(getBaseContext(), mCoverImage.getDrawable()), "image/jpeg"));

        return params;
    }
};

VolleySingleton.getInstance(getBaseContext()).addToRequestQueue(multipartRequest);

Periksa kode lengkap VolleyMultipartRequestdi intisari saya .

Angga Ari Wijaya
sumber
Saya tidak ingin mengubahnya menjadi byte atau string. untuk kasus saya, sisi server mengharapkan file dan banyak teks (key-value pair, multipart form data), bukan byte atau string. Apakah itu mungkin?
Milon
ya itu, Anda dapat memperlakukan data Anda di sisi server seperti data formulir multi bagian dalam formulir web`` sebenarnya kode tersebut adalah memodifikasi permintaan header http agar sesuai dan formulir web serupa sehingga itu adalah solusi yang Anda cari ...
Angga Ari Wijaya
@AhamadullahSaikat kamu mendapat sesuatu karena hal yang sama dalam proyek saya mengirim banyak permintaan dengan nama yang sama
Ricky Patel
20

Hanya ingin menambah jawabannya. Saya mencoba mencari cara untuk menambahkan bidang teks ke tubuh dan membuat fungsi berikut untuk melakukannya:

private void buildTextPart(DataOutputStream dataOutputStream, String parameterName, String parameterValue) throws IOException {
    dataOutputStream.writeBytes(twoHyphens + boundary + lineEnd);
    dataOutputStream.writeBytes("Content-Disposition: form-data; name=\"" + parameterName + "\"" + lineEnd);
    dataOutputStream.writeBytes("Content-Type: text/plain; charset=UTF-8" + lineEnd);
    dataOutputStream.writeBytes(lineEnd);
    dataOutputStream.writeBytes(parameterValue + lineEnd);
}

Ini bekerja dengan cukup baik.

Oscar Salguero
sumber
Saya telah menyematkan solusi @BNK ke dalam aplikasi saya. Saya memilih foto di perpustakaan ponsel saya dan mengirimkannya melalui data formulir multi-bagian, tetapi perlu beberapa detik (~ 10-15) sebelum dikirim ke server. Apakah ada cara untuk mengurangi biaya tambahan ini? atau rekomendasi lainnya?
casillas
1
Setelah penghentian HttpEntity, unggahan multi-bagian dengan voli menjadi sangat rumit, ditambah lagi dengan menerapkan permintaan PATCH yang memusingkan, jadi saya akhirnya menjauh dari tendangan voli dan menerapkan RetroFit ( square.github.io/retrofit ) di semua aplikasi saya. Saya akan merekomendasikan Anda untuk melakukan hal yang sama karena RetroFit memberi Anda kompatibilitas mundur yang lebih baik dan bukti masa depan Aplikasi Anda.
Oscar Salguero
Saya setuju dengan @RacZo, namun saya lebih suka OkHttp :), saya masih menggunakan Volley untuk permintaan jaringan lainnya. Saya telah menyesuaikan tendangan voli Google menghapus Apache lib, diposting ke github.com/ngocchung/volleynoapache , namun hanya diuji untuk Get, Post dan Multipart.
BNK
Saya perlu menggunakan permintaan PATCH menggunakan perpustakaan Volley. Bagaimana saya bisa mencapai ini.
Jagadesh Seeram
8

Bagi mereka yang kesulitan mengirim parameter utf-8 dan masih belum berhasil, masalah yang saya alami ada di dataOutputStream, dan ubah kode @RacZo menjadi kode di bawah ini:

private void buildTextPart(DataOutputStream dataOutputStream, String parameterName, String parameterValue) throws IOException {
        dataOutputStream.writeBytes(twoHyphens + boundary + lineEnd);
        dataOutputStream.writeBytes("Content-Disposition: form-data; name=\"");
        dataOutputStream.write(parameterName.getBytes("UTF-8"));
        dataOutputStream.writeBytes(lineEnd);
        dataOutputStream.writeBytes("Content-Type: text/plain; charset=UTF-8" + lineEnd);
        dataOutputStream.writeBytes(lineEnd);
        dataOutputStream.write(parameterValue.getBytes("UTF-8"));
        dataOutputStream.writeBytes(lineEnd);
    } 
Sepehr Nozaryian
sumber
2

Berikut adalah versi Kotlin dari kelas yang mengizinkan permintaan multi bagian dengan Volley 1.1.1.

Ini sebagian besar didasarkan pada solusi @ BNK tetapi sedikit disederhanakan. Saya tidak melihat adanya masalah kinerja tertentu. Saya mengunggah foto 5Mb dalam waktu sekitar 3 detik.

class MultipartWebservice(context: Context) {

    private var queue: RequestQueue? = null

    private val boundary = "apiclient-" + System.currentTimeMillis()
    private val mimeType = "multipart/form-data;boundary=$boundary"

    init {
        queue = Volley.newRequestQueue(context)
    }

    fun sendMultipartRequest(
        method: Int,
        url: String,
        fileData: ByteArray,
        fileName: String,
        listener: Response.Listener<NetworkResponse>,
        errorListener: Response.ErrorListener
    ) {

        // Create multi part byte array
        val bos = ByteArrayOutputStream()
        val dos = DataOutputStream(bos)
        buildMultipartContent(dos, fileData, fileName)
        val multipartBody = bos.toByteArray()

        // Request header, if needed
        val headers = HashMap<String, String>()
        headers["API-TOKEN"] = "458e126682d577c97d225bbd73a75b5989f65e977b6d8d4b2267537019ad9d20"

        val request = MultipartRequest(
            method,
            url,
            errorListener,
            listener,
            headers,
            mimeType,
            multipartBody
        )

        queue?.add(request)

    }

    @Throws(IOException::class)
    private fun buildMultipartContent(dos: DataOutputStream, fileData: ByteArray, fileName: String) {

        val twoHyphens = "--"
        val lineEnd = "\r\n"

        dos.writeBytes(twoHyphens + boundary + lineEnd)
        dos.writeBytes("Content-Disposition: form-data; name=\"file\"; filename=\"$fileName\"$lineEnd")
        dos.writeBytes(lineEnd)
        dos.write(fileData)
        dos.writeBytes(lineEnd)
        dos.writeBytes(twoHyphens + boundary + twoHyphens + lineEnd)
    }

    class MultipartRequest(
        method: Int,
        url: String,
        errorListener: Response.ErrorListener?,
        private var listener: Response.Listener<NetworkResponse>,
        private var headers: MutableMap<String, String>,
        private var mimeType: String,
        private var multipartBody: ByteArray
    ) : Request<NetworkResponse>(method, url, errorListener) {

        override fun getHeaders(): MutableMap<String, String> {
            return if (headers.isEmpty()) super.getHeaders() else headers
        }

        override fun getBodyContentType(): String {
            return mimeType
        }

        override fun getBody(): ByteArray {
            return multipartBody
        }

        override fun parseNetworkResponse(response: NetworkResponse?): Response<NetworkResponse> {
            return try {
                Response.success(response, HttpHeaderParser.parseCacheHeaders(response))
            } catch (e: Exception) {
                Response.error(ParseError(e))
            }
        }

        override fun deliverResponse(response: NetworkResponse?) {
            listener.onResponse(response)
        }
    }
}
Ika
sumber
Hai. Ada informasi lebih lanjut tentang bagaimana kita dapat menggunakan kelas di atas? Misalnya bagaimana kita bisa mengunggah gambar .jpg dengannya?
Thanasis
0

Saya menemukan pembungkus perpustakaan voli asli yang lebih mudah diintegrasikan untuk permintaan multi-bagian. Ini juga mendukung pengunggahan data multi-bagian bersama dengan parameter permintaan lainnya. Oleh karena itu saya membagikan kode saya untuk pengembang masa depan yang mungkin mengalami masalah yang saya alami (yaitu mengunggah data multi-bagian menggunakan voli bersama dengan beberapa parameter lainnya).

Tambahkan pustaka berikut di build.gradlefile.

dependencies {
    compile 'dev.dworks.libs:volleyplus:+'
}

Harap dicatat bahwa, saya menghapus perpustakaan voli asli dari perpustakaan sayabuild.gradle dan menggunakan perpustakaan di atas sebagai gantinya yang dapat menangani permintaan multi-bagian dan normal yang memiliki teknik integrasi serupa.

Kemudian saya hanya perlu menulis kelas berikut yang menangani operasi permintaan POST.

public class POSTMediasTask {
    public void uploadMedia(final Context context, String filePath) {

        String url = getUrlForPOSTMedia(); // This is a dummy function which returns the POST url for you
        SimpleMultiPartRequest multiPartRequestWithParams = new SimpleMultiPartRequest(Request.Method.POST, url,
                new Response.Listener<String>() {
                    @Override
                    public void onResponse(String response) {
                        Log.d("Response", response);
                        // TODO: Do something on success
                    }
                }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                // TODO: Handle your error here
            }
        });

        // Add the file here
        multiPartRequestWithParams.addFile("file", filePath);

        // Add the params here
        multiPartRequestWithParams.addStringParam("param1", "SomeParamValue1");
        multiPartRequestWithParams.addStringParam("param2", "SomeParamValue2");

        RequestQueue queue = Volley.newRequestQueue(context);
        queue.add(multiPartRequestWithParams);
    }
}

Sekarang jalankan tugas seperti berikut.

new POSTMediasTask().uploadMedia(context, mediaPath);

Anda dapat mengupload file satu per satu menggunakan perpustakaan ini. Namun, saya dapat mengunggah banyak file, hanya dengan memulai banyak tugas.

Semoga membantu!

Reaz Murshed
sumber