Upload Base64 Encoded Images In Android To PHP Server
Android Uploading Base64 Encoded Images To PHP Server in Retrofit.

In this post I will show you a sample of how to upload base64 encoded images to a PHP server in Android without needing to utilize the multipart data forms in Retrofit.

The Need

I recently had a project that required me to send images to a PHP server from Android using Retrofit.

My previous post below shows how to implement this in both Android and on a PHP server using Retrofit Multipart form data.

Upload Image Files To PHP Server In Android Using Retrofit

The issue I had was processing the JSON data sent from Android to the PHP server needed to be parsed a particular way. The sample above uses multipart form data. In my use case, I required the data to be sent inside the JSON data somehow for less processing overhead on the server side. NOTE: Meaning I would have needed to re-query the server data again to process the file a certain way.

Solution

The solution I found was to encode the image into base64 for the transfer then decode it on the server side. This will allow me to upload base64 encoded images to PHP server in Android using Retrofit without needing multipart data forms.

Code

AccessAPI

The AccessAPI for Retrofit to send the data to the server. This is the big change I needed for my use case. I couldn't use the MultiPart form data. I needed to send similar to what is stated below. Annotation of @POST then the data being sent annotated with @Body.

interface AccessApi {

    @POST("<server_addr>.php")
    fun testBaseImageFormUpload(@Body formData : FormData) : Call<FormResponse>

}

FormData

FormData holds the formatted data being sent to the PHP server.

data class FormData(
    @SerializedName("desc")
    val desc : String,
    @SerializedName("base_image")
    val baseImage : String? = null,
    @SerializedName("ext")
    val ext : String? = null,
)

FormResponse

FormResponse format to read response from PHP server.

data class FormResponse(
    @SerializedName("code")
    val code : Int,
    @SerializedName("response")
    val response : String
)

UserForm

UserForm is the Composable used for the user UI.

@OptIn(ExperimentalGlideComposeApi::class)
@Composable
fun UserForm(
    viewModel : ViewModel,
    modifier: Modifier,
    state: FormState,
    handleEvent: (FormIntent) -> Unit
) {

    var image by remember { mutableStateOf(ImageItemType("", "","")) }

    val imageHandle = rememberCoroutineScope()

    val selectImage =
        rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { result ->
            if (result != null) {
                imageHandle.launch {
                    if(image.filepath.isNotEmpty()){
                        viewModel.deleteTempFile(image.filepath)
                    }
                    image = viewModel.createTempFileFromUriAndEncode(result)
                    handleEvent(FormIntent.updateImageData(image))
                }
            }
        }

    var desc by remember { mutableStateOf("") }

    Column(modifier = modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally) {
        Text(text = state.desc)

        if(state.imageData != null) {
            Text(text = state.imageData.filepath)
            GlideImage(modifier = Modifier.size(200.dp), model = state.imageData.filepath, contentDescription = null)
        }

        Button(onClick = {
            selectImage.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
        }) {
            Text(text = "Select Image")
        }

        TextField(value = desc, onValueChange = { desc = it })

        if(!state.isLoading) {
            Button(onClick = {
                handleEvent(FormIntent.updateBaseImageFormState(state.copy(isLoading = true, desc = desc)))
            }) {
                Text(text = "Post Data")
            }
        }
    }

}

FormIntent

FormIntent is used as the interface with the UI and the viewmodel in the MVI architecture.

sealed interface FormIntent {

    data class updateBaseImageFormState(val formState : FormState) : FormIntent

    data class updateImageData(val imageData : ImageItemType) : FormIntent

}

FormState

FormState hold the user UI state and is updated using the viewmodel.

data class FormState (
    val response : Int = 0,
    val isLoading : Boolean = false,
    val desc : String = "",
    val imageData : ImageItemType? = null
)

ImageItemType

I created a simple data class name ImageItemType to hold required data.

data class ImageItemType(
    val filepath: String,
    val base : String,
    val ext : String
)

ViewModel

ViewModel performs the actions for the UI. I also attached the ViewModelFactory class at bottom. I have it in a separate class file but that is your choice.

