Search code examples
spring-bootkotlinandroid-jetpack-composeretrofitmultipartform-data

Uploading a list of images to a Java Spring boot server using Retrofit


So I am building a simple android app and I am trying to upload a list of images to a Spring boot server using retrofit. Retrofit is on the kotlin client side. So I first select the images from the phone's gallery and display them on the screen. They are displayed well. Second step is to upload them onto the server and this is where the problem is. First I convert the imageUrls into MultipartBody.Part objects here:

class FormDataContainer(context: Context): AppContainer {
    private val baseUrl = "http://192.168.246.6:8080/pManager/"
    private val retrofit = Retrofit.Builder()
        .baseUrl(baseUrl)
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    private val retrofitService: PMangerApiService by lazy {
        retrofit.create(PMangerApiService::class.java)
    }

    override val pMangerApiRepository: PMangerApiRepository by lazy {
        NetworkPManagerApiRepository(retrofitService)
    }
}

Then I invoke the API and pass the images together with some other data in this code:

viewModelScope.launch {
            try {
                Log.i("UPLOAD_PROPERTY", "Before making network request")

                val response = pManagerApiRepository.createProperty(
                    token = "Bearer ${userDetails.token}",
                    userId = userDetails.userId.toString(),
                    property = property,
                    images = imageParts
                )

                if (response.isSuccessful) {
                    val responseBody = response.body()
                    if (responseBody != null) {
                        Log.i("CREATE_PROPERTY_RESPONSE", "Status Code: ${responseBody.statusCode}, Message: ${responseBody.message}")
                    } else {
                        Log.i("CREATE_PROPERTY_RESPONSE", "Response body is null")
                    }
                } else {
                    Log.e("CREATE_PROPERTY_RESPONSE", "Unsuccessful response: ${response.code()}")
                }
            } catch (e: Exception) {
                Log.e("CREATE_PROPERTY_EXCEPTION", "Exception: ${e.message}")
            }
        }

for(part in imageParts) {
            Log.i("IMAGE_PARTS", part.toString())
        }

I have tried to uplod two images so the two parts look like this:

okhttp3.MultipartBody$Part@c8aade3 okhttp3.MultipartBody$Part@89963e0

This is the client api endpoint for sending the whole data:

@Multipart
    @POST("api/property/userId={id}/create")
    suspend fun uploadPropertyDetails(
        @Header("Authorization") token: String,
        @Path("id") userId: String,
        @Part("data") property: Property,
        @Part imageFiles: List<MultipartBody.Part>
    ): Response<PropertyUploadResponse>

And this is how I build retrofit:

class DefaultContainer(context: Context): AppContainer {
    private val json = Json { ignoreUnknownKeys = true }

    private val baseUrl = "http://192.168.246.6:8080/pManager/"
    private val retrofit = Retrofit.Builder()
        .baseUrl(baseUrl)
        .addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
        .build()

    private val retrofitService: PMangerApiService by lazy {
        retrofit.create(PMangerApiService::class.java)
    }
    override val pMangerApiRepository: PMangerApiRepository by lazy {
        NetworkPManagerApiRepository(retrofitService)
    }

}

The server side API endpoint is:

@PostMapping(value = "property/userId={userId}/create", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE})
    public ResponseEntity<Response> addProperty(@RequestPart(value = "data") @Valid PropertyDTO propertyDTO,@PathVariable int userId,
                                                @RequestParam (value = "imageFiles", required = false) MultipartFile[] imageFiles) throws IOException {
        System.out.println("RESPONSE");
        System.out.println();
        System.out.println(propertyDTO.toString());
        System.out.println();
        System.out.println("IMAGE FILES:");
        for(MultipartFile file : imageFiles) {
            System.out.println(file);
        }

        System.out.println();
        return buildResponse("property",propertyService.createProperty(propertyDTO, imageFiles, userId), "Created successfully", HttpStatus.CREATED);
    }

I get this error in the server console:

java.lang.NullPointerException: Cannot read the array length because "<local4>" is null

and imageFiles is null

property is posted successfully but the images fail at the same time, the error message is indicated above.

However when I call the same endpoint from postman, the images are uploaded successfull. imageFiles are:

org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@3eb7d8a7 org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@3a9c47fc

So basically I am trying to upload form-data to Spring boot server. The data is posted successful if done from postman. However the request fails if done from the mobile app. Actually it's only the photos that are not being sent because the property is posted successful.


Solution

  • It worked when I updated this line:

    imageParts.add(MultipartBody.Part.createFormData(name = "image", file.name, requestFile))
    

    New line:

    imageParts.add(MultipartBody.Part.createFormData(name = "imageFiles", file.name, requestFile))
    

    Basically, the name parameter of createFormData in kotlin client side should match the value parameter of @RequestPart annotation for the images part (imageFiles) in java spring boot api endpoint. This is the Spring boot end point:

    @PostMapping(value = "property/userId={userId}/create", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE})
        public ResponseEntity<Response> addProperty(@RequestPart(value = "data") @Valid PropertyDTO propertyDTO,@PathVariable int userId,
                                                    @RequestPart (value = "imageFiles", required = false) MultipartFile[] imageFiles) throws IOException {
    
            System.out.println();
            return buildResponse("property",propertyService.createProperty(propertyDTO, imageFiles, userId), "Created successfully", HttpStatus.CREATED);
        } 
    

    And this is the client side code for creating MultipartBody.Part objects:

    var imageParts = ArrayList<MultipartBody.Part>()
            _imagesUiState.value.images.forEach {file ->
                Log.i("IMAGE_PATH", file.path)
                val imageFile = File(file.path)
                val requestFile = imageFile.asRequestBody("image/*".toMediaTypeOrNull())
                val imagePart = MultipartBody.Part.createFormData("imageFiles", imageFile.name, requestFile)
                imageParts.add(imagePart)
            }