Fading Coder

One Final Commit for the Last Sprint

Home > Tools > Content

Implementing REST Clients and Multipart Uploads with Retrofit on Android

Tools May 7 4

Gradle Dependencies

Add the core Retrofit library, a JSON converter, and an OkHttp logging interceptor to the module-level build.gradle file.

dependencies {
    implementation "com.squareup.retrofit2:retrofit:2.9.0"
    implementation "com.squareup.retrofit2:converter-gson:2.9.0"
    implementation "com.squareup.okhttp3:logging-interceptor:4.10.0"
}

Manifest and Security Configuration

Declare internet access and storage permissions. Configure cleartext traffic allowance and register a FileProvider for secure file URI sharing.

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

    <application
        android:networkSecurityConfig="@xml/security_policy"
        ... >
        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="${applicationId}.file_provider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>
    </application>
</manifest>

Create res/xml/security_policy.xml to permit HTTP traffic during development:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="true" />
</network-security-config>

Define res/xml/file_paths.xml for external storage access:

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <external-path name="shared_media" path="." />
</paths>

API Contract Definition

Define endpoints using Retrofit annotations. Form-encoded POST requests and multipart file uploads are declared as interface methods.

public interface RemoteApiContract {
    @POST("v1/catalog")
    @FormUrlEncoded
    Call<ApiResponse> fetchCatalog(
            @Field("page_index") int pageIndex,
            @Field("page_size") int pageSize
    );

    @POST("v1/media/publish")
    @Multipart
    Call<UploadResponse> submitPhoto(
            @Part MultipartBody.Part filePart
    );
}

Runtime Permission Handler

Android 6.0+ requires dynamic permission requests. A utility class simplifies checking and requesting storage access.

public class PermissionManager {
    public static final String[] STORAGE_ACCESS = {
            Manifest.permission.READ_EXTERNAL_STORAGE,
            Manifest.permission.WRITE_EXTERNAL_STORAGE
    };
    public static final int REQUEST_STORAGE = 202;

    public static boolean ensureGranted(Activity host, String[] permissions, int requestCode) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
            return true;
        }
        for (String perm : permissions) {
            if (ContextCompat.checkSelfPermission(host, perm) != PackageManager.PERMISSION_GRANTED) {
                ActivityCompat.requestPermissions(host, permissions, requestCode);
                return false;
            }
        }
        return true;
    }

    public static boolean allApproved(int[] results) {
        if (results == null || results.length == 0) return false;
        for (int res : results) {
            if (res != PackageManager.PERMISSION_GRANTED) return false;
        }
        return true;
    }
}

Layout Resource

A simple interface containing a single trigger button.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/btn_trigger_upload"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="Select & Upload Image"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintWidth_percent="0.8" />
</androidx.constraintlayout.widget.ConstraintLayout>

Client Initialization and Request Execution

Configure the HTTP client with a logging interceptor, attach the Gson converter, and instantiate the API interface. Synchronous calls must run on a background thread, while asynchronous calls utilize callbacks on the main thread.

public class NetworkDemoActivity extends AppCompatActivity {
    private RemoteApiContract remoteApi;
    private static final String BASE_URL = "http://192.168.1.100:8080/";

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

        HttpLoggingInterceptor logger = new HttpLoggingInterceptor(msg -> Log.d("HttpTrace", msg));
        logger.setLevel(HttpLoggingInterceptor.Level.BODY);

        OkHttpClient httpEngine = new OkHttpClient.Builder()
                .addInterceptor(logger)
                .connectTimeout(15, TimeUnit.SECONDS)
                .build();

        Retrofit adapter = new Retrofit.Builder()
                .baseUrl(BASE_URL)
                .client(httpEngine)
                .addConverterFactory(GsonConverterFactory.create())
                .build();

        remoteApi = adapter.create(RemoteApiContract.class);