class ViewModel(val applicationContext: Application) : ViewModel() {

    private val _mvistate: MutableStateFlow<FormState> = MutableStateFlow<FormState>(FormState())
    val mvistate: StateFlow<FormState> = _mvistate.asStateFlow()

    val fileTools = FileTools(applicationContext)

    fun handleEvent(event: FormIntent) {
        when (event) {
            is FormIntent.updateBaseImageFormState -> {
                updateBaseImageFormState(event)
            }

            is FormIntent.updateImageData -> {
                updateImageData(event)
            }
        }
    }

    private fun updateImageData(event : FormIntent.updateImageData){
        _mvistate.update {
            _mvistate.value.copy(imageData = event.imageData)
        }
    }

    private fun updateBaseImageFormState(event: FormIntent.updateBaseImageFormState) {
        viewModelScope.launch {
            _mvistate.update { event.formState }

            if(_mvistate.value.imageData == null){
                return@launch
            }

            val query = RetrofitHelper.getInstance(applicationContext).create(AccessApi::class.java)

            val call = query.testBaseImageFormUpload(
                FormData(
                    desc = event.formState.desc,
                    baseImage = event.formState.imageData?.base,
                    ext = event.formState.imageData?.ext
                )
            )
            call.enqueue(object : Callback<FormResponse> {
                override fun onFailure(p0: Call<FormResponse>, error: Throwable) {
                    viewModelScope.launch {
                        _mvistate.update { _mvistate.value.copy(isLoading = false, response = -1) }
                    }
                }

                override fun onResponse(p0: Call<FormResponse>, response: Response<FormResponse>) {
                    if (response.code() == 200) {

                        _mvistate.update {
                            _mvistate.value.copy(
                                isLoading = false,
                                response = 1
                            )
                        }

                    } else {

                        _mvistate.update {
                            _mvistate.value.copy(
                                isLoading = false,
                                response = -1
                            )
                        }

                    }
                }

            })
        }
    }

    fun deleteTempFile(path : String){
        viewModelScope.launch {
            fileTools.deleteTempFile(File(path))
        }
    }

    suspend fun createTempFileFromUriAndEncode(uri : Uri) : ImageItemType {
        val job = viewModelScope.async {
            val file = fileTools.createTempFileFromUri(uri)
            var base: String? = null
            if (file != null) {
                base = fileTools.encodeImageToBase64(file)
                val imageItem = ImageItemType(file.absolutePath, base, file.extension)
                return@async imageItem
            } else {
                return@async ImageItemType("", "","")
            }
        }
        return job.await()
    }
}

class ViewModelFactory(private val application : Application) : ViewModelProvider.Factory {

    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return if(modelClass.isAssignableFrom(com.itgeek25.samplebase64imageupload.viewmodel.ViewModel::class.java)){
            ViewModel(application) as T
        } else {
            throw IllegalArgumentException("ViewModel Not Found!!")
        }
    }
}

FormResponses

FormResponses is posted in the previous post link above but I will add here as well. This wasn't changed.

data class FormResponse(
    @SerializedName("code")
    val code : Int,
    @SerializedName("response")
    val response : String
)

MainActivity

Finally in the MainActivity to put all this together.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val factory = ViewModelFactory(this.application)
        val viewModel = ViewModelProvider(
            this,
            factory
        )[ViewModel::class.java]

        setContent {
            SampleBase64ImageUploadTheme {

                val state = viewModel.mvistate.collectAsStateWithLifecycle().value

                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    UserForm(
                        viewModel,
                        modifier = Modifier.padding(innerPadding),
                        state,
                        viewModel::handleEvent)
                }
            }
        }
    }
}

Note About the FileTools

I added more to the FileTools class and rewrote it a bit. After the user selects an image from the Android picker dialog, the FileTools class is used.

FileTools Functions Exampled

createTempFileFromUrimethod will get called with the Uri of the selected image. This uri is used in the query of the Android contentresolver to get the image location. This image is temporarily created in the app cache directory
createTempFileScaleBitmapmethod does the same as above but just scales the image to a set value hardcoded in the method
deleteTempFilemethod is just to clean things up after the image is successfully uploaded to server or could be used if the user changes their mind and wants to upload a different image form the Android picker dialog
encodeImageToBase64method to encode the given file to base64 and return a string

