A good approach to do multipart file upload in Android

Learn a good approach to do multipart file upload in android with practical examples, diagrams, and best practices. Covers java, android, multithreading development techniques with visual explanati...

Robust Multipart File Uploads in Android: A Comprehensive Guide

Hero image for A good approach to do multipart file upload in Android

Learn how to implement reliable multipart file uploads in Android using HttpURLConnection, handling large files, progress updates, and error scenarios.

Uploading files from an Android device to a server is a common requirement for many applications. While various libraries simplify this task, understanding the underlying mechanics of multipart/form-data requests is crucial for robust implementation, especially when dealing with large files, network fluctuations, and progress tracking. This article will guide you through building a reliable multipart file upload mechanism using standard Java APIs, focusing on HttpURLConnection and best practices for Android development.

Understanding Multipart/form-data

Multipart/form-data is an HTTP content type that allows clients to send multiple types of data (e.g., text fields, binary files) in a single request. Each part of the data is separated by a unique boundary string. This format is essential for file uploads as it allows the server to distinguish between different form fields and the actual file content.

flowchart TD
    A[Android App] --> B{HTTP POST Request}
    B --"Content-Type: multipart/form-data"--> C[Server]
    C --"Parses Boundary"--> D[Extracts Form Fields]
    C --"Extracts File Data"--> E[Saves File]
    D & E --> F[Server Response]
    F --> A

Multipart File Upload Process Flow

Implementing the Upload Logic

The core of the upload process involves constructing the HTTP request with the correct headers and body. We'll use HttpURLConnection for this, as it's a lightweight and efficient option built into the Android SDK. The process involves setting up the connection, writing the multipart data (text fields and file content) to the output stream, and then reading the server's response.

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Map;

public class MultipartUploader {

    private static final String LINE_FEED = "\r\n";
    private HttpURLConnection httpConn;
    private String charset;
    private DataOutputStream outputStream;
    private String boundary;

    public MultipartUploader(String requestURL, String charset) throws Exception {
        this.charset = charset;
        boundary = "----" + System.currentTimeMillis();

        URL url = new URL(requestURL);
        httpConn = (HttpURLConnection) url.openConnection();
        httpConn.setUseCaches(false);
        httpConn.setDoOutput(true); // indicates POST method
        httpConn.setDoInput(true);
        httpConn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
        httpConn.setRequestProperty("User-Agent", "AndroidMultipartUploader");
        outputStream = new DataOutputStream(httpConn.getOutputStream());
    }

    public void addFormField(String name, String value) throws Exception {
        outputStream.writeBytes("--" + boundary + LINE_FEED);
        outputStream.writeBytes("Content-Disposition: form-data; name=\"" + name + "\"" + LINE_FEED);
        outputStream.writeBytes("Content-Type: text/plain; charset=" + charset + LINE_FEED);
        outputStream.writeBytes(LINE_FEED);
        outputStream.writeBytes(value + LINE_FEED);
        outputStream.flush();
    }

    public void addFilePart(String fieldName, File uploadFile) throws Exception {
        String fileName = uploadFile.getName();
        outputStream.writeBytes("--" + boundary + LINE_FEED);
        outputStream.writeBytes("Content-Disposition: form-data; name=\"" + fieldName + "\"; filename=\"" + fileName + "\"" + LINE_FEED);
        outputStream.writeBytes("Content-Type: " + HttpURLConnection.guessContentTypeFromName(fileName) + LINE_FEED);
        outputStream.writeBytes("Content-Transfer-Encoding: binary" + LINE_FEED);
        outputStream.writeBytes(LINE_FEED);
        outputStream.flush();

        FileInputStream inputStream = new FileInputStream(uploadFile);
        byte[] buffer = new byte[4096];
        int bytesRead = -1;
        while ((bytesRead = inputStream.read(buffer)) != -1) {
            outputStream.write(buffer, 0, bytesRead);
        }
        outputStream.flush();
        inputStream.close();
        outputStream.writeBytes(LINE_FEED);
    }

