Development/Android

[Android, Retrofit] Multipart를 활용하여 서버에 이미지 파일 데이터 전송

SeungYong.Lee 2024. 1. 29. 12:27
반응형

Mutipart 무엇인가?

- Multipart는 HTTP에서 여러 종류의 데이터를 동시에 전송하기 위해 사용되는 방식입니다.

- 'Content-Type' 헤더에 'multipart/form-data'값을 가지며 여러 개의 part로 구성됩니다.

- 주로 파일 업로드나 폼 데이터 전송 등에 사용됩니다.

 

Multipart 활용을 위한 Api 호출 함수 구성

@Multipart
@PATCH("/api/v1/users/{id}")
suspend fun editUserImage(
    @HeaderMap headers: HashMap<String, String>,
    @Path("id") id: Int,
    @Part file: MultipartBody.Part
): Response<Unit>

@Multipart 어노테이션과 보낼 파일 데이터에 대해서는 @Part를 사용하여 구성해줍니다.

Multipart 활용 가능하도록 uri 변환

특정 로직으로 파일 데이터의 uri를 가져왔다면 이것을 Part로 변환하는 작업이 필요합니다.
먼저 특정 uri를 File 데이터로 변환하는 Converter를 구성해 줍니다.

object FileConverter {
    @SuppressLint("Recycle")
    fun uriToFile(context: Context, uri: Uri): File? {
        val inputStream: InputStream? = context.contentResolver.openInputStream(uri)
        inputStream?.let {
            val file = createTempImageFile(context)
            copyInputStreamToFile(it, file)
            return file
        }
        return null
    }

    private fun createTempImageFile(context: Context): File {
        val timeStamp = System.currentTimeMillis()
        val imageFileName = "JPEG_${timeStamp}_"
        val storageDir: File? = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
        return File.createTempFile(
            imageFileName,
            ".jpg",
            storageDir
        )
    }

    private fun copyInputStreamToFile(inputStream: InputStream, file: File) {
        try {
            FileOutputStream(file).use { outputStream ->
                val buffer = ByteArray(4 * 1024) // buffer size
                var read: Int
                while (inputStream.read(buffer).also { read = it } != -1) {
                    outputStream.write(buffer, 0, read)
                }
                outputStream.flush()
            }
        } catch (e: IOException) {
            e.printStackTrace()
        } finally {
            try {
                inputStream.close()
            } catch (e: IOException) {
                e.printStackTrace()
            }
        }
    }
}

외부 저장소에서 가져온 이미지의 uri를 기반으로 새로운 파일을 생성하여 반환하는 로직입니다.

 

@HiltViewModel
class UserUpdateViewModel @Inject constructor(
    @NetworkModule.Main private val apiService: ApiService
) : ViewModel() {
    fun updateUserProfileImg(header: String, userId: Int, imageFile: File): Flow<ApiResult<UserUpdateResult>> = flow {
        try {
            val imageRequestBody = imageFile.asRequestBody("image/*".toMediaTypeOrNull())
            val imagePart = MultipartBody.Part.createFormData("image", imageFile.name, imageRequestBody)
            val field : HashMap<String, String> = HashMap()
            field["Authorization"] = header
            val response = apiService.editUserImage(field, id = userId, file = imagePart)
            emit(ApiResult.Success(UserUpdateResult(response.isSuccessful, response.code())))
        } catch (e: java.lang.Exception) {
            emit(ApiResult.Error(e.localizedMessage ?: "An error occurred", 0))
        }
    }
}

ViewModel에서 변환된 File로 Part 생성 및 통신 처리를 진행합니다.

asRequestBody로 파일 형식에 맞는 Media RequestBody를 만들어줍니다. 이미지 파일이므로 "image/*"를 사용했습니다.
영상 파일이라면 "video/*"를 활용하겠지요?

생성된 파일의 이름과 RequestBody를 조합하여 Part.FormData를 생성하고, 이를 Api 통신에 활용하면 되겠습니다.

val imagePart = MultipartBody.Part.createFormData("image", imageFile.name, imageRequestBody)
val response = apiService.editUserImage(field, id = userId, file = imagePart)
emit(ApiResult.Success(UserUpdateResult(response.isSuccessful, response.code())))
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
    if (result.resultCode == RESULT_OK) {
        val data: Intent? = result.data
        data?.data?.let { uri ->
            imageUri = uri
            val imageFile = FileConverter.uriToFile(requireContext(), uri) ?: return@let
            viewLifecycleOwner.lifecycleScope.launch {
            ....
                userEditViewModel.updateUserProfileImg(token, userId = userId, imageFile).collect { result ->
                    when (result) {
                        is ApiResult.Success -> {
                            if (result.data.isSuccess) {
                                Toast.makeText(
                                    context,
                                    "저장되었습니다. ${result.data.responseCode}",
                                    Toast.LENGTH_SHORT
                                ).show()
                                binding.profileImg.setImageURI(imageUri)
                            }
                        }
                        else -> return@collect
                    }
                }
            }
        }
    }
}
반응형