FileTools

class FileTools(val context: Context) : FileToolsInf {

    override suspend fun createTempFileFromUri(uri: Uri): File? {
        var filename = ""
        context.contentResolver.query(uri, null, null, null, null).use { cursor ->
            if (cursor != null) {
                val columnName = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
                cursor.moveToFirst()
                filename = cursor.getString(columnName) ?: ""
            }
        }
        Log.i("Filename: ", filename)
        if (filename.isEmpty()) return null
        val tempFile = File(context.cacheDir, filename)
        withContext(Dispatchers.IO) {
            tempFile.createNewFile()
            val inputStream = context.contentResolver.openInputStream(uri)
            val outputStream = withContext(Dispatchers.IO) {
                FileOutputStream(tempFile)
            }
            outputStream.use {
                inputStream?.copyTo(it)
            }
        }
        return tempFile
    }

    override suspend fun createTempFileScaleBitmap(uri: Uri): File? {
        var filename = ""
        context.contentResolver.query(uri, null, null, null, null).use { cursor ->
            if (cursor != null) {
                val columnName = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
                cursor.moveToFirst()
                filename = cursor.getString(columnName) ?: ""
            }
        }
        Log.i("Filename: ", filename)
        if (filename.isEmpty()) return null
        val tempFile = File(context.cacheDir, filename)
        withContext(Dispatchers.IO) {
            tempFile.createNewFile()
            val inputStream = context.contentResolver.openInputStream(uri)
            val bitmap = BitmapFactory.decodeStream(inputStream)
            Log.d("Original", "Width: ${bitmap.width} - Height: ${bitmap.height}")
            val width = 480
            val aspectRatio = bitmap.width / bitmap.height.toFloat()
            val height = width / aspectRatio
            Log.d("Scaling", "Width: $width - Aspect: $aspectRatio - Height: $height")
            val newBitmap = Bitmap.createScaledBitmap(bitmap, width, height.toInt(), false)

            val outputStream = withContext(Dispatchers.IO) {
                FileOutputStream(tempFile)
            }
            newBitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)

            outputStream.use {
                inputStream?.copyTo(it)
            }
        }
        return tempFile

    }

    override suspend fun deleteTempFile(tempFile: File) {

        Log.i("TempFile", tempFile.absolutePath)
        if (tempFile.exists()) {
            Log.i("TempFile", "Exists!")
            if (tempFile.delete()) Log.i("TempFile", "Cached file deleted")
        }
    }

    override suspend fun getFile(uri: Uri): File {
        return uri.toFile()
    }

    override suspend fun encodeImageToBase64(file: File): String {
        return Base64.encodeToString(file.readBytes(), Base64.DEFAULT).trim()
    }

}

interface FileToolsInf {
    suspend fun createTempFileFromUri(uri: Uri): File?

    suspend fun createTempFileScaleBitmap(uri: Uri): File?

    suspend fun deleteTempFile(tempFile: File)

    suspend fun getFile(uri: Uri): File

    suspend fun encodeImageToBase64(file: File): String
}

PHP

Finally this is the main code to get the image in PHP on your server.

<?php

if(!isset($_POST)){
    echo json_encode(array(
        'code' => 2,
        'response' => "Error"
    ));
    exit;
} 
$data = json_decode(file_get_contents('php://input'));  

$baseImage = $data->base;
$image = base64_decode($baseImage);
$desc = $data->desc;
$fileExt = ".".$data->ext;

$imageDir = "./images/";
if(!file_exists($imageDir)){ 
    mkdir($imageDir);
 }
$finalImg = $imageDir.$desc.$fileExt;

file_put_contents($finalImg, $image);

echo json_encode(array(
    'code' => 0,
    'response' => $desc . "-" . $finalImg
));
exit;


?>

Hope this was useful.

Leave a Reply

Your email address will not be published. Required fields are marked *