In this post I will show you the steps I took to upload image files to a PHP server in Android using Retrofit.
Below I will demonstrate
- how to create a user picker dialog in Android
- get the Uri from the image picked
- query the content resolver in Android to get the necessary path needed to create a file to use in the upload process
- use Retrofit to post a form with data to a local PHP server
- the PHP script used to get the image which includes renaming and creating the file in the chosen directory
The method used to upload the image to a PHP server in Android using Retrofit utilizing multipart form data. Each part is sent as a different entity as shown below. One part sends the JSON data. Another part sends the image file.
NOTE: The link below is another approach used to send image files to a PHP server in Android using Retrofit but without needing to use multipart data forms.
Upload Base64 Encoded Images In Android To PHP Server
Add Libraries
build.gradle
build.gradle - module level
//retrofit
implementation(libs.retrofit)
//GSON
implementation(libs.converter.gson)
//http client interceptor
implementation(libs.logging.interceptor)
//glide
implementation(libs.glide)
libs.versions.toml
[versions]
...
loggingInterceptor = "4.11.0"
retrofit = "2.11.0"
converterGson = "2.10.0"
glide = "1.0.0-beta01"
[libraries]
...
logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "loggingInterceptor" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "converterGson" }
glide = { module = "com.github.bumptech.glide:compose", version.ref = "glide" }
Code
First I will share my project structure via the image below. You don't have to copy exactly, this is just to keep things organized.