        findViewById(R.id.btn_trigger_upload).setOnClickListener(v -> initiateMediaPicker());
        executeBackgroundSync();
    }

    private void executeBackgroundSync() {
        new Thread(() -> {
            try {
                Response<ApiResponse> syncResult = remoteApi.fetchCatalog(0, 10).execute();
                if (syncResult.isSuccessful() && syncResult.body() != null) {
                    Log.i("SyncTask", "Received: " + syncResult.body().toString());
                }
            } catch (IOException e) {
                Log.e("SyncTask", "Network failure", e);
            }
        }).start();
    }

    private void executeAsyncCall() {
        remoteApi.fetchCatalog(0, 10).enqueue(new Callback<ApiResponse>() {
            @Override
            public void onResponse(Call<ApiResponse> call, Response<ApiResponse> response) {
                if (response.isSuccessful()) {
                    Log.d("AsyncTask", "Data loaded successfully");
                }
            }

            @Override
            public void onFailure(Call<ApiResponse> call, Throwable t) {
                Log.w("AsyncTask", "Request cancelled or failed", t);
            }
        });
    }
}

Media Selection and Multipart Upload

Launch the system gallery, resolve the selected URI to a file path, construct a multipart request body, and transmit it to the server.

    private void initiateMediaPicker() {
        if (PermissionManager.ensureGranted(this, PermissionManager.STORAGE_ACCESS, PermissionManager.REQUEST_STORAGE)) {
            Intent galleryIntent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
            startActivityForResult(galleryIntent, 1001);
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == 1001 && resultCode == RESULT_OK && data != null) {
            Uri imageUri = data.getData();
            String realPath = resolveFilePath(imageUri);
            if (realPath != null) {
                transmitImage(realPath);
            }
        }
    }

    private String resolveFilePath(Uri contentUri) {
        String[] projection = { MediaStore.Images.Media.DATA };
        try (Cursor cursor = getContentResolver().query(contentUri, projection, null, null, null)) {
            if (cursor != null && cursor.moveToFirst()) {
                int index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
                return cursor.getString(index);
            }
        }
        return null;
    }

    private void transmitImage(String absolutePath) {
        File targetFile = new File(absolutePath);
        RequestBody fileBody = RequestBody.create(MediaType.parse("image/jpeg"), targetFile);
        MultipartBody.Part multipartPart = MultipartBody.Part.createFormData("attachment", targetFile.getName(), fileBody);

        remoteApi.submitPhoto(multipartPart).enqueue(new Callback<UploadResponse>() {
            @Override
            public void onResponse(Call<UploadResponse> call, Response<UploadResponse> response) {
                if (response.isSuccessful() && response.body() != null) {
                    Log.i("Upload", "Server response: " + response.body().getStatus());
                }
            }

            @Override
            public void onFailure(Call<UploadResponse> call, Throwable t) {
                Log.e("Upload", "Transmission error", t);
            }
        });
    }

Data Models

Plain Java objects representing API payloads. Gson automatically maps JSON keys to class fields.

public class ApiResponse {
    public int total_count;
    public List<CatalogItem> items;
    public String next_cursor;
}

public class UploadResponse {
    public String status;
    public String file_url;
    public long uploaded_at;
}

public class CatalogItem {
    public String item_id;
    public String title;
    public double price;
    public boolean in_stock;
    public String thumbnail_url;
}

Related Articles

Efficient Usage of HTTP Client in IntelliJ IDEA

IntelliJ IDEA incorporates a versatile HTTP client tool, enabling developres to interact with RESTful services and APIs effectively with in the editor. This functionality streamlines workflows, replac...

Installing CocoaPods on macOS Catalina (10.15) Using a User-Managed Ruby

System Ruby on macOS 10.15 frequently fails to build native gems required by CocoaPods (for example, ffi), leading to errors like: ERROR: Failed to build gem native extension checking for ffi.h... no...

Resolve PhpStorm "Interpreter is not specified or invalid" on WAMP (Windows)

Symptom PhpStorm displays: "Interpreter is not specified or invalid. Press ‘Fix’ to edit your project configuration." This occurs when the IDE cannot locate a valid PHP CLI executable or when the debu...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.