    public String finish() throws Exception {
        outputStream.writeBytes(LINE_FEED);
        outputStream.writeBytes("--" + boundary + "--" + LINE_FEED);
        outputStream.flush();
        outputStream.close();

        int status = httpConn.getResponseCode();
        if (status == HttpURLConnection.HTTP_OK) {
            BufferedReader reader = new BufferedReader(new InputStreamReader(httpConn.getInputStream()));
            StringBuilder response = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                response.append(line);
            }
            reader.close();
            httpConn.disconnect();
            return response.toString();
        } else {
            throw new Exception("Server returned non-OK status: " + status);
        }
    }
}

Basic MultipartUploader class using HttpURLConnection

Integrating with Android and Asynchronous Execution

Network operations, including file uploads, must be performed on a background thread to prevent blocking the UI and causing Application Not Responding (ANR) errors. Android provides several mechanisms for this, such as AsyncTask (deprecated but still seen), ExecutorService, or Kotlin Coroutines. For simplicity, we'll illustrate with a basic Thread and Handler for UI updates.

import android.os.Handler;
import android.os.Looper;
import android.widget.Toast;
import java.io.File;
import java.util.HashMap;
import java.util.Map;

public class UploadManager {

    private static final String UPLOAD_URL = "http://your-server.com/upload"; // Replace with your server URL
    private Handler mainHandler = new Handler(Looper.getMainLooper());

    public interface UploadCallback {
        void onUploadSuccess(String response);
        void onUploadFailure(String error);
        void onUploadProgress(int progress);
    }

    public void uploadFile(final File fileToUpload, final Map<String, String> formFields, final UploadCallback callback) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    MultipartUploader uploader = new MultipartUploader(UPLOAD_URL, "UTF-8");

                    for (Map.Entry<String, String> entry : formFields.entrySet()) {
                        uploader.addFormField(entry.getKey(), entry.getValue());
                    }
                    uploader.addFilePart("file", fileToUpload);

                    final String response = uploader.finish();
                    mainHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            callback.onUploadSuccess(response);
                        }
                    });

                } catch (final Exception e) {
                    mainHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            callback.onUploadFailure(e.getMessage());
                        }
                    });
                }
            }
        }).start();
    }

    // Example usage in an Activity/Fragment
    public void initiateUpload(File selectedFile) {
        Map<String, String> fields = new HashMap<>();
        fields.put("description", "My awesome photo");
        fields.put("userId", "123");

        uploadFile(selectedFile, fields, new UploadCallback() {
            @Override
            public void onUploadSuccess(String response) {
                Toast.makeText(null, "Upload successful: " + response, Toast.LENGTH_LONG).show();
            }

            @Override
            public void onUploadFailure(String error) {
                Toast.makeText(null, "Upload failed: " + error, Toast.LENGTH_LONG).show();
            }

            @Override
            public void onUploadProgress(int progress) {
                // Update UI progress bar here
            }
        });
    }
}

Android integration with background thread and UI updates

Error Handling and Best Practices

Robust error handling is critical for any network operation. This includes catching IOException for network issues, handling non-200 HTTP status codes from the server, and providing meaningful feedback to the user. Additionally, consider these best practices:

  • Retry Mechanism: Implement a retry logic with exponential backoff for transient network errors.
  • Cancellation: Provide a way for users to cancel ongoing uploads.
  • Background Services: For long-running uploads, especially when the app might be in the background, consider using a Foreground Service to ensure the upload completes and to display a persistent notification.
  • Security: Always use HTTPS for all network communications to protect sensitive data during transfer.
  • File Paths: Be careful with file paths. On Android, use Context.getFilesDir() or Context.getExternalFilesDir() for app-specific storage, or the Storage Access Framework for user-selected files.
sequenceDiagram
    participant App as Android App
    participant Uploader as MultipartUploader
    participant Server as Remote Server

    App->>Uploader: init(URL, charset)
    loop Add Form Fields
        App->>Uploader: addFormField(name, value)
    end
    App->>Uploader: addFilePart(fieldName, file)
    Uploader->>Server: HTTP POST Request (multipart/form-data)
    Server-->>Uploader: HTTP Response (Status Code)
    alt Success (200 OK)
        Uploader->>Server: Read Response Body
        Server-->>Uploader: Response Data
        Uploader->>App: onUploadSuccess(response)
    else Failure (Non-200 Status or Exception)
        Uploader->>App: onUploadFailure(error)
    end

Sequence Diagram of Multipart File Upload