I will go through each file and post its contents.
AndroidManifest.xml
AndroidManifest.xml - add appropriate permissions needed, also allow cleartext if you are using a local test server without SSL
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<application
...
android:usesCleartextTraffic="true"
...>
MainActivity
MainActivity - the main activity of the app. This will hold a reference to the main composable as well as a reference to access the ViewModel. I will be using the MVI ViewModel architecture.
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 {
SampleRetrofitImageUploadTheme {
val state = viewModel.mvistate.collectAsStateWithLifecycle().value
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
UserForm(
modifier = Modifier.padding(innerPadding),
state,
viewModel::handleEvent)
}
}
}
}
}
ViewModelFactory
ViewModelFactory - used to create the ViewModel as well as the ability to pass context to it for later use.
class ViewModelFactory(private val application : Application) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return if(modelClass.isAssignableFrom(com.itgeek25.sampleretrofitimageupload.viewmodel.ViewModel::class.java)){
ViewModel(application) as T
} else {
throw IllegalArgumentException("ViewModel Not Found!!")
}
}
}
ViewModel
ViewModel - does the work for the UI. Processes file Uri for use in the data form as well as makes the access call to Retrofit to send request.
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.updateFormState -> {
updateFormState(event)
}
}
}
private fun updateFormState(event: FormIntent.updateFormState) {
viewModelScope.launch {
_mvistate.update { event.formState }
}
if (!event.formState.isLoading) {
event.formState.image?.let {
viewModelScope.launch {
val file = fileTools.createTempFileFromUri(it)!!
_mvistate.update { _mvistate.value.copy(image = file.toUri()) }
}
}
return
}
val file: File? = _mvistate.value.image?.path?.let { File(it) }
if(file == null){
viewModelScope.launch {
_mvistate.update { _mvistate.value.copy(isLoading = false, response = -1) }
}
return
}
val query = RetrofitHelper.getInstance(applicationContext).create(AccessApi::class.java)
val requestBody = file.asRequestBody()
val imgPart = MultipartBody.Part.createFormData("img", file.name, requestBody)
val call = query.testImageFormUpload(
FormData(
desc = event.formState.desc,
img = event.formState.image!!
),
imgPart
)
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) {
viewModelScope.launch {
_mvistate.update {
_mvistate.value.copy(
isLoading = false,
response = 1
)
}
}
} else {
viewModelScope.launch {
_mvistate.update {
_mvistate.value.copy(
isLoading = false,
response = -1
)
}
}
}
}
})
}
}
FormState
FormState - used to hold the state of the UI elements and the form data. Primarily used so the ViewModel knows what is going on during the user interactions and the request. This is not completely necessary to add in this test but I added it as good practice.
data class FormState (
val response : Int = 0,
val isLoading : Boolean = false,
val desc : String = "",
val image : Uri? = null
)
FormIntent
FormIntent - use to make action calls in the MVI model with the ViewModel.
sealed interface FormIntent {
data class updateFormState(val formState : FormState) : FormIntent
}
RetrofitHelper
RetrofitHelper - object used to create instance of the Retrofit library making the network request.
object RetrofitHelper {
val baseUrl = "<server_address_here>"
fun getInstance(applicationContext : Context): Retrofit {
val httpLoggingInterceptor: HttpLoggingInterceptor = HttpLoggingInterceptor()
httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY)
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(httpLoggingInterceptor)
.build()
val gson = GsonBuilder()
.setLenient().create()
return Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create(gson))
.client(okHttpClient)
.build()
}
}
AccessApi
AccessApi - interface used to process the Retrofit network request. This will hold a reference of what and how the data is being sent. When sending a file, you will need to use Multipart data types. Each item in the form will need to be annotated with @Part("something") along with the function being annotated with @Multipart.
interface AccessApi {
@Multipart
@POST("<filename_on_server>.php")
fun testImageFormUpload(@Part("data") formData : FormData,
@Part img : MultipartBody.Part) : Call<FormResponse>
}
FormData
FormData - specify the data structure that is being sent in Retrofit.
data class FormData(
@SerializedName("desc")
val desc : String,
@SerializedName("img")
val img : Uri,
)
FormResponse
FormResponse - this is used to get a response form the server after the query request has been sent.
data class FormResponse(
@SerializedName("code")
val code : Int,
@SerializedName("response")
val response : String
)
FileTools
FileTools - this is used after the user chooses the image they want.
This does the following:
- the Uri from the image picker
- queries the contentresolver in Android to get the real filepath
- creates a temporary file that can then be used to send with the Retrofit library to the server.
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
}
}
interface FileToolsInf {
suspend fun createTempFileFromUri(uri : Uri) : File?
}
FileTools As Extension Function
You could instead of creating this class as I did above, create an extension function for it like below. During tests, I simply added this to the bottom of the ViewModel.
fun Context.createTempFileFromUri(uri: Uri) : File? {
var filename = ""
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.isNullOrEmpty()) return null
val tempFile = File(cacheDir, filename)
tempFile.createNewFile()
val inputStream = contentResolver.openInputStream(uri)
val outputStream = FileOutputStream(tempFile)
outputStream.use {
inputStream?.copyTo(it)
}
return tempFile
}
UserForm
UserForm - this is the Composable used to the user.
The process of the UI:
- the user clicks the button to choose image
- the user then types in a description name in the TextField
- this description gets sent in the form data to the server as the new image filename
@Composable
fun UserForm(
modifier: Modifier,
state: FormState,
handleEvent: (FormIntent) -> Unit
) {
val selectImage = rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { result ->
if(result != null){
handleEvent(FormIntent.updateFormState(state.copy(image = result)))
}
}
var desc by remember { mutableStateOf("") }
Column(modifier = modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = state.desc)
if(state.image != null) {
Text(text = state.image.encodedPath ?: "")
GlideImage(modifier = Modifier.size(200.dp), model = state.image, contentDescription = null)
}
Button(onClick = {
selectImage.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
}) {
Text(text = "Select Image")
}
TextField(value = desc, onValueChange = { desc = it })
if(!state.isLoading && state.image != null) {
Button(onClick = {
handleEvent(FormIntent.updateFormState(state.copy(isLoading = true, desc = desc)))
}) {
Text(text = "Post Data")
}
}
}
}
PHP Server Script
NOTE: This script is basic and needs better error handling.
The script will:
- ensure it is a POST method call
- get the form data sent and place in a json variable
- get the file extension of the image being sent
- change the name of the new image file
- create a directory to place final image if it doesn't exist
- return json array with a server response in the form of the FormResponse used as the return call in the Android Retrofit query above
<?php
if(!isset($_POST['data'])){
echo json_encode(array(
'code' => 1,
'response' => "Error"
));
exit;
}
$data = $_POST["data"];
$json = json_decode($data, true);
$desc = $json['desc'];
$file = $_FILES['img'];
$imageType = exif_imagetype($file['tmp_name']);
if(!$imageType){
echo json_encode(array(
'code' => 1,
'response' => "Error"
));
exit;
}
$fileExt = image_type_to_extension($imageType, true);
$imageDir = "./images/";
if(!file_exists($imageDir)){
mkdir($imageDir);
}
$finalImg = $imageDir.$desc.$fileExt;
copy($file['tmp_name'], $finalImg);
//move_uploaded_file($file['tmp_name'], $finalImg);
unlink($file['tmp_name']);
echo json_encode(array(
'code' => 0,
'response' => $desc . "-" . $finalImg
));
exit;
?>
Issue
You may notice I commented out the move_uploaded_file() method above. The script worked but my test environment was giving permission issues viewing the final file. I tried chmod as well as change directories. Still same issue. In production server, the script works with the move_uploaded_file().
Hope this was helpful.