Step 25: Show images from the KMP backend in the KMM Android and iOS apps
Let’s make our app more colourful by adding some pictures.
We’ll start with getting the UI ready to show the images from the backend.
Later we’ll take care of uploading images.
We already have Coil and Kingfisher as image loading libraries in Android and iOS.
So far we’ve been using only a static placeholder image for the recipes. Let’s change it.
The backend already can send us a pizza image, so let’s use it.
Go to RecipeUiModel.kt
file in commonMain/model
.
Add a list of images to this model, so that it looks like this:
data class RecipeUiModel (
val id: Long = 0,
val title: String,
val ingredients: List<Ingredient> = listOf(),
val instructions: List<Instruction> = listOf(),
val images: List<RecipeImage> = listOf()
)
Adjust the toRecipeRequest()
:
fun RecipeResponse.toRecipeUiModel() = RecipeUiModel(
id = id,
title = title,
ingredients = ingredients,
instructions = instructions,
images = images
)
Now, in RecipesRemoteSource
add a function postImage()
that will later be responsible for sending a new image to the backend:
suspend fun postImage(recipeId: Long, imageFile: ImageFile) = recipesApi.postImage(recipeId, imageFile)
In RecipeRepository
change the methods as well so that we operate with RecipeUiModel
here:
class RecipeRepository : KoinComponent {
private val recipeRemoteSource: RecipeRemoteSource by inject()
suspend fun postRecipe(recipe: RecipeUiModel): Long? =
recipeRemoteSource.postRecipe(recipe.toRecipeRequest())
suspend fun getRecipes(): List<RecipeUiModel> =
recipeRemoteSource.getRecipes().map { it.toRecipeUiModel() }
suspend fun getRecipe(id: Long): RecipeUiModel =
recipeRemoteSource.getRecipe(id).toRecipeUiModel()
}
Modify RecipeDetailsViewModel
. First remove redundant toRecipeUiModel()
and toRecipeRequest()
, because we moved them to RecipeRepository
.
Also add an empty list of images to the initial value of recipe variable:
private val _recipe = MutableStateFlow<RecipeUiModel?>(
RecipeUiModel(
title = "",
ingredients = listOf(),
instructions = listOf(),
images = listOf()
)
)
As it’s more reasonable to use RecipeUiModel
in our UI we’ll refactor all the occurrences of RecipeResponse
to RecipeUiModel
. This will also help us later to handle postImage()
to the backend.
Go to RecipeViewModel
and change all occurrences of RecipeResponse
to RecipeUiModel
to stay consistent. Check out the repository if you feel confused about how to do it.
Now let’s switch to the UI, starting with Android.
In RecipesScreen
change all occurrence of RecipeResponse
to RecipeUiModel
to stay consistent.
Also you’ll need to adjust RecipeListItemPreview
in RecipesScreen
to use RecipeUiModel
:
val recipe = RecipeUiModel(id = 0, title = title, listOf(), listOf(), listOf())
Then change the HeaderImage(image = placeholder, padding)
in RecipeDetailsScreen
to:
recipe?.images?.let {
if (it.isNotEmpty()){
HeaderImage(it[0].image) [1]
} else {
HeaderImage(placeholder) [2]
}
}
[1] If we receive any images from backend, we’ll display the first one for now.
[2] Otherwise we’ll stick to our placeholder
If you run the app and open an existing recipe, you’ll see the pizza image that you uploaded, otherwise there will still be a placeholder image.

Now let’s add a button to the placeholder image, which you’ll use to change the placeholder to a picture from your phone gallery.
In RecipeDetailsScreen
add a new composable:
@Composable
private fun PlaceholderImage(padding: PaddingValues) {
val placeholder = "https://m.media-amazon.com/images/I/413qxEF0QPL._AC_.jpg"
Box(contentAlignment = Alignment.Center) {
HeaderImage(image = placeholder, padding = padding)
IconButton(
onClick = {},
modifier = Modifier.clip(CircleShape).background(Color.Cyan)
) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = "edit image",
modifier = Modifier
.size(64.dp),
tint = Color.Black
)
}
}
}
In RecipeDetailsScreen
Scaffold
after recipe?.images?.let...
change
HeaderImage(placeholder, padding)
to
PlaceholderImage(padding)
Build and run the app.
Open a recipe without an uploaded image. You’ll see:

So far your onClick
doesn’t do anything, and we’ll change it later.
But now let’s adapt our Swift UI code to the changes in shared logic.
Change RecipeResponse
to RecipeUiModel
everywhere in the iOS project.
Go to RecipeView
and change item’s type from RecipeResponse
to RecipeUiModel
.
Do the same with recipes list in RecipesState
.
RecipeDetailState
is already using RecipeUiModel
.
Now go to recipeDetail
group and add a RecipePlaceHolderView
there. Just like in Android it shows a placeholder image with a button to edit it:
import SwiftUI
import shared
import Kingfisher
struct RecipePlaceholderView: View {
var body: some View {
ZStack{
KFImage(URL(string: "https://m.media-amazon.com/images/I/413qxEF0QPL._AC_.jpg"))
.resizable()
.frame(width: 200, height: 200)
Image(systemName: "camera.circle.fill")
.font(.system(size: 28, weight: .light))
.foregroundColor(Color(#colorLiteral(red: 0.4352535307, green: 0.4353201389, blue: 0.4352389574, alpha: 1)))
.frame(width: 50, height: 50)
.background(Color(#colorLiteral(red: 0.9386131763, green: 0.9536930919, blue: 0.9635006785, alpha: 1)))
.clipShape(Circle())
}
}
}
Finally instead of just showing a placeholder (with KFImage
) in RecipeDetailView
, we will switch between an image from the backend and a placeholder view
if (state.recipe?.images.isEmpty == false){
KFImage(URL(string: state.recipe?.images.first))
.resizable()
.frame(width: 200, height: 200)
} else {
RecipePlaceholderView().padding(16)
}
Run the app: now you can change the image of your recipe not only on Android, but also on iOS!

And it took us only several minutes to adjust this iOS functionality to what we already had in the shared code!
In the next step we’ll extend our shared code to send an image to the backend from the Android or iOS app.