KMPizza
Kotlin Multiplatform + Pizza = ❤️

Intro: All you need is KMP (and pizza)

What is Kotlin Multiplatform? In short, it’s the future of mobile development. You don’t believe me? Let me try to persuade you: Kotlin Multiplatform is still young, but it’s a very promising technology. It allows developers to share parts of code between different platforms, whether it’s your backend, or an iOS app, or an Android app, or even a web or desktop. Kotlin will find its way anywhere.

It’s particularly great for mobile developers, because it can help us leverage the best of Kotlin to quickly build apps that look amazing both on iOS and Android. You’ll be able to share lots of code between the two platforms and focus on making UI appealing to the user! Moreover, you can build a backend in Kotlin that will complement your KMM app and make it even more sustainable.

I’ve spent a lot of time researching on KMP, struggling through gradle files and dependencies, building apps for work and for myself and would love to share my experience with everyone else who’s interested.

So stock up with pizza ingredients and let’s explore the exciting world of KMP together! 🤓

This repository will accompany each step of the blog: KMPizza Repo

Photo by Ivan Torres on Unsplash

This project is inspired by:
KaMPKit
MultiplatformPlayground
PeopleInSpace

Step 31: Deploy Ktor backend on Fly.io

“Where should I move my hobby rpject from Heroku?” After researching the alternatives, I’ve settled with Fly.io. It looks like a fairly easy (compared to Google Cloud Platform) option and has good plans for hobby projects.

Also I love their UI.
Isn’t this art and the haikus inspiring?
It took me 5 minutes to log in just because I was refreshing the page for more haikus 😀

fly haiku

The best thing for your small pet project is that every Fly.io plan has a free allowance, which (according to documentation) includes:

Additional resources are billed at the usage-based pricing (see documentation).

To deploy KMPizza backendx on Fly.io, we’ll need the docker image from the previous step.

The steps to get started are well described here.
I’ll summarize what I’ve done on my Mac:

First install flyctl tool: brew install flyctl

Then sign up flyctl auth signup

I found that signing up with GitHub would be the most convenient for me.

sign up

You can “Try Fly.io for free”, but you’ll still need to add you credit card data afterwards, so I advise you to do it right away.

get started

After signing up and logging in you create a new Fly App.
Fly.io is perfect for deploying docker images. That’s why in the previous step you learned how to use Docker containers.

Go back to KMPizza project. To deploy on Fly your app needs a fly.toml file with instructions how to deploy your app. In command line go to the root of the project and create this file with:

fly launch

You’ll see something like:

fly launch

In the next step, as you already have a postgres database separately with ElephantSQL reply no:

 
? Would you like to set up a Postgresql database now? No
? Would you like to set up an Upstash Redis database now? No

Also reply no to whether you want to deploy right now:

 
? Would you like to deploy now? (y/N) N

Finally in the command line you’ll see

Your app is ready! Deploy with flyctl deploy

After these steps in the project root you’ll have a fly.toml file with a default configuration to deploy your app, something like this:

 
app = "kmpizza"
kill_signal = "SIGINT"
kill_timeout = 5
processes = []

[env]

[experimental]
 auto_rollback = true

[[services]]
 http_checks = []
 internal_port = 8080
 processes = ["app"]
 protocol = "tcp"
 script_checks = []
 [services.concurrency]
   hard_limit = 25
   soft_limit = 20
   type = "connections"

 [[services.ports]]
   force_https = true
   handlers = ["http"]
   port = 80

 [[services.ports]]
   handlers = ["tls", "http"]
   port = 443

 [[services.tcp_checks]]
   grace_period = "1s"
   interval = "15s"
   restart_limit = 0
   timeout = "2s"

As you can see, it has the basic information for deployment on Fly.io: the app’s name, used ports, concurrent connection limits etc.

Now build your Docker image locally (if you haven’t done it in the previous step yet) and deploy the image on Fly with:

fly deploy --local-only

In the end you’ll see something like:

deploy locally

But the deployment fails. Ooops.

failed deployment

If you take a look at the logs you’ll see our old grumpy friend, the NullPointerException:

Caused by: java.lang.NullPointerException: getenv("bucketName") must not be null

To set your secrets for Fly.io use all the environment variables you need for the deployment (just like every time before) in the following command:

flyctl secrets set SECRET_ONE=secret1 SECRET_TWO=secret2

For KMPizza it’s something like

flyctl secrets set bucketName=kmpizza secretKey=MYVERYSECRETKEY accessKey=MYVERYSECRETKEY region=eu-central-1 DB_NAME=ElephantDb DB_USER=ElephantUser DB_PASS=ElephantPassword

Run fly deploy --local-only again:

success

Yay! It worked.

Check out your app with flyctl status

fly status

One of the most useful pieces of information here is your hostname: kmpizza.fly.dev.
You can also check it in the user friendly Fly web UI, and browse through the app overview, set secrets, manage certificates and billing.

web app UI

To visit your app either run flyctl open or type https://kmpizza.fly.dev/pizza in a browser.
You’ll see an initial page with KMPizza greeting you.

Finally go back to your KMPizza project in Android Studio and in KtorApiImpl.kt use the new backend url:

 
val prodUrl = "https://kmpizza.fly.dev/"

Now I’m hungry and I’ll go grab a slice of pizza.

Step 30: Store data with ElephantSQL and containerize Ktor backend application with Docker

As Heroku cancelled its free plans and sharing knowledge has suddenly become expensive, I’ve decided to move KMPizza Ktor backend somewhere else. To do so I also had to learn how to containerize my backend app with Docker.

Another change was database related. Even though many PaS services offer a Postgres plugin, it usually comes with extra expenses, so I decided to host the PostgresSQL database separately. I went with ElephantSQL, as they have very attractive offers for small projects. And they have a free plan for such tiny ones as KMPizza. But the best thing about ElephantSQl is that you don’t have to enter your credit card data if you only need a free plan.

elephantsql plans

Connecting to ElephantSQL

To start with, go to ElephantSQL.
Sign up, create a new team and then click on Create a New Instance.

create a new instance button

Then go on and create a Tiny Turtle Database instance.

select tiny turtle plan

It takes less than a minute.
Voilà, you have the connection data:

connection data

Here you see the most important pieces: user DB_USER, default database name DB_NAME, password DB_PASS and the URL which you’ll partly use as your connection string.

And you can see the new instance in the list of all your projects:

database list

Now let’s get back to our app and see what we need to change in the code.

First, in application.conf extend the database configuration with the following properties:

 
user = "postgres"
user = ${?DB_USER}
password = "password"
password = ${?DB_PASS}
db_name = "recipecollection"
db_name = ${?DB_NAME}

These properties will be set from the environment variables later. In the past we used to set the whole JDBC_DATABASE_URL string from the environment, but I’ve dicided it’d be nicer to split it into several parts and concatenate into a connection string after.

Hence, adjust the init {} in your LocalSourceImpl.kt to use the new connection data:

 
init {
   val config = application.environment.config.config("database")
   val dbUser = config.property("user").getString() [1]
   val dbPassword = config.property("password").getString() [2]
   val dbName = config.property("db_name").getString() [3]
   val instanceConnection = config.property("instance_connection").getString()
   val url = "jdbc:postgresql://snuffleupagus.db.elephantsql.com/$dbName" [4]



   dispatcher = newFixedThreadPoolContext(poolSize, "database-pool")
   val hikariConfig = HikariConfig().apply {
       jdbcUrl = url
       maximumPoolSize = poolSize
       driverClassName = driver
       username = dbUser [5]
       password = dbPassword [6]
       validate()
   }



}

[1], [2], [3] Extract the database user, password and database name from the environment variables
[4] Use the new database url to connect
[5], [6] Use the username and password to authenticate

Now proceed to the next step before deploying the backend: packaging it into a docker container.

Using Docker

Follow the instructions to install desktop Docker application on your computer. It’s rather uncomplicated if you carefully follow all the steps and check that your computer meets all the system requirements.

Now return to KMPizza. Create a new file in the KMPizza project root called Dockerfile:

dockerfile

Copy the following configuration to the Dockerfile:

 
FROM openjdk:11
EXPOSE 8080:8080
RUN mkdir /app
COPY ./backend/build/libs/Backend.jar /app/Backend.jar
ENTRYPOINT ["java","-jar","/app/Backend.jar"]

This file basically tells Docker to build an image from our shadow jar file in ./backend/build/libs/Backend.jar and expose port 8080 for the access.

Now we need to create a new shadow jar package for the backend, so that we can containerize it. Just like in Step 4 we simply need to rebuild the project and look for a new jar file in /backend/build/libs/.

However, currently I’m running into an IR lowering error trying to rebuild the project. But here’s a temporary workaround for it.

As the error comes from Koin usage in CommonModule.kt we can replace

 
fun initKoin(appDeclaration: KoinAppDeclaration) = startKoin {
    appDeclaration()
    modules(
        apiModule,
        repositoryModule,
        viewModelModule,
        platformModule,
        coreModule
    )

with

 
fun initKoin(appDeclaration: KoinAppDeclaration) =  {}

Now make the project again and the new Backend.jar will appear in the build/libs folder.
May be there’s a better solution to this problem - reach out to me if you have an idea.
Don’t forget to reverse your changes to CommonModule.kt afterwards!

Now it’s time to containerize this Backend.jar according to the Dockerfile.
Open your command line in the KMPizza project root.
Run the following command:

docker build -t kmpizza .

This tell the Docker to create and image using the Dockerfile from the current directory. The -t parameter tells it to tag the image “kmpizza”. You should be able to see your new image in the Docker Desktop:

docker desktop image

Finally, we can set the local environment variables from the commands line just like before:

export bucketName=kmpizza
export secretKey=your_AWS_secret_key
export accessKey=your_AWS_access_key
export region=eu-central-1
export DB_NAME=your_ElephantSQL_db_name
export DB_USER=your_ElephantSQL_user
export DB_PASS=your_ElephantSQL_password

Now tell Docker to use the variables from local environment when you test run the container locally:

docker container run --env accessKey --env secretKey --env region --env bucketName --env DB_NAME --env DB_USER --env DB_PASS -p8080:8080 kmpizza

[1] Here with –env parameter you pass every secret to Docker from the environment variables.
[2] With -p8080:8080 you specify the ports you’ve exposed in the Dockerfile.
[3] And kmpizza stands for the application tag you used when building an image.

As usually, go to localhost:8080/pizza and verify it. Alternatively, with Postman you can try sending a recipe to your new database with the /recipes endpoint and see that it’s still working.

Our Ktor server runs locally now and is connected to ElephantSQL cloud database.
In the next step we’ll see how to deploy it somewhere else 😉

Step 29: Add assets and a placeholder for no internet connection

Remember the task from Step 27?

If you try opening one of the locally saved recipes without internet connection, you’ll notice that there is no image. Fix it by adding a “No Internet” placeholder instead of an empty space when the user can’t download the image.

Here’s my solution.
First, find an image you’d like to show when image libraries fail to load a placeholder image from the Internet.
In Android place this image in res/drawable:

drawable folder

In iOS first create an Asset Catalog by clicking on “New File…” and adding an Asset catalog.

add asset catalog

You’ll see it in your app structure:

ios structure

Then just drag and drop the chosen image to this catalog:

asset image ios

Now we can use this image in our apps.

In Android simply extend the HeaderImage loading with error:

 
AsyncImage(
   model = image,
   modifier = Modifier.size(200.dp),
   contentDescription = null,
   error = painterResource(id = R.drawable.no_pizza)
)

Turn off the connection. Build and run the Android app. You’ll see if the recipe has an image, but it couldn’t be downloaded, there’ll be a no-connection placeholder instead:

android no-connection

Similarly, in iOS we can extend KFImage logics in RecipeDetailView with .placeholder:

 
if (state.recipe?.images.isEmpty == false){
                KFImage(URL(string: state.recipe?.images.first?.image ?? ""))
                    .resizable()
                    .placeholder { Image("no_pizza").resizable() }
                    .frame(width: 180, height: 180)
            }

However, if you try running the app you’ll get an error about a missing AppIcon.
Looks like we accidentally deleted something 😱
But no worries, it’s a good excuse to add an AppIcon to our app!

First, go to https://appicon.co and create a set of icons for you Android and iOS apps.

Then simply take Assets.xcassets and drag it to your iOS project.
Then remove the old Assets catalog and place your no-connection image to the new one.

Also change the name of the iOS app here:

ios app change name

Build and run the app again. Turn off the internet connection and you’ll see a no-connection placeholder image.

ios no-cnnection

And the app has a new icon and name now as well:

ios ap icon

Finally, let’s also add an app icon and change the name in the Android app.
Go to File -> New -> Image Asset

android image asset

Then choose your image

choose image asset

Finally, in Android Manifest specify the app icon (and also the app name):

 
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"

Build and run the app again. In your Android apps you’ll see the new icon and a new name:

android app icon

Now, KMPizza has grown to be a rather functional KMP app. It has a Ktor backend and a shared business layer. What’s more, it also shares the ViewModel layer between iOS and Android.

But in software development there’s always room for growth. Since I started with KMP a lot of new libraries appeared, which make the development easier. There are also different approaches to a shared local database - for example, using Realm. There are other Kotlin backend frameworks as well, like Kotlin Spring, Micronaut, Vert.X. In general, KMP offers a great playground for your future experiments and projects.

Meanwhile, there are already teams that are using KMP in prouduciton to gain a competitive edge. Once KMP goes stable, there will be even more reasons to adopt it.

May be your KMP project will be the next big thing?

Use your imagination and never stop learning!

Step 28: Refactoring the iOS KMM App: NavigationLink and Binding

There’s always room for perfection, so let’s brush up our iOS app a little bit.
Go to RecipesState and add

 
func getRecipes() {
    viewModel.getRecipes()
}

Then in RecipesView make some small adjustments.
Change

 
    NavigationLink(destination: RecipeDetailView (
        id: recipe.id.toKotlinLong(), 
        isPresented: self.$isRecipeDetailsShown, 
        uploadSuccess: self.$uploadSuccess)
        ) 

to

 
    NavigationLink(destination: RecipeDetailView (
        id: recipe.id [1], 
        isPresented: self.$isRecipeDetailsShown, 
        uploadSuccess: self.$uploadSuccess)
        ) 

[1] We’ll pass to RecipeDetailView a recipeId as Int64? and convert to KotlinLong? later.

Also in onAppear() change

 
if uploadSuccess {
    state.viewModel.getRecipes()
}

to

 
if uploadSuccess {
    state.getRecipes()
}

Then in FloatingActionButton remove:

 
.simultaneousGesture(
    TapGesture().onEnded {
        isPresented = true
    }
)

Now go to RecipeDetailsState.
Remove

 
    @Published private(set) var upload: KotlinBoolean? = nil

And replace

 
self.upload = upload
    if ((self.upload?.boolValue ?? false) == true){
        self.uploadSuccess = true
        self.isPresented = false
    }

with just

 
if ((upload?.boolValue ?? false) == true){
    self.uploadSuccess = true
    self.isPresented = false
}

Also in RecipeDetailState add several published values, which we’ll use in RecipeDetailView after:

 
    @Published var title: String = "" {
        didSet {
            viewModel.onTitleChanged(title: title)
        }
    }
    
    @Published var ingredients: [Ingredient] = [] {
        didSet {
            if let ingredient = ingredients.last {
            viewModel.onIngredientsChanged(ingredient: ingredient)
            }
        }
    }
    
    @Published var instructions: [Instruction] = [] {
        didSet {
            if let instruction = instructions.last {
            viewModel.onInstructionsChanged(instruction: instruction)
            }
        }
    }
    
    @Published var image: UIImage? = nil {
        didSet {
            viewModel.onImageChanged(image: image ?? UIImage())
        }
    }

Now go to RecipeDetailView
First change init to

 
    init(id: Int64?, isPresented: Binding<Bool>, uploadSuccess: Binding<Bool> ) { [1]
        self.recipeId = id?.toKotlinLong()
        state = RecipeDetailState(recipeId: id?.toKotlinLong(), isPresented: isPresented, uploadSuccess: uploadSuccess)
    }

[1] I think it’s better to pass id as Int64? here and then convert it to KotlinLong? for the RecipeDetailState

Now in EditIngredients replace

 
    var ingredients: [Ingredient]?
    var viewModel: RecipeDetailsViewModel

with

 
    @Binding var ingredients: [Ingredient]

And change the AddButton to

 
AddButton {
        ingredients.append(Ingredient(id: 0, name: name, amount: amount, metric: metric))
        name = ""
        amountString = ""
        metric = ""
    }.disabled(!isValid)
     .padding()

Do the same for EditInstrucitons:
Change

 
    var instructions: [Instruction]?
    var viewModel: RecipeDetailsViewModel

to

 
    @Binding var instructions: [Instruction]

Add

 
private var isValid: Bool {
    return description != ""
}

So that the add instruciton plus button is active only when the user types an instruction. Use it in body:

 
 var body: some View {
        
        Instructions(instructions: instructions)
        
        HStack {
            Text ("\(instructions.count  + 1). ")
            TextField("Description", text: $description)
                .frame(maxWidth: .infinity, alignment: .leading)
        }
        .font(.body)
        
        AddButton {
            instructions.append(Instruction(id: 0, order: Int32(instructions.count + 1), description: description))
            description = ""
        }.disabled(!isValid)
            .padding()
    }

As you can see, we no longer directly call onChanged methods in RecipeDetailView. Instead, we hid those in RecipeDetailState and used @Published to manipulate them from RecipeDetailView.

Now in RecipeDetailView replace

 
                RecipePlaceholderView(image: Binding(
                    get: { state.recipe?.localImage},
                    set: {
                        if let image = $0 {
                            state.viewModel.onImageChanged(image: image)
                        }
                    }
                )).padding(16)

with just

 
RecipePlaceholderView(image: $state.image).padding(16)

Replace

 
TextField("What is the name of this dish?", text: Binding(
    get: { state.recipe?.title ?? "" },
    set: { state.viewModel.onTitleChanged(title: $0) }
    ))
    .multilineTextAlignment(.center)
    .padding()

with

 
TextField("What is the name of this dish?", text: $state.title)
    .multilineTextAlignment(.center)
    .padding()

Then replace

 
EditIngredients(ingredients: state.recipe?.ingredients, viewModel: state.viewModel )

with

 
EditIngredients(ingredients: $state.ingredients)

And finally replace

 
EditInstructions(instructions: state.recipe?.instructions, viewModel: state.viewModel )

with

 
EditInstructions(instructions: $state.instructions)

As you can see, now the body looks leaner and you use binding in RecipeDetailState to save changes in the shared view model:

 
 var body: some View {
        ScrollView {
            if (state.recipe?.images.isEmpty == false){
                KFImage(URL(string: state.recipe?.images.first?.image ?? ""))
                    .resizable()
                    .frame(width: 200, height: 150)
            } else {
                RecipePlaceholderView(image: $state.image).padding(16)
            }
            
            if (recipeId != nil) {
                Text(state.recipe?.title ?? "")
                    .font(.headline)
                    .padding()
            } else {
                TextField("What is the name of this dish?", text: $state.title)
                    .multilineTextAlignment(.center)
                    .padding()
            }
            
            Text("Ingredients")
                .font(.subheadline)
            if (recipeId != nil) {
                Ingredients(ingredients: state.recipe?.ingredients)
            } else {
                EditIngredients(ingredients: $state.ingredients)
            }
            
            Text("Instructions")
                .font(.subheadline)
            if (recipeId != nil) {
                Instructions(instructions: state.recipe?.instructions)
            } else {
                EditInstructions(instructions: $state.instructions)
            }
            
            if (recipeId == nil) {
                Button("Save recipe") {
                    state.saveRecipe()
                }
                .buttonStyle(FilledButtonStyle())
                .padding()
            }
            
        }.padding()
    }

Build and run the app. It still works just like before:

pizza app

Step 27: Add a local database to KMM Android and iOS app with SQLDelight

Now that we basically have a functional app, we can adapt it to network emergencies - what if you’re baking pizza in a remote mountain hut and there’s no connection?

pizza

To make the app more persistent, we’ll equip it with a local database. For this purpose we’ll use SQLDelight library, which is based on SQLLite.

The following setup follows the official documentation:

“SQLDelight generates type-safe Kotlin APIs from SQL statements. It also provides a multiplatform implementation of the SQLite driver.”

Define the latest stable version of the plugin in Versions.kt:

 
const val SQL_DELIGHT = "1.5.3"

In object Common add the dependency:

 
const val SQLDELIGHT_PLUGIN = "com.squareup.sqldelight:gradle-plugin:$SQL_DELIGHT"

Then use it in project level build.gradle.kts:

 
   classpath(Versions.Common.SQLDELIGHT_PLUGIN)

Then add this plugin to other plugins in shared build.gradle.kts:

 
plugins {
   kotlin("multiplatform")
   kotlin("native.cocoapods")
   id("com.android.library")
   kotlin("plugin.serialization") version Versions.KOTLIN_VERSION
   id("com.squareup.sqldelight")
}

Now we need to setup database drivers, therefore we’ll add the following dependencies to Versions.kt:

In object Common:

 
const val SQLDELIGHT_DRIVER = "com.squareup.sqldelight:runtime:$SQL_DELIGHT"

In object Android:

 
const val SQLDELIGHT_DRIVER = "com.squareup.sqldelight:android-driver:$SQL_DELIGHT"

In object iOS:

 
const val SQLDELIGHT_DRIVER = "com.squareup.sqldelight:native-driver:$SQL_DELIGHT"

Go to shared build.gradle.kts. Add the dependencies in appropriate sourceSets:

commonMain:

 
implementation(Versions.Common.SQLDELIGHT_DRIVER)

androidMain:

 
implementation(Versions.Android.SQLDELIGHT_DRIVER)

iosMain:

 
implementation(Versions.iOS.SQLDELIGHT_DRIVER)

Sync your project.

Now we need to configure our database. We will create a database named PizzaDatabase with the package name dev.tutorial.kmpizza.db for the generated Kotlin classes. At the bottom of shared build.gradle.kts add:

 
sqldelight {
   database("KmpizzaDatabase") { // This will be the name of the generated database class.
       packageName = "dev.tutorial.kmpizza.db"
       sourceFolders = listOf("sqldelight")
    }
}

“SQLDelight provides multiple platform-specific implementations of the SQLite driver, so you should create it for each platform separately.”
So let’s use a suitable implementation for our platforms.
Go to shared CommonModule.kt and add a platformModule:

 
modules(
   apiModule,
   repositoryModule,
   viewModelModule,
   platformModule
)

Here we’ll specify the SqlDelight Driver. In shared di package create a new file PlatformModule.kt and add the following:

 
expect val platformModule: Module

Now add PlatformModule.kt to androidMain/dev/tutorial/kmpizza/di and PlatformModule.kt to iosMain/dev/tutorial/kmpizza/di, and write actual platform modules.

androidMain:

 
actual val platformModule = module {
   single<SqlDriver> {
       AndroidSqliteDriver(
           KmpizzaDatabase.Schema,
           get(),
           "kmpizza.db"
       )
   }
}

iosMain:

 
actual val platformModule = module {
   single<SqlDriver> { NativeSqliteDriver(KmpizzaDatabase.Schema, "kmpizza.db") }
}

Also add linking to sqlite in the ios project:

link sqlite ios

Add

 
@Suppress("NO_ACTUAL_FOR_EXPECT")

to suppress the warning for JVM module.

You may notice that KmpizzaDatabase is unresolved. That’s because it doesn’t exist yet - we need to create it first.

Create a package sqldelight in shared/src/commonMain.
There, create a package that corresponds to the package you specified earlier: dev/tutorial/kmpizza/db
In that package, create KmpizzaDatabase.sq file.
Here you’ll write down all your SQL queries.
In the end you’ll have the following structure:

db structure

Now let’s create our tables for recipes.
In KmpizzaDatabase.sq write the following:

 
import com.example.recipecollection.model.Ingredient;
import com.example.recipecollection.model.Instruction;
import com.example.recipecollection.model.RecipeImage;
import kotlin.collections.List;
 
CREATE TABLE Recipe (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
ingredients TEXT AS List<Ingredient> NOT NULL,
instructions TEXT AS List<Instruction> NOT NULL,
images TEXT AS List<RecipeImage> NOT NULL
);

You’ll see the IDE suggesting you to install the SQLDelight plugin - it’s a good idea.
The plugin will help you read SQL statements.

sql ide plugin

You still may see List as unresolved, but it’s just an IDE bug and will not affect the build process.

Sync and rebuild the app. Now you can import KmpizzaDatabase where it was previously unresolved.

import kmpizza database

For simplicity of the tutorial, we won’t create separate tables for Ingredients and Instructions. Instead, we’ll use custom types and columnAdapters to convert objects to text. In commonMain create a new folder local and add RecipesLocalSource.kt there. Don’t write anything into the class yet, just add these adapters to repositories on top and add the necessary imports from your project and kotlinx.serialization:

 
val listOfInstructionsAdapter = object : ColumnAdapter<List<Instruction>, String> {
   override fun decode(databaseValue: String) = if (databaseValue.isEmpty()) emptyList() else databaseValue.split("|").map { Json.decodeFromString<Instruction>(it) }
   override fun encode(value: List<Instruction>) = if (value.isEmpty()) "" else value.joinToString(separator = "|") { Json.encodeToString(it) }
}
 
val listOfIngredientsAdapter = object : ColumnAdapter<List<Ingredient>, String> {
   override fun decode(databaseValue: String) = if (databaseValue.isEmpty()) emptyList() else databaseValue.split("|").map { Json.decodeFromString<Ingredient>(it) }
   override fun encode(value: List<Ingredient>) = if (value.isEmpty()) "" else value.joinToString(separator = "|") { Json.encodeToString(it) }
}
 
val listOfRecipeImagesAdapter = object : ColumnAdapter<List<RecipeImage>, String> {
   override fun decode(databaseValue: String) = if (databaseValue.isEmpty()) emptyList() else databaseValue.split("|").map { Json.decodeFromString<RecipeImage>(it) }
   override fun encode(value: List<RecipeImage>) = if (value.isEmpty()) "" else value.joinToString(separator = "|") { Json.encodeToString(it) }
}
 
class RecipeLocalSource {
}

Now we can rebuild the project and you’ll find generated sqldelight files.
To use them, we’ll return to our CommonModule.kt and add a new module:

 
private val coreModule = module {
   single {
       KmpizzaDatabase(
           get(),
           Recipe.Adapter(
               instructionsAdapter = listOfInstructionsAdapter,
               ingredientsAdapter = listOfIngredientsAdapter,
               imagesAdapter = listOfRecipeImagesAdapter
           )
       )
   }
}

Of course, we’ll add it to modules too:

 
modules(
   apiModule,
   repositoryModule,
   viewModelModule,
   platformModule,
   coreModule
)

Remember about the RecipeLocalSource. Use the recipe local database there as parameter:

 
class RecipeLocalSource (private val dbRef: KmpizzaDatabase) {
}

And then in CommonModule.kt add the following to the top of repositoryModule:

 
factory { RecipeLocalSource(get()) }

Now it’s time to write some queries and see how we can use them in RecipeLocalSource.
Go back to RecipeDatabase.sq and add following SQL queries:

 
getAllRecipes:
SELECT * FROM Recipe;
 
getRecipeById:
SELECT * FROM Recipe WHERE id = ?;
 
insertOrReplaceRecipe:
INSERT OR REPLACE INTO Recipe (
                            id,
                            title,
                            ingredients,
                            instructions,
                            images)
                            VALUES(?, ?, ?, ?, ?);

Now let’s use them to retrieve data.
In RecipesLocalSource add following functions:

 
fun Recipe.mapToRecipeUiModel(): RecipeUiModel{ [1]
   return RecipeUiModel(
       id, title, ingredients, instructions, images
   )
}
 
fun getAllRecipes() : List<RecipeUiModel> = 
       dbRef.recipeDatabaseQueries [2]
           .getAllRecipes()
           .executeAsList()
           .map { it.mapToRecipeUiModel() }
 
 
fun getRecipeById(id: Long) : RecipeUiModel? =
   dbRef.recipeDatabaseQueries
           .getRecipeById(id)
           .executeAsOneOrNull()?.mapToRecipeUiModel()
 
 
fun insertOrReplaceRecipe(recipe: RecipeUiModel) {
   dbRef.recipeDatabaseQueries
       .insertOrReplaceRecipe(recipe.id, recipe.title, recipe.ingredients, recipe.instructions, recipe.images)
}

[1] This mapper function that will help you convert database Recipe to RecipeUiModel
[2] Use the generated query to get all recipes from the local database and convert them to RecipeUiModel
[3], [4] Similarly we can extract a recipe by id from the local database or insert a new recipe

Now we need to adjust the repository. The idea is to use remote source and back up with local source data.
Go to RecipeRepository and add recipeLocalSource below private val recipeRemoteSource

 
private val recipeLocalSource: RecipeLocalSource by inject()

Then adjust all the methods to use recipeLocalSource:

 
suspend fun postRecipe(recipe: RecipeUiModel): Long? {
   return try {
       val id = recipeRemoteSource.postRecipe(recipe.toRecipeRequest()) [1]
       id?.let {
           val entry = recipe.copy(id = it)
           recipeLocalSource.insertOrReplaceRecipe(entry) [2]
       }
       id
   } catch (e: Exception) {
       null
   }
}

[1] Here we try to save our new recipe remotely
[2] If we succeed, we should receive a recipe id from the api, then we can save the recipe locally

 
suspend fun getRecipes(): List<RecipeUiModel> {
   return try {
       val recipes = recipeRemoteSource.getRecipes().map {
           it.toRecipeUiModel()
       } [1]
       recipes.forEach {
           recipeLocalSource.insertOrReplaceRecipe(it)
       } [2]
       recipes
   } catch (e: Exception) {
       recipeLocalSource.getAllRecipes() [3]
   }
}

[1] Here we try to get all recipes from backend
[2] If we succeed, we want to save all the recipes that we received locally
[3] Otherwise we’ll just get what we have saved locally

 
suspend fun getRecipe(id: Long): RecipeUiModel? {
   return try {
       recipeRemoteSource.getRecipe(id).toRecipeUiModel()[1]
   } catch (e: Exception) {
       recipeLocalSource.getRecipeById(id)[2]
   }
}

[1] Try getting the recipe from remote source
[2] If it doesn’t work, then get the local backup

 
suspend fun postRecipeImage(recipeId: Long, imageFile: ImageFile) = recipeRemoteSource.postImage(recipeId, imageFile) [1]

[1] This request remains unchanged

Build and run the app. You’ll get an error:
Caused by: org.koin.core.error.NoBeanDefFoundException: |- No definition found for class:'android.content.Context'. Check your definitions!

This is because we forgot to add the androidContext when starting the app. Go to MainApp.kt and change initKoin {} to this:

 
initKoin {
   androidContext(this@MainApp)
}

Now turn off your internet connection and run the Android app again. You will still see that recipe in the recipe list.

android no connection

However, the app has just the basic database functionality now. You won’t be able to create a new recipe offline. If you’re curious, try adjusting RecipeRepository so that allows you more operations when offline.

Now build and run your iOS app. It behaves similarly.

ios no connection

See, we didn’t have to change anything in iOS at all - it all came with the changes in the shared code.
Isn’t it awesome?

Here’s a task for you: if you try opening one of the locally saved recipes without internet connection, you’ll notice that there is no image. Fix it by adding a “No Internet” placeholder instead of an empty space when the user can’t download the image.

Step 26: Post images to AWS from a KMM Android or iOS app

Now we’ll add pictures to our recipes.
We already implemented this feature on backend, so we can upload pictures with /recipes/{recipeId}/recipeImage endpoint.

If you forgot, this is the method for it:

 
private fun Routing.addRecipeImage(localSource: LocalSource) {...}

Let’s see how to implement it in the shared networking and then use it in Android and iOS apps.

Go to the shared commonMain/util folder and add File.kt.
Declare our expected ImageFile and a helper function to convert it to ByteArray:

 
expect class ImageFile
expect fun ImageFile.toByteArray(): ByteArray

Then just like before write actual functions for File.kt

androidMain:

 
import android.content.ContentResolver
import android.net.Uri
 
actual typealias ImageFile = ImageUri [1]
 
actual fun ImageFile.toByteArray() = contentResolver.openInputStream(uri)?.use { [2]
   it.readBytes()
} ?: throw IllegalStateException("Couldn't open inputStream $uri")
 
class ImageUri(val uri: Uri, val contentResolver: ContentResolver)
 
fun Uri.toImageFile(contentResolver: ContentResolver): ImageFile { [3]
   return ImageFile(this, contentResolver)
}

[1] Here we set a typealias for our expect class ImageFile, which in case of Android is ImageUri.
[2] Transform ImageFiles to ByteArray using contentResolver.
[3] Also use contentResolver functionality to view images.

Similarly for iOS we add File.kt in common/iosMain

iosMain:

 
actual typealias ImageFile = UIImage
 
actual fun ImageFile.toByteArray() = UIImagePNGRepresentation(this)?.toByteArray() ?: emptyArray<Byte>().toByteArray()
 
@OptIn(ExperimentalUnsignedTypes::class)
private fun NSData.toByteArray(): ByteArray = ByteArray(length.toInt()).apply {
   usePinned {
       memcpy(it.addressOf(0), bytes, length)
   }
}

You may see a warning, saying Expected function 'toByteArray' has no actual declaration in module kmpizza.shared for JVM.
Suppress this warning with the following annotation to the expect functions:

 
@Suppress("NO_ACTUAL_FOR_EXPECT")

Finally, we need to think of how to send the local image from the user’s gallery to the backend.
Extend the RecipeUiModel, so that it allows manipulations with ImageFiles.
To start with, add a localImage parameter:

 
data class RecipeUiModel (
   val title: String,
   val ingredients: List<Ingredient> = listOf(),
   val instructions: List<Instruction> = listOf(),
   val images: List<RecipeImage> = listOf(),
   val localImage: ImageFile? = null,
)

Go to RecipesApi and add the following network request:

 
suspend fun postImage(recipeId: Long, icon: ImageFile): Unit = client.submitFormWithBinaryData( [1]
   formData {
       appendInput(key = IMAGE_FILE_PART, headers = Headers.build { [2]
           append(HttpHeaders.ContentDisposition, "filename=${recipeId}_image") [3]
       }) {
           buildPacket { writeFully(icon.toByteArray()) }
       }
   }) {
   apiUrl("$RECIPES_BASE_URL/${recipeId}/recipeImage") [4]
}
   .body()

[1] Create the form data request
[2] Add const val IMAGE_FILE_PART = "image" to companion object
[3] For now we’re just uploading one image per recipe, therefore we pass no name to the function and save the image just with a generic name relevant to recipeId
[4] use the recipesApi to post the formData, just like we did using Postman

Now add this to RecipeRemoteSource:

 
suspend fun postImage(recipeId: Long, imageFile: ImageFile) = recipesApi.postImage(recipeId, imageFile)

Also adjust the RecipeRepository:

 
suspend fun postRecipeImage(recipeId: Long, imageFile: ImageFile) = recipeRemoteSource.postImage(recipeId, imageFile)

Finally, when uploading the recipe, right after we send the recipe data, we’ll attach an image to the recipe.
Go to RecipeDetailsViewModel and change saveRecipe() to:

 
fun saveRecipe() {
   coroutineScope.launch {
           recipe.value?.let { recipe ->
               if (recipe.title.isNotEmpty() && recipe.ingredients.isNotEmpty() && recipe.instructions.isNotEmpty()){ 
                   val result = recipeRepository.postRecipe(recipe)
                   val imageUploadRequest = recipe.localImage.let { image ->
                       async { [1]
                           result?.let { it ->
                               if (image != null) {
                                   recipeRepository.postRecipeImage(it, image)
                               }
                           }
                       }
                   }
                   imageUploadRequest.await() [2]
                   result?.let { _upload.value = true } [3]
               }
           }
   }
}

[1] If the recipe was uploaded successfully, post the recipe Image asynchronously
[2] Wait for the result of image upload
[3] If it was uploaded successfully, set the upload state value to true

Now let’s go back to our apps and adjust the UIs, starting with Android. In RecipeDetailsScreen before your Previews add:

 
private fun ActivityResultLauncher<String>.launchAsImageResult() = launch("image/*") [1]
 
@Composable
fun registerForGalleryResult(callback: (ImageFile) -> Unit) =
   (LocalContext.current as AppCompatActivity).contentResolver.let { contentResolver -> [2]
       rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
           uri?.toImageFile(contentResolver)?.let(callback) [3]
       }
   }

[1] Start the choose image from Gallery activity in Android
[2] Using contentResolver, get the chosen image
[3] Using our actual function, transform the local image to ImageFile, which can be sent to the backend

Above your Scaffold in RecipeDetailsScreen add a state value that will trigger the onImageChanged in the ViewModel:

 
val getImage = registerForGalleryResult(viewModel::onImageChanged)

In the shared RecipeDetailsViewModel add onImageChanged to the EditRecipeChangeListener:

  
interface EditRecipeChangeListener {
   fun onTitleChanged(title: String)
   fun onIngredientsChanged(ingredient: Ingredient)
   fun onInstructionsChanged(instruction: Instruction)
   fun onImageChanged(image: ImageFile)
}

And implement it in RecipeDetailsViewModel:

 
override fun onImageChanged(image: ImageFile) {
   _recipe.value = _recipe.value?.copy(localImage = image)
}

Now let’s go back to RecipeDetailsScreen. Here we can finally use getImage. First add it to PlaceholderImage signature. Also add an isEditable boolean to switch on the gallery functionality:

  
private fun PlaceholderImage(
    padding: PaddingValues,
    getImage: ActivityResultLauncher<String>,
    localImage: ImageFile?,
    isEditable: Boolean
)

And modify the PlaceholderImage, so that it changes from placeholder image to the chosen image from your library:

 
@Composable
private fun PlaceholderImage(
   padding: PaddingValues,
   getImage: ActivityResultLauncher<String>,
   localImage: ImageFile?,
   isEditable: Boolean
) {
   val placeholder = "https://m.media-amazon.com/images/I/413qxEF0QPL._AC_.jpg"
   Box(contentAlignment = Alignment.Center) {
      HeaderImage(image = localImage?.uri ?: placeholder, padding = padding)
       if (isEditable) {
           IconButton(
               modifier = Modifier
                   .clip(CircleShape)
                   .background(Color.Cyan),
               onClick = { getImage.launchAsImageResult() }
           ) {
               Icon(
                   imageVector = Icons.Default.Add,
                   contentDescription = "edit image",
                   modifier = Modifier
                       .size(64.dp),
                   tint = Color.Black
               )
           }
       }
   }
}

Also adjust the HeaderImage composable:

  
private fun HeaderImage(image: Any, padding: PaddingValues)

And use it in Scaffold:

  
recipe?.images?.let {
   if (it.isNotEmpty()) {
       HeaderImage(it[0].image, padding)
   } else {
       PlaceholderImage(padding, getImage, recipe?.localImage, recipeId == null)
   }
}

Build and run the app. You’ll see an error.

java.lang.ClassCastException: dev.tutorial.kmpizza.android.MainActivity cannot be cast to androidx.appcompat.app.AppCompatActivity

That’s because we need to make MainActivity an AppCompatActivity to use ActivityResultLauncher.
Make MainActivity extend AppCompatAcitivity instead of ActivityComponent:

  
MainActivity : AppCompatActivity()

Now if you run it again and try adding a new recipe, you’ll be able to choose an image from your gallery.

pizza on android

Now let’s do the same in our iOS project.

We can add an ImagePicker, which will help us to choose images from your phone library. You can read more about the image picker implementation here

To the utils group in the iOS project add a new file ImagePicker:

  
import SwiftUI
 
struct ImagePickerIOS: UIViewControllerRepresentable {
    @Environment(\.presentationMode) var presentationMode
    var onImageSelected: ((UIImage) -> ())
 
    func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePickerIOS>) -> UIImagePickerController {
        let picker = UIImagePickerController()
        picker.delegate = context.coordinator
        return picker
    }
 
    func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePickerIOS>) {
 
    }
 
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
 
    class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
        let parent: ImagePickerIOS
 
        init(_ parent: ImagePickerIOS) {
            self.parent = parent
        }
 
        func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
            if let uiImage = info[.originalImage] as? UIImage {
                parent.onImageSelected(uiImage)
            }
 
            parent.presentationMode.wrappedValue.dismiss()
        }
    }
}

And the adjust the code in RecipePlaceholderView:

   
struct RecipePlaceholderView: View {
    @Binding var image: UIImage? [1]
    @State private var showingImagePicker = false [2]
    
    var body: some View {
        ZStack{
            if let image = image {
                Image(uiImage: image) [3]
                    .resizable()
                    .frame(width: 200, height: 150)
            } else {
                KFImage(URL(string: "https://m.media-amazon.com/images/I/413qxEF0QPL._AC_.jpg")) [4]
                    .resizable()
                    .frame(width: 200, height: 150)
            }
            
            Image(systemName: "camera.circle.fill") [5]
                .font(.system(size: 28, weight: .light))
                .foregroundColor(Color.black)
                .frame(width: 50, height: 50)
                .background(Color.accentColor)
                .clipShape(Circle())
            
        }
        .sheet(isPresented: $showingImagePicker){ [6]
            ImagePickerIOS(onImageSelected: {
                self.image = $0
            })
        }
        .onTapGesture { [7]
            showingImagePicker.toggle()
        }
    }
}

[1] First add a Binding to hold the UIImage
[2] Also a state variable to signal whether the imagePicker button should be visible or not
[3] In the view body if there an image to show, then show it
[4] Otherwise, show a placeholder image
[5] Put an “Add image” button on top
[6] With the showingImagePicker state variable control whether the imagePicker should show
[7] And toggle the showingImagePicker by tapping the image button, thus opening the image picker

Also change the relevant code in RecipeDetailView body to:

  
  if (state.recipe?.images.isEmpty == false){
                KFImage(URL(string: state.recipe?.images.first?.image ?? "")) [1]
                    .resizable()
                    .frame(width: 200, height: 150)
            } else {
                RecipePlaceholderView(image: Binding(
                    get: { state.recipe?.localImage}, [2]
                    set: {
                        if let image = $0 {
                            state.viewModel.onImageChanged(image: image) [3]
                        }
                    }
                )).padding(16)
            }

[1] If there is an image in th recipe, show it
[2] Otherwise when the recipe is edited, show a placeholder image view with an add button layover
[3] And set the image in the view model once it’s changed in the app

Build and run the app.

pizza on ios

Now we can upload images from the phone! In the next step we’ll learn how to use a local database as a back up for when there’s no connection.

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.

pizza on android

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:

add image placeholder

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!

show images 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.

Step 24: Add a shared Log and navigate back on upload success in the iOS app

To investigate what’s happening in iOS when we want to save a new recipe let’s add a logging function to the shared module.

In commonMain util folder add a new file Log.kt with the following expect function:

 
expect val log: (String) -> Unit

Add corresponding actual functions to androidMain and iosMain, like we did before with CoroutineViewModel.

androidMain:

 
actual val log: (String) -> Unit = {
   Log.d("RecipesLog", it)
}

iosMain:

 
actual val log: (String) -> Unit = { println(it) }

Also add a missing actual function to jvmMain:

 
actual val log: (String) -> Unit = { }

Now add some log output to the saveRecipe function:

 
fun saveRecipe() {
   coroutineScope.launch {
           recipe.value?.let {
               if (it.title.isNotEmpty() && it.ingredients.isNotEmpty() && it.instructions.isNotEmpty()){
                   log(it.toString())
                   val result = recipeRepository.postRecipe(it)
                   log(result.toString())
                   result?.let { _upload.value = true }
               }
           }
   }
}

Make sure you imported the shared log function.
Build and run the Android app.
Try saving a new recipe, you’ll see:

 
D/RecipesLog: RecipeUiModel(id=0, title=Pie, ingredients=[Ingredient(id=0, name=Flour, amount=1.0, metric=Kg)], instructions=[Instruction(id=0, order=1, description=Mix all ingredients)])
D/RecipesLog: 10

Now do the same for iOS, you’ll see the following log output:

 
RecipeUiModel(id=0, title=Pie, ingredients=[Ingredient(id=null, name=Flour, amount=0.0, metric=Kg)], instructions=[Instruction(id=null, order=1, description=Mix all together)])
null

See the difference?
Right! We forgot that we use Long for our backend and shouldn’t be setting them to null, so let’s change it to 0 in AddButtons:

 
    viewModel.onIngredientsChanged(ingredient: Ingredient(id: 0, name: name, amount: amount ?? 0.0, metric: metric))
    viewModel.onInstructionsChanged(instruction: Instruction(id: 0, order: Int32((instructions?.count ?? 0) + 1), description: description))

Now run the app again…and it kind of works: if you start the app again and refresh the recipes list, you’ll see the new recipe!

However, we want to see the new recipe in the list once we save it.
We will fix it up later.

First let’s handle the upload value from the shared RecipeDetailViewModel, because we want to close the recipe detail view only after the request succeeded.

Change the upload variable in RecipeDetailState to KotlinBoolean?:

 
@Published private(set) var upload: KotlinBoolean? = nil

Add the following observe function in init():

 
viewModel.observeUpload{ upload in
            self.upload = upload
        }

Now we need to adjust the app’s behavior when Save Recipe button in clicked. Remove presentationMode.wrappedValue.dismiss() from the Save Button in RecipeDetailView, as well as Environment variable @Environment(\.presentationMode) var presentationModel.

In RecipesView add

 
@State var isRecipeDetailsShown: Bool = false

We’ll bind the isRecipeDetailsShown with the RecipeDetailsView, so that isRecipeDetailsShown is manipulated within the RecipeDetailsScreen and receives the result of the upload variable from the shared view model.

Add to the RecipeDetailState the following variable:

 
@Binding var isPresented: Bool

Change init():

 
 init(recipeId: KotlinLong?, isPresented: Binding<Bool>) {
        self.recipeId = recipeId
        self._isPresented = isPresented
        
        viewModel = RecipeDetailsViewModel(id: recipeId)
        
        viewModel.observeRecipe { recipe in
            self.recipe = recipe
        }
    }

And move the observeUpload() to saveRecipe():

 
 func saveRecipe(){
        viewModel.saveRecipe()
        
        viewModel.observeUpload { upload in
            self.upload = upload
            if ((self.upload?.boolValue ?? false) == true){
                self.isPresented = false
            }
        }
    }

Also change RecipeDetailView by passing the isPresented binding further to the state:

 
  init(id: KotlinLong?, isPresented: Binding<Bool>) {
        self.recipeId = id
        state = RecipeDetailState(recipeId: id, isPresented: isPresented)
    }

The body in RecipeDetailView should trigger saveRecipe():

 
 if (recipeId == nil) {
                Button("Save recipe") {
                    state.saveRecipe()
                }
                .buttonStyle(FilledButtonStyle())
                .padding()
            }

Then in RecipesView we’ll pass this state variable to our RecipeDetailsView like this.

 
 NavigationLink(destination: RecipeDetailView (id: recipe.id.toKotlinLong(), isPresented: self.$isRecipeDetailsShown)) {
                    RecipeView(item: recipe)
                }

Finally change the FloatingActionButton, so that we can signal when it’s pressed and the RecipeDetailsView is shown or not.

struct FloatingActionButton: View {
    @Binding var isPresented: Bool
    
    var body: some View {
        NavigationLink(destination: RecipeDetailView (id: nil, isPresented: $isPresented), isActive: $isPresented) {
            Image(systemName: "plus.circle.fill")
                .resizable()
                .frame(width: 56, height: 56)
                .foregroundColor(Color.accentColor)
                .shadow(color: .gray, radius: 0.2, x: 1, y: 1)
        }
        .simultaneousGesture(
            TapGesture().onEnded {
                isPresented = true
            })
    }
}

Don’t forget to pass the value to FloatingActionButton in RecipesView body:

 
FloatingActionButton(isPresented: self.$isRecipeDetailsShown)

Now if you test the app your RecipeDetailView will close only after it receives upload == true from the shared view model.

However, the recipe list still doesn’t update when we go back.
Let’s solve this problem now.
Open RecipeDetailState and add another variable just like you did with isPresented:

 
    @Binding var uploadSuccess: Bool

In init add uploadSuccess: Binding<Bool> to the signature and initiate with

 
        self._uploadSuccess = uploadSuccess

In saveRecipe below self.isPresented = false add self.uploadSuccess = true

In RecipeDetailView change the init section:

 
init(id: KotlinLong?, isPresented: Binding<Bool>, uploadSuccess: Binding<Bool> ) {
        self.recipeId = id
        state = RecipeDetailState(recipeId: id, isPresented: isPresented, uploadSuccess: uploadSuccess)
    }

Finally, in RecipesView add

 
@State var uploadSuccess: Bool = false 

and pass it to RecipeDetailView where necessary:

 
NavigationLink(destination: RecipeDetailView (id: recipe.id as? KotlinLong, isPresented: self.$isRecipeDetailsShown, uploadSuccess: self.$uploadSuccess)) {
        RecipeView(item: recipe)
    }
 FloatingActionButton(isPresented: self.$isRecipeDetailsShown, uploadSuccess: self.$uploadSuccess)
 

Also adjust FloatingActionButton struct:

 
NavigationLink(destination: RecipeDetailView (id: nil, isPresented: $isPresented, uploadSuccess: self.$uploadSuccess), isActive: $isPresented)
 

Add now an onAppear callback to NavigationLink in RecipesView:

 
    .onAppear {
        if uploadSuccess {
            state.viewModel.getRecipes()
        }

This will make sure that the shared view model gets the latest recipe list from the api and saves it in Kotlin Flow, which in turn passes it to Swift UI.

In the next step we’ll learn how to show images from our backend in the KMM Android and iOS apps.

Step 23: Create editable Recipe Details View in Swift UI and bind to the shared View Model

For a Floating Action Button add the following struct in RecipesView:

 
struct FloatingActionButton: View {
    var body: some View {
        Button(action: {
            print("Add new recipe") [1]
        }) {
            Image(systemName: "plus.circle.fill") [2]
                .resizable()
                .frame(width: 56, height: 56)
                .foregroundColor(Color.accentColor)
                .shadow(color: .gray, radius: 0.2, x: 1, y: 1)
        }
    } 
}

[1] This closure defines an action that gets executed when the user clicks the button, for now it’s just printing in log output
[2] Here we describe how the button looks

Then use it in RecipesView like this:

 
var body: some View {
        ZStack(alignment: .bottomTrailing) { [1]
            List(state.recipes, id: \.id) { recipe in
                NavigationLink(destination: RecipeDetailView (id: recipe.id.toKotlinLong())) {
                    RecipeView(item: recipe)
                    
                }
            }
            .listStyle(PlainListStyle())
            FloatingActionButton() [2]
                .padding()
                .frame(alignment: .bottomTrailing)
        }
    }

[1] We’re using ZStack to place the FloatingActionButton over the Recipe List
[2] Use the FloatingActionButton struct we created before

Build and run the app.

Big blue add recipe button

Click on the button and you’ll see Add new recipe printed in the log.
We need to change this to actually be able to add a recipe.
To do so, replace the Button with NavigationLink

 
struct FloatingActionButton: View {
    var body: some View {
        NavigationLink(destination: RecipeDetailView (id: nil)) {
            Image(systemName: "plus.circle.fill")
                .resizable()
                .frame(width: 70, height: 70)
                .foregroundColor(Color.accentColor)
                .shadow(color: .gray, radius: 0.2, x: 1, y: 1)
        }
    }
}

[1] Instead of using a Button that prints a log we’ll navigate to RecipeDetailView

In the RecipeDetailView change the title item so that if we already have a recipe (with recipeId), it displays a title, otherwise it displays a text field:

 
ScrollView {
            KFImage(URL(string: "https://m.media-amazon.com/images/I/413qxEF0QPL._AC_.jpg"))
                .resizable()
                .frame(width: 200, height: 150)
            
            if (recipeId != nil) {
                Text(state.recipe?.title ?? "")
                    .font(.headline)
                    .padding()
            } else {
                TextField("What is the name of this dish?", text: Binding(
                    get: { state.recipe?.title ?? "" },
                    set: { state.viewModel.onTitleChanged(title: $0) }
                ))
                .multilineTextAlignment(.center)
                .padding()
            }
. . .
}

[1] If it’s an old recipe with recipeId, display the title
[2] Otherwise create an input field
[3] The title gets updated according to the recipe title from the shared RecipeDetailsViewModel
[4] when the user enters a new title the value in the shared model is updated via onTitleChanged()

You’ll also need a plus button to add more ingredients or instructions like the one we had in Android.
Add this struct below the RecipeDetailView struct:

 
struct AddButton: View {
    let action: () -> Void
    
    var body: some View {
        Button(action: action) {
            Image(systemName: "plus.circle.fill")
                .resizable()
                .frame(width: 30, height: 30)
                .foregroundColor(Color.accentColor)
                .shadow(color: .gray, radius: 0.2, x: 1, y: 1)
        }.padding()
    }  
}

Add other structs for the UI, starting with the Ingredients section.
This View shows a simple list of ingredients with respective names, amounts and metric and arranges them in a similar way like in Android:

 
struct Ingredients: View {
    
    var ingredients: [Ingredient]?
    
    var body: some View {
        LazyVStack {
            ForEach(Array(ingredients?.enumerated() ?? [].enumerated()), id: \.offset) { index, ingredient in
                HStack {
                    Text(ingredient.name)
                        .font(.body)
                        .frame(maxWidth: .infinity, alignment: .leading)
                    
                    HStack {
                        Text("\(ingredient.amount, specifier: "%.1f")")
                        Text(ingredient.metric)
                            .font(.body)
                    }
                }
                
            })
        }
        .padding()
    }
}

[1] Here we can’t use id to build a LazyVStack, because all of them has the same id = nil, that’s why we are using enumerated() as workaround to display the ingredients. Otherwise you’ll get an error: LazyVStackLayout: the ID nil is used by multiple child views, this will give undefined results!

The Instructions section is similar to the Ingredients section:

 
struct Instructions: View {
    
    var instructions: [Instruction]? [1]
    
    var body: some View {
        LazyVStack {
            ForEach(instructions ?? [], id: \.self, content: { instruction in [1]
                HStack {
                    Text("\(instruction.order). ")
                        .font(.body)
                    Text(instruction.description_)
                        .font(.body)
                        .frame(maxWidth: .infinity, alignment: .leading)
                }
                .padding()
                
            })
        }
    }   
}

[1] Using \.self is another way to avoid the LazyVStackLayout error, but for instructions we could also use \.order, because every order is unique

Previous structs were not editable, but we also want the user to be able to add or edit recipes in our RecipeDetailsView:

 
struct EditIngredients: View {
    
    var ingredients: [Ingredient]?
    var viewModel: RecipeDetailsViewModel [1]
    
    @State private var name: String = "" [2]
    @State private var amountString: String = ""
    @State private var metric: String = ""
    
    private let formatter = NumberFormatter()
    
    private var amount: Double { [3]
        return formatter.number(from: amountString)?.doubleValue ?? 0
    }
    
    private var isValid: Bool {
        return name != "" && amount > 0 && metric != ""
    }
      
    var body: some View {
        Ingredients(ingredients: ingredients)
        
        HStack {
            TextField("Ingredient", text: $name)
                .frame(maxWidth: .infinity, alignment: .leading)
            
            HStack {
                TextField("Amount", text: $amountString)
                TextField("Metric", text: $metric)
            }
        }
        .font(.body)
        
        AddButton(action: {
            viewModel.onIngredientsChanged(ingredient: Ingredient(id: nil, name: name, amount: amount, metric: metric)) [4]
            name = ""
            amountString = ""
            metric = ""
        })
        .padding()
    }
}

[1] This creates an instance of the shared RecipeDetailsViewModel, so that we can use onIngredietnsChanged() function from it
[2] Name, amountString and metric are editable states
[3] In Swift Optional and NumberFormatter() don’t get along well, therefore we need cusotm logic to bind values to TextField, because we don’t want to see anything but a hint in the Amount TextField if amount==nil
[4] Here we only care about updating the RecipeDetailsViewModel after the user clicks the Add (Ingredient) Button again: then the current state of the previous ingredient is refreshed in the RecipeDetailsViewModel

We use the same principle for EditInstructions:

struct EditInstructions: View {
    
    var instructions: [Instruction]?
    var viewModel: RecipeDetailsViewModel
    
    @State private var description: String = ""
    
    
    var body: some View {
        
        Instructions(instructions: instructions)
        
        HStack {
            Text ("\((instructions?.count ?? 0) + 1). ")
                .font(.body)
            TextField("Description", text: $description)
                .font(.body)
                .frame(maxWidth: .infinity, alignment: .leading)
    
        }
        
        
        AddButton(action: {
            viewModel.onInstructionsChanged(instruction: Instruction(id: nil, order: Int32((instructions?.count ?? 0) + 1), description: description))
            description = ""
        })
        
            .padding() 
    } 
}

Then simply add all these components to the RecipeDetailView body:

 
 var body: some View {
        ScrollView {
            KFImage(URL(string: "https://m.media-amazon.com/images/I/413qxEF0QPL._AC_.jpg"))
                .resizable()
                .frame(width: 200, height: 150)

            if (recipeId != nil) {
                Text(state.recipe?.title ?? "")
                    .font(.headline)
            } else {
                TextField("What is the name of this dish?", text: Binding(
                    get: { state.recipe?.title ?? "" },
                    set: { state.viewModel.onTitleChanged(title: $0) }
                )).multilineTextAlignment(.center)
            }
            
            Text("Ingredients") [1]
                .font(.subheadline)
            if (recipeId != nil) {
                Ingredients(ingredients: state.recipe?.ingredients)
            } else {
                EditIngredients(ingredients: state.recipe?.ingredients, viewModel: state.viewModel )
            }
            
            Text("Instructions") [2]
                .font(.subheadline)
            if (recipeId != nil) {
                Instructions(instructions: state.recipe?.instructions)
            } else {
                EditInstructions(instructions: state.recipe?.instructions, viewModel: state.viewModel )
            }
        }
    }

[1] If there’s already a recipeId, show the recipe details, otherwise use the editable struct
[2] Same for the instructions section

Build and run the app. If you choose a recipe from the list you’ll see the details view as before:

Old recipe details screen

If you click on the big blue add recipe button you’ll see the empty add recipe screen:

Add new recipe details screen

With this setup we can either view the recipe details OR add a new one, meaning we can’t edit an existing recipe. But that’s enough for now, we can adjust this behaviour later.

Last but not least, we need a button to save the new recipe.
Add saveRecipe() function to RecipeDetailState to leverage the method from the shared RecipeDetailsViewModel:

 
func saveRecipe(){
        viewModel.saveRecipe()
    }

Create a style for the submit button:

 
struct FilledButtonStyle: ButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
        configuration
            .label
            .font(.title3)
            .foregroundColor(.white)
            .padding(.horizontal)
            .padding(.vertical, 12)
            .frame(maxWidth: .infinity)
            .background(configuration.isPressed ? Color.blue.opacity(0.3) : Color.blue)
            .cornerRadius(25)
    }
}

Then place it at the end of the body inside RecipeDetailView:

 
if (recipeId == nil) {
                Button("Save recipe") {
                    state.saveRecipe()
                }
                .buttonStyle(FilledButtonStyle())
                .padding()
            }

Build and run the app. Here it is, our saveRecipe() button:

Add new recipe details screen

We want the RecipeDetailsView to close automatically once the user saves the recipe. Add to the top of RecipeDetailView struct:

 
@Environment(\.presentationMode) var presentationMode

Use it to close the view when clicking on save Button (this will later be enhanced with upload flow):

 
        Button("Save recipe") {
            state.saveRecipe()
            presentationMode.wrappedValue.dismiss()
        }

Build and run the iOS app.
Now we can fill out the form and press the save button, but there’s no new recipe in the list…Why?
We’ll investigate it next time.

Step 22: Bind iOS Swift UI to shared RecipeDetail ViewModel and add Navigation

Now let’s add a new Swift UI Recipe Details screen and Navigation to it.
Create recipeDetail folder and add RecipeDetailState.swift and RecipeDetailView.swift files:

create recipe detail state and view files

Add Navigation to ContentView:

 
        NavigationView {
            VStack {
                RecipesView()
            }
        }

Then, add a navigation link to the recipes list in RecipesView.swift like this:

 
List(state.recipes, id: \.id) { recipe in
            NavigationLink(destination: RecipeDetailView (id: KotlinLong.init(value: recipe.id))) { [1]
                    RecipeView(item: recipe)
               
            }
        }

[1] Here we’re transforming Swift Int64? To KotlinLong?, which is a Kotlin Native type of recipeId

To make it neater, let’s write an extension. Create a separate Extensions.swift in a utils group:

add toKotlinLong() extension
 
import shared
 
extension Int64 {
    func toKotlinLong() -> KotlinLong {
        return KotlinLong(value: self)
    }
}

Now you can use this extension instead of KotlinLong.init(value: recipe.id)):

 
NavigationLink(destination: RecipeDetailView (id: recipe.id.toKotlinLong())) {
                    RecipeView(item: recipe)
               
            }

Populate the RecipeDetailState.swift:

 
import SwiftUI
import Foundation
import shared
 
class RecipeDetailState: ObservableObject{
    
    let recipeId: KotlinLong? [1]
    let viewModel: RecipeDetailsViewModel [2]
    
    @Published private(set) var recipe: RecipeUiModel? = nil [3]
    @Published private(set) var upload: Bool? = nil
    
    init(recipeId: KotlinLong?) {
        self.recipeId = recipeId
        viewModel = RecipeDetailsViewModel(id: recipeId)
        
        viewModel.observeRecipe { recipe in
            self.recipe = recipe
        }
    }
    
    deinit {
        viewModel.dispose()
    }
}

[1] We will use KotlinLong for our recipe ids
[2] The shared RecipeDetailsViewModel will hold the data and deliver it to iOS through Kotlin flows, just like it did before in the recipes list view model
[3] In this @Published recipe property we’ll store the recipe details received through the shared view model.

Finally, add this to the RecipeDetailView.swift:

 
import SwiftUI
import shared
import Kingfisher
 
struct RecipeDetailView: View {
    
    let recipeId: KotlinLong?
    @ObservedObject var state: RecipeDetailState
    
    init(id: KotlinLong?) {
        self.recipeId = id
        state = RecipeDetailState(recipeId: id)
    }
    
    var body: some View {
        ScrollView {
            LazyVStack {
                ForEach(state.recipe?.ingredients ?? [], id: \.id, content: { ingredient in
                    Text(ingredient.name)
                        .font(.body)
                })
            }
        }
        
    }
    
}

[1] The observable RecipeDetailState will hold the data for the RecipeDetailView
[2] To begin with, we are just displaying a list of ingredients to make sure everything works

Run the App now and try navigation to the pizza recipe. You’ll see the list of ingredients.

add toKotlinLong() extension

Now add other views to the recipe detail screen:

 
var body: some View {
        ScrollView { [1]
            Text(state.recipe?.title ?? "")
                .font(.headline)
           KFImage(URL(string: "https://m.media-amazon.com/images/I/413qxEF0QPL._AC_.jpg"))
                .resizable()
                .frame(width: 200, height: 150)
                .padding(.trailing)
            
            Text("Ingredients") [3]
                .font(.subheadline)
            LazyVStack {
                ForEach(state.recipe?.ingredients ?? [], id: \.id, content: { ingredient in
                    HStack {
                        Text(ingredient.name)
                            .font(.body)
                            .frame(maxWidth: .infinity, alignment: .leading)
                        
                        HStack {
                            Text("\(ingredient.amount, specifier: "%.1f")")
                            Text(ingredient.metric)
                                .font(.body)
                        }
                    }
                    
                })
            }
            .padding()
            
            Text("Instructions") [4]
                .font(.subheadline)
            LazyVStack {
                ForEach(state.recipe?.instructions ?? [], id: \.id, content: { instruction in
                    HStack {
                        Text("\(instruction.order). ")
                            .font(.body)
                        Text(instruction.description_)
                            .font(.body)
                            .frame(maxWidth: .infinity, alignment: .leading)
                    }
                    .padding()
                    
                })
            }
        }
        
    }

[1] Use ScrollView to make RecipeDetailsView scrollable in case it has to many ingredients or instructions
[2] Add a recipe title image with Kingfisher image library
[3] Add a section with ingredients’ names and measurements
[4] Add a section with instructions

Run the app again, go to recipe detail view and you’ll see, it looks much better now.

add toKotlinLong() extension

Our next step will be: adding a floating button and edit recipe functionality.

Step 21: Add images to iOS app with Kingfisher image library

In Step 15 we started creating our iOS App Swift UI.
So far it has only a main view with the list of recipes.
However, if you try running the app, it’ll crash with an error.

That’s because we need to use a special version of coroutines library (native-mt).
Add the following dependency to your project. In Versions.kt:

  
const val COROUTINES_MT = "1.6.3-native-mt"

In Common object:

 
const val COROUTINES = "org.jetbrains.kotlinx:kotlinx-coroutines-core:$COROUTINES_MT"

In shared build.gradle.kts within the commonMain sourceSet:

 
// Coroutines
implementation(Versions.Common.COROUTINES) {
   version {
       strictly(Versions.COROUTINES_MT)
   }
}

Also add the following to your project gradle.properties:

 
kotlin.native.binary.memoryModel=experimental
kotlin.native.binary.freezing=disabled

These settings will enable the new memory model management.
Run the app again and you’ll see a list of recipe titles.

Now let’s make our iOS app look more like our Android app by adding other cool features.

First, add an image loading library.

add packages menu
add package to iosApp

You have a new package dependency now

new package dependency

Add a new file to the recipes folder: RecipeView.
Here we’ll describe how we want one Recipe to look like in the list of recipes.

 
import SwiftUI
import shared
import struct Kingfisher.KFImage [1]
 
struct RecipeView: View {
    
    var item: RecipeResponse
    
    var body: some View {
        HStack(alignment: .center) {
            KFImage(URL(string: "https://m.media-amazon.com/images/I/413qxEF0QPL._AC_.jpg"))
                .resizable()
                .frame(width: 64, height: 64, alignment: .center)
                .cornerRadius(8)
                .padding(.trailing)
            VStack(alignment: .leading) {
                Text(item.title)
                    .font(.body)
            }
        }
    }
}

[1] Don’t forget to import KFImage from the Kingfisher imaging library, for now we’ll use it with a hardcoded placeholder image
[2] The recipe view will hold Recipe as an entity
[3] HStack is like a Row and VStack is like a Column in Jetpack Compose.

Now you can replace the Text(recipe.title) in RecipesView with the following:

 
   RecipeView(item: recipe)

As a result, you should be able to see a nice list with image placeholders for every recipe.

ios recipes list with images

In the next step we’ll proceed with RecipeDetailView and add Navigation to our iOS App.

Step 20: Connect shared RecipeDetails ViewModel to Android Compose UI

Now that we have a shared RecipeDetailsViewModel it’s time to build the UI, and we’ll start with Android.

First, add some values to strings.xml in res/values folder:

 
<resources>
   <string name="name">Name</string>
   <string name="amount">Amount</string>
   <string name="metric">Metric</string>
   <string name="description">Description</string>
   <string name="save">Save</string>
   <string name="title">What is the name of this dish?</string>
</resources>

In RecipeDetailsScreen.kt use the new RecipeDetailsViewModel:

 
   val viewModel = remember { RecipeDetailsViewModel(recipeId) }
   val recipe by viewModel.recipe.collectAsState()

Add an upload state and react to its changes:

 
val upload by viewModel.upload.collectAsState()

if (upload){
   upPress()
   viewModel.resetUpload()
}

Adjust the Scaffold to have a scrolling state and add Edit fields in case when the recipeId is null (i.e. we want to add a new recipe):

 
val scrolling = Modifier.verticalScroll(rememberScrollState())

Scaffold(
   topBar = { TopBar(upPress = upPress) })
{
   Column(
       modifier = scrolling.padding(8.dp),
       horizontalAlignment = Alignment.CenterHorizontally
   ) {
       HeaderImage(image = placeholder)
       recipeId?.let { recipe?.title?.let { Title(it) } } ?: EditTitle(changeListener = viewModel, title = recipe?.title)
       SectionHeader(title = "Ingredients")
       recipeId?.let { recipe?.ingredients?.let { Ingredients(it)} } ?: EditIngredients(changeListener = viewModel, items = recipe?.ingredients)
       SectionHeader(title = "Instructions")
       recipeId?.let { recipe?.instructions?.let { Instructions(it) }} ?: EditInstructions(changeListener = viewModel, items = recipe?.instructions)
       if (recipeId == null){
           SubmitButton {
               viewModel.saveRecipe()
           }
       }
   }
}

With the Submit button we’ll send the recipe data to backend:

 
@Composable
fun SubmitButton(
   onClick: () -> Unit){
   ExtendedFloatingActionButton(
       text = { Text(text = stringResource(id = R.string.save))},
       shape = RoundedCornerShape(51),
       onClick = onClick,
       modifier = Modifier
           .fillMaxWidth()
           .padding(vertical = 4.dp)
   )
}

These new “edit recipe” components control user interactions with the input fields and pass the appropriate changes to the view model.
Here’s an example of EditInstructions:

 
@Composable
private fun EditInstructions(changeListener: EditRecipeChangeListener, items: List<Instruction>?) {
   var isEdited by remember { mutableStateOf(false) } [1]
   var description by remember { mutableStateOf("") } [2]

   val onDescriptionChanged = { value: String ->
       description = value
   }

   val onInstructionAdded = {
       isEdited = true
       if (description.isNotEmpty()){
           changeListener.onInstructionsChanged(Instruction(order = items?.size?.plus(1) ?: 1, description = description))
           description = ""
       }
   } [3]

  Instructions(items = items) [4]
   if (isEdited){
       NewInstruction(description = description, onDescriptionChanged = onDescriptionChanged)
   } [5]
   AddItemButton(onAddInstruction = onInstructionAdded) [6]
}

[1] Keep track of whether the Instructions section is being edited
[2] Holds the current state of the instruction description, which is manipulated by the user
[3] The onInstructionAdded callback follows the AddItemButton click and activates a new description field tor user input. When the user clicks the button again it adds the previous instruction to the list of instructions for this recipe via viewModel.onInstructionsChanged()
[4] Shows all the instructions that have already been added
[5] Under all the added instructions we have a field for user input, which is shown when isEdited was triggered
[6] At the bottom of the section there’s an AddItemButton which allows the user to add a new instruction

 
@Composable
private fun AddItemButton(onAddInstruction: () -> Unit = {}) {
   IconButton(onClick = onAddInstruction) {
       Icon(
           painter = rememberAsyncImagePainter(R.drawable.ic_add),
           contentDescription = null,
           modifier = Modifier.clip(CircleShape))
   }
}

Check the repository for the full code and other components like EditTitle, EditIngredients and others.
KMPizza Repo

Once your UI is ready, run the project.
You’ll encounter an error saying that you received an unexpected variable in your JSON Response. That’s because you have images in the json RecipeResponse, but the Recipe entity on the app side doesn’t.

Let’s fix it by temporarily changing the types. This way we’ll avoid errors with images, which we’ll add later on to the backend.

 
internal class RecipeRemoteSource(
   private val recipesApi: RecipesApi
) {

   suspend fun getRecipes() = recipesApi.getRecipes().map { it.toRecipe() }
   suspend fun getRecipe(id: Long) = recipesApi.getRecipe(id).toRecipe()
   suspend fun postRecipe(recipe: Recipe) = recipesApi.postRecipe(recipe)
}

fun RecipeResponse.toRecipe() = Recipe (id = id, title = title, ingredients = ingredients, instructions = instructions)

Now it’s fixed and the Android UI is ready.
In the next step we’ll move on to playing with Swift UI.

Step 19: Add a shared Recipe Details View Model

Now let’s create a proper ViewModel for the RecipeDetails screen. Keep in mind that we’ll also reuse this ViewModel to add new recipes afterwards.

First in the shared module in the model folder add an RecipeUiModel, which we’ll use as an interface between shared and UI layer:

 
data class RecipeUiModel (
   val id: Long = 0,
   val title: String,
   val ingredients: List<Ingredient> = listOf(),
   val instructions: List<Instruction> = listOf()
)

Go to shared -> remote -> RecipesRemoteSource.kt and add getRecipe():

 
suspend fun getRecipe(id: Long) = recipesApi.getRecipe(id)

And use it in RecipeRepository:

 
suspend fun getRecipe(id: Long) : RecipeResponse = recipeRemoteSource.getRecipe(id)

Next, we will adjust our models so that we can use them for our UI as well.
In the best case scenario you want to create UiModels and use them to display data, but in this tutorial we’ll just make id nullable for our shared models (except RecipeResponse, because it cannot arrive without id).
Also rename Recipe to RecipeRequest for clarity:

 
@Serializable
data class RecipeRequest (
   val id: Long? = 0,
   val title: String,
   val ingredients: List<Ingredient>,
   val instructions: List<Instruction>
)

@Serializable
data class Ingredient(
   val id: Long? = 0,
   val name: String,
   val amount: Double,
   val metric: String

)
@Serializable
data class Instruction(
   val id: Long? = 0,
   val order: Int,
   var description: String,
)

Then create helper functions to transform your RecipeResponse to RecipeUiModel and back:

 
fun RecipeResponse.toRecipeUiModel() = RecipeUiModel(
   id = id,
   title = title,
   ingredients = ingredients,
   instructions = instructions
)

fun RecipeUiModel.toRecipeRequest() = RecipeRequest(
   id = id,
   title = title,
   ingredients = ingredients,
   instructions = instructions
)

Now in shared->viewmodel folder add RecipeDetailsViewModel.kt.
Here create an EditRecipeChangeListener to help the ViewModel observe the changes in the user interface:

 
interface EditRecipeChangeListener { [1]
   fun onTitleChanged(title: String)
   fun onIngredientsChanged(ingredient: Ingredient)
   fun onInstructionsChanged(instruction: Instruction)
}

And finally add the RecipeDetailsViewModel itself:

 
class RecipeDetailsViewModel(private val id: Long?) : CoroutineViewModel(), KoinComponent, EditRecipeChangeListener { [2]
   private val recipeRepository: RecipeRepository by inject()

   private val _recipe = MutableStateFlow(EditRecipeUiModel()) [3]
   val recipe: StateFlow<EditRecipeUiModel> = _recipe

   init {
      id?.let { getRecipe(it) } [4]
   }

   fun getRecipe(id: Long) {
   coroutineScope.launch {
       _recipe.value = recipeRepository.getRecipe(id).toRecipeUiModel()[5]
   }
}


   @Suppress("unused")
fun observeRecipe(onChange: (RecipeUiModel?) -> Unit) { [6]
   recipe.onEach {
       onChange(it)
   }.launchIn(coroutineScope)
}


  override fun onTitleChanged(title: String) { [7]
   _recipe.value = _recipe.value?.copy(title = title)

}

override fun onIngredientsChanged(ingredient: Ingredient) {
   val ingredients = _recipe.value?.ingredients
   _recipe.value = _recipe.value?.copy(ingredients = ingredients?.plus(ingredient) ?: listOf(ingredient))

}

override fun onInstructionsChanged(instruction: Instruction) {
   val instructions = _recipe.value?.instructions
   _recipe.value = _recipe.value?.copy(instructions = instructions?.plus(instruction) ?: listOf(instruction))

}

[1] We created an interface to react to UI changes
[2] RecipeDetailsViewModel implements this interface
[3] The ViewModel also holds the mutable state flow of the recipe, which we’ll display on the platform side
[4] If we have a recipeId, we’ll load it from our backend, otherwise start with an empty new recipe
[5] Transform RecipeResponse to RecipeUiModel
[6] This method just like in RecipesViewModel will be used to observe the recipe flow from iOS
[7] These overridden methods belong to EditRecipeChangeListener interface. Here you modify the data in the Recipe StateFlow, which passes the newest data to the UI

To edit or create a recipe add an appropriate function in shared -> RecipesApi:

 
suspend fun postRecipe(recipe: Recipe): Long? {
   try{
       return client.post {
           json()
           apiUrl(RECIPES_BASE_URL)
           setBody(body)
       }.body()
   } catch (e: Exception){
       return null
   }
}

Use it in RecipesRemoteSource:

 
suspend fun postRecipe(recipe: Recipe) = recipesApi.postRecipe(recipe)

Add this to RecipeRepository:

 
suspend fun postRecipe(recipe: Recipe): Long?  = recipeRemoteSource.postRecipe(recipe)

In RecipeDetailsViewModel:

 
fun saveRecipe() {
   coroutineScope.launch {
       recipe.value?.let {
           if (it.title.isNotEmpty() && it.ingredients.isNotEmpty() && it.instructions.isNotEmpty()){
               val result = recipeRepository.postRecipe(it.toRecipeRequest())[1]
               result?.let { _upload.value = true } [2]
           }
       }
   }
}

[1] Transform RecipeUiModel to RecipeRequest
[2] Use the result value to send a signal back to the UI

Here we also add the upload lever to know when the new recipe was successfully uploaded, so that we can go back to the recipe list.
Add the following to the RecipeDetailsViewModel:

 
private val _upload = MutableStateFlow<Boolean>(false)
val upload: StateFlow<Boolean> = _upload

@Suppress("unused")
fun observeUpload(onChange: (Boolean?) -> Unit) {
   upload.onEach {
       onChange(it)
   }.launchIn(coroutineScope)
}

fun resetUpload(){
   _upload.value = false
}

The Recipe Details View Model is ready!
Now we can add new recipes to the database!
In the next step we’ll connect this ViewModel to our Android Compose UI.

Step 18: Add a TopBar and a Floating Action Button to the Jetpack Compose UI

Now we can navigate from one screen to another, but we can’t go back, because there’s no TopBar navigation.
Let’s set it up.

First, create a TopBar composable in the utils folder.

create topbar

It will receive an upPress callback and navigate back when the button is pressed:

 
@Composable
fun TopBar(upPress: () -> Unit) {
   Surface(elevation = 8.dp) {
       Row(modifier = Modifier.fillMaxWidth()) {
           Icon(
               painter = painterResource(id = R.drawable.ic_back),
               modifier = Modifier.clickable(onClick = upPress)
                   .padding(16.dp),
               contentDescription = null
           )
       }
   }
}

Then use it in the RecipeDetailsScreen. Wrap the Column composable that we already had there in a Scaffold with a topBar parameter:

 
Scaffold(
   topBar = { TopBar(upPress = upPress) })
{
   Column( . . . ) { . . . }
}

Build and run the app.
Now we can navigate back from the recipe details to the main list.

create topbar

Next we’ll add a floating button to our RecipesScreen, so that we can create a new recipe.

To do so, first set up the navigation.
We’ll reuse the RecipeDetailsScreen for editing and creating a new recipe.
In the MainScreen add a new navigation destination like this:

 
composable(
   Navigation.RecipeDetails.route
) {
   RecipeDetailsScreen( 
       upPress = { navController.popBackStack() })
}

When the recipeId is null, we’ll use the route /recipeDetails and create a new recipe.
Otherwise we’ll view or edit the one with provided id, using the route that we defined before:

 
composable(
   "${Navigation.RecipeDetails.route}/{id}",
   arguments = listOf(navArgument("id") { type = NavType.LongType })
) {
   RecipeDetailsScreen(
       recipeId = it.arguments!!.getLong("id"),
       upPress = { navController.popBackStack() })
}

Change the parameter to nullable in RecipeDetailsScreen:

 
RecipeDetailsScreen(recipeId: Long? = null, upPress: () -> Unit)

Finally, add a floating button to create new recipes.
In MainScreen add the missing parameter to RecipesScreen destination:

 
onAddRecipe = { navController.navigate("recipeDetails") }

Change the RecipesScreen signature to accept onAddRecipe callback:

 
public fun RecipesScreen(onRecipeClicked: (RecipeResponse) -> Unit, onAddRecipe: () -> Unit)

And wrap the Recipes composable in a Scaffold with a floating action button:

 
Scaffold(floatingActionButton = {
   FloatingActionButton(onClick = onAddRecipe) {
       Icon(
           painter = painterResource(id = R.drawable.ic_add),
           contentDescription = null
       )
   }
}) {
   Recipes(items = recipes, onRecipeClicked = onRecipeClicked)
}

Build and run the app.

create topbar

Now you can navigate to an empty skeleton of the “Create new recipe” screen, but it’s not functional yet.
In the next step we’ll add a new shared ViewModel for the RecipeDetailScreen.

Step 17: Add Navigation to Jetpack Compose UI

Now that we have both RecipesScreen and RecipeDetailScreen, we can set up navigation.

Clicking on one of the list items, the user will navigate from main screen to the details screen of the recipe with the chosen id.

First, we need a couple of routes that we’ve already discussed when designing our app.

Add utils package to ui package and create a Navigation.kt routing file with:

 
sealed class Navigation(val route: String) {

   object Recipes : Navigation("recipes")
   object RecipeDetails : Navigation("recipeDetails")
}

Now go to the MainScreen and add a NavHost there, replacing the RecipesScreen:

 
@Composable
public fun MainScreen() {
   val navController = rememberNavController()
   val navBackStackEntry by navController.currentBackStackEntryAsState()

   NavHost(
       navController = navController,
       startDestination = Navigation.Recipes.route, [1]
       builder = {
           composable(Navigation.Recipes.route) {
               RecipesScreen(onRecipeClicked = { [2] navController.navigate("recipeDetail/${it.id}") })
           }
           composable(
               "recipeDetail/{id}",
               arguments = listOf(navArgument("id") { type = NavType.LongType })
           ) {
               RecipeDetailsScreen( [3]
                   recipeId = it.arguments!!.getLong("id"),
                   upPress = { navController.popBackStack() })
           }
       })
}

[1] The startDestination will be the RecipesScreen
[2] We define two destinations in this piece. One is RecipesScreen, which has an onRecipeClicked callback function. It is unresolved now, but it will define the behaviour when one of the items in our RecipeList gets clicked: the navController will take us to the RecipeDetailsScreen with the recipe’s id.
[3] Another destination, RecipeDetailsScreen will receive the recipe id as an argument

Also adjust the RecipeScreen, which now has a parameter onRecipeClicked and will pass it down to the responsible component:

 
@Composable
public fun RecipesScreen(onRecipeClicked: (Recipe) -> Unit) {
  . . .
   Recipes (items = recipes, onRecipeClicked = onRecipeClicked)
}

@Composable
fun Recipes(
   items: List<Recipe>,
   onRecipeClicked: (Recipe) -> Unit
) {
   LazyColumn {
       itemsIndexed(items = items,
           itemContent = { _, item ->
               RecipeListItem(item, onRecipeClicked = onRecipeClicked)
           })

   }
}

@Composable
fun RecipeListItem(
recipe: RecipeResponse,
onRecipeClicked: (RecipeResponse) -> Unit){
. . .
   Row(
       verticalAlignment = Alignment.CenterVertically,
       modifier = Modifier
           .height(128.dp)
           .clickable { onRecipeClicked(recipe) }
   )
   . . .
}

If needed, also adjust the Preview composable:

 
@Preview
@Composable
fun RecipeListItemPreview(
) {
. . .
   RecipeListItem(recipe) {}
}

Build and run the app. Click on a recipe list item.
You’re in the RecipeDetailScreen now.

open ContentView

Now let’s go back to the recipe screen.
Wait, what?
We can’t go back - we don’t have a top bar yet!
In the next step we’ll learn how to set up a top bar with Jetpack Compose.

Step 16: Use Coil for Images in Jetpack Compose UI

Let’s decorate the recipe list with images!

We haven’t implemented any logic to get images from backend and handle them in our shared business logic, so we’ll use placeholders at first.

To use images you need the Coil image library. Add these to Versions.kt:

 
const val COIL  = "2.0.0-rc03"

object Android {
. . .
const val COMPOSE_COIL = "io.coil-kt:coil-compose:$COIL"
}

Add this to the androidApp build.gradle.kts:

 
implementation (Versions.Android.COMPOSE_COIL)

Sync your project files.
Add a RecipeListItem composable to RecipesScreen.kt.
Now we can use Coil to load an image for the RecipeListItem:

 
@Composable
fun RecipeListItem(
   recipe: RecipeResponse
) {
   val placeholder = "https://m.media-amazon.com/images/I/413qxEF0QPL._AC_.jpg"
   Row(
       verticalAlignment = Alignment.CenterVertically,
       modifier = Modifier.height(128.dp)
   ) {
       AsyncImage(
           modifier = Modifier.padding(8.dp),
           model = placeholder,
           contentDescription = null
       )
       Text(
           text = recipe.title)
   }
}

If you want to see a preview of the UI Item, it can be easily done with Compose, just add this to the bottom of your file:

 
@Preview
@Composable
fun RecipeListItemPreview(
) {
   val title = "Best Dish in the World"
   val recipe = RecipeResponse(id = 0, title = title, listOf(), listOf(), listOf() )
   RecipeListItem(recipe)
}

Finally, you can use your recipe list item in the recipe list from before. Change Recipes composable:

 
@Composable
fun Recipes(
   items: List<Recipe> 
) {
   LazyColumn {
       itemsIndexed(items = items,
           itemContent = { _, item ->
               RecipeListItem(item)
           })

   }
}

If you run the app now, you will see placeholder image next to the Pizza dough recipe.

open ContentView

We want our app not only to show a list of recipe titles, but also some other information like ingredients and instructions. When the recipe is clicked we want to move to the recipe details screen.

Add a RecipeDetailsScreen.kt to ui package.

 
@Composable
public fun RecipeDetailsScreen(recipeId: Long, upPress: () -> Unit) [1] { 
   val viewModel = remember { RecipeViewModel() } [2]
   val recipes by viewModel.recipes.collectAsState()
   val recipe = recipes.find { it.id == recipeId } [3]
   val placeholder = "https://m.media-amazon.com/images/I/413qxEF0QPL._AC_.jpg"

   Column(
       modifier = Modifier.padding(8.dp),
       horizontalAlignment = Alignment.CenterHorizontally
   ) {
       HeaderImage(image = placeholder)
       recipe?.let { Title(it.title) }
       SectionHeader(title = "Ingredients")
       recipe?.let { Ingredients(it.ingredients) }
       SectionHeader(title = "Instructions")
       recipe?.let { Instructions(it.instructions) }
   }
}

[1] As parameters pass the recipe id and a function that will let us go back to the recipes list.
[2] For now just like in RecipesScreen we use the RecipeViewModel.
[3] We get all the recipes from RecipeViewModel and find the one we want by the id that we’ve passed from RecipesScreen. It’s not the perfect solution, but we’ll touch it up later.

Check out the project repo for full code.

Look at the SectionHeader composable:

 
@Composable
private fun SectionHeader(title: String) {
   Column(
       modifier = Modifier
           .fillMaxWidth()
           .padding(top = 8.dp),
       horizontalAlignment = Alignment.CenterHorizontally
   ) {
       Text(text = title)
       Image(
           painter = rememberAsyncImagePainter(R.drawable.ornament),
           contentDescription = null,
       ) [1]
   }
}

[1] Unlike AsyncImage that we used before to make an image request and load an external resource, Image with rememberAsyncImagePainter is used to load a drawable from local resources.

This is how simple it is to use Coil image library!
In the next step we’ll set up navigation to move from recipes list to recipe details screen.

Step 15: Bind iOS UI to the shared KMM ViewModel

Now let’s move to the iOS side for a while and implement the UI there.
First, open the workspace from iosApp folder inside your project folder in XCode.

open iosApp workspace

Open ContentView

open ContentView

There you’ll see

struct ContentView: View {
	let greet = Greeting().greeting()
 
	var body: some View {
		Text(greet)
	}
}

As we’ve already removed the Greeting file from our shared module, we won’t be able to access it here, so let’s replace

 
let greet = Greeting().greeting()

with

let greet = "Give me pizza!"

You should be able to run the code now and see the app asking for pizza.

Let’s bind this iOS app to the recipe backend just like we did for Android.
The networking logic is already in the shared module. All we need to do is to find a way to use that logic in our ios UI.

First, create an observable state for our recipe list screen.
Organise the iosApp structure bit: Create a New Group ui and move ContenView there.
Also add recipes group to the ui package.
In recipes Create a New File RecipesState.

create RecipesState

The simplest version of RecipesState looks like this:

 
import Foundation
import shared
 
class RecipesState: ObservableObject { [1]     
    let viewModel: RecipeViewModel
    
    @Published private var recipes: [RecipeResponse]  = [] [2]
    
    init() { 
        viewModel = RecipeViewModel() [3]
         
        viewModel.observeRecipes { recipes in
            self.recipes = recipes [4]
        }
    }
    
    deinit {
        viewModel.dispose()
    }
}

[1] @Published wrapper combined with the ObservableObject protocol will help us to observe changes to the published properties within this observable RecipesState and redraw our UI accordingly.
[2] In this @Published recipes list property we’ll store the recipes received from backend through the shared module.
[3] Look how easily we can initiate the shared RecipeViewModel instance in iOS!
[4] Thanks to observeRecipes function from the shared module we can get the recipes from the backend and use them for the iOS UI

Now, add RecipesView file to the recipes group.
Use the observable RecipesState object in the RecipesView like this:

import SwiftUI
import shared
 
struct RecipesView: View {
     
    @ObservedObject var state: RecipesState [1]
    
    init() { 
        state = RecipesState()
    }
    
    var body: some View {
            List(state.recipes, id: \.id) { recipe in [2]
                    Text(recipe.title)
            }
            .listStyle(PlainListStyle())
    }
}

[1] Here we’re using the RecipesState as an ObservedObject.
[2] Whenever the @Published list of recipes in RecipesState changes, we’ll refresh the List (analog for lazyColumn in Compose) with recipe titles.

Now change the main ContentView to display the RecipeView:

 
struct ContentView: View {
    
    var body: some View {
            VStack {
                RecipesView()
            }
    }
}

But if you try running the app, you’ll get an error: Uncaught Kotlin exception: kotlin.Throwable: The process was terminated due to the unhandled exception thrown in the coroutine [StandaloneCoroutine{Cancelling}@258c458, MainDispatcher]: KoinApplication has not been started

AGAIN?! 😱
No panic.
We’ll just need to setup Koin for the iosApp.
First, go back to your Android Studio and in CommonModule add

 
fun initKoin() = initKoin {}

Then return to XCode and change your iosApp.swift to the following:

 
import SwiftUI
import shared
 
@main
struct KMPizzaApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
 
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
 
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization
        CommonModuleKt.doInitKoin()
        return true
    }
}

This piece of code allows us to customize the entry point for the iosApp and lets us define when to start Koin.
Here we create an AppDelegate and while doing it, initialize Koin with this line: CommonModuleKt.doInitKoin()

Now run the app again.
It may take a while, but you’ll finally see the “Pizza dough” item in our one-item-list.
Yay! We’ve moved one step forward.

happy flying pizza

In the next step we’ll come up with a better design and learn to use KMP image libraries.

Step 14: Glue everything together with Koin dependency injection

This time we’re going to look into dependency injection with Koin.
In a simple app you can get away with manually injecting every dependency you need.
However, the bigger your app becomes, the more reasonable it is to use a dedicated dependency injection framework.

First, add core dependency to the Versions.kt:

 
object Common { 
. . .
const val KOIN_CORE = "io.insert-koin:koin-core:$KOIN_VERSION"
}

And implement it in the shared build.gradle.kts in commonMain with:

 
val commonMain by getting {
   dependencies {
       . . .
       api(Versions.Common.KOIN_CORE)
   }
}

Sync your project files.

Now instead of using RecipeRemoteSource directly to fetch data let’s create a repository for our recipes. In the future, it will manage data between our remote source, which we’ve already implemented and the local source, which we’ll implement later with SQLDelight.

Add a repository package to your commonMain.
Create a RecipeRepository.kt there.

create repository

Now that we have Koin, we can create a recipe repository by making it a KoinComponent and injecting a recipeRemoteSource:

 
class RecipeRepository : KoinComponent {
   private val recipeRemoteSource: RecipeRemoteSource by inject()

   suspend fun postRecipe(recipe: Recipe): Long = recipeRemoteSource.postRecipe(recipe)
   suspend fun getRecipes(): List<RecipeResponse> = recipeRemoteSource.getRecipes()
}

This is a very simple repository, because we don’t have any other source but the backend now.

Likewise, you can replace RecipeRemoteSource with RecipeRepository in your RecipeViewModel by simply injecting it:

 
class RecipeViewModel : CoroutineViewModel(), KoinComponent {
   private val recipeRepository: RecipeRepository by inject()
. . .
}

Don’t forget to change getRecipes():

 
fun getRecipes() {
   coroutineScope.launch {
       _recipes.value = recipeRepository.getRecipes()
   }
}

Now you can remove all the manual injection of RecipeRemoteSource in MainActivity, RecipesScreen and MainScreen.

Use RecipeViewModel in RecipesScreen just like this:

 
public fun RecipesScreen() {
   val viewModel = remember {
       RecipeViewModel()
   }
   val recipes by viewModel.recipes.collectAsState()

   Recipes(items = recipes)
}

Your MainActivity will look much prettier now with no manual injection:

 
class MainActivity : ComponentActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContent {
           MaterialTheme {
               MainScreen()
           }
       }
   }
}

Try running the app now and you’ll get an error message:
java.lang.IllegalStateException: KoinApplication has not been started

That is because we have not applied our universal Koin glue yet!
We used dependency injection everywhere, but our app doesn’t know about it.
Therefore we first need to glue the shared and android platform code together.

First create a di folder in the shared commonMain and add a CommonModule.kt there with the following code:

 
fun initKoin(appDeclaration: KoinAppDeclaration) [1] = startKoin {
   modules( [2]
       apiModule
   )
}

private val apiModule = module {
   single<KtorApi> { KtorApiImpl() } [3]
   factory { RecipesApi(get()) }
}

[1] This will initialize Koin on the platform side. We’ll see how to use it later, when writing the platform-specific code.
[2] Here we’ll keep all the modules that we need for dependency injection. For now it’s just our networking.
[3] We create a singleton of KtorApi and then use a factory to create RecipesApi whenever we need one.

Similarly, we’ll add a viewModel module and a repositoryModule.
The final version has to look like this:

 
fun initKoin(appDeclaration: KoinAppDeclaration) = startKoin {
   appDeclaration()
   modules(
       apiModule,
       repositoryModule,
       viewModelModule
   )
}


private val apiModule = module {
   single<KtorApi> { KtorApiImpl() }
   factory { RecipesApi(get()) }
}

private val viewModelModule = module {
   single{ RecipeViewModel() }
}

private val repositoryModule = module {
   factory { RecipeRemoteSource(get()) }
   single { RecipeRepository() }
}

Now we’re only missing platform-related Koin dependencies.
Add this to your Versions.kt:

 
object Android {
const val KOIN_ANDROID_MAIN = "io.insert-koin:koin-android:$KOIN_VERSION"
. . .
}

In Android app build.gradle:

 
implementation(Versions.Android.KOIN_ANDROID_MAIN)

In the androidApp create a MainApp class which will extend the Application class create MainApp

Start the Koin application in MainApp.kt:

 
@Suppress("unused")
class MainApp : Application() {

   override fun onCreate() {
       super.onCreate()
       initKoin { 
       }
   }
}

Finally, we need to inform the android app about our starting point, which we do in the Manifest by adding this line to application:

 
   android:name=".MainApp"

Run the app again and you’ll see “Pizza dough” on the main screen.
With the help of Koin our project looks neater and it’s still working just like before!

Now that our android app is up and running, in the next step we’ll do the same for the iOS app by binding iOS UI to the shared KMM ViewModel.

Step 13: Bind Jetpack Compose UI to the shared KMM ViewModel

We’ve been working on the shared module for a long time.
Let’s see if it brought us anything.

For a moment let’s step back from the shared module and work on our apps.

If you run the Android app now, you’ll still see the template “Hello, Android…” text.
We won’t need it, so you can remove Greeting and Platform files from your commonMain, as well as androidMain and iosMain.

We’ll also use Jetpack Compose for our UI, so we’ll rebuild the MainActivity and eliminate the layout folder.

But first import Jetpack Compose dependencies.
In your Versions.kt add:

 
const val COMPOSE = "1.1.1"
const val COMPOSE_ACT = "1.6.0-alpha01"
const val COMPOSE_NAV = "2.5.0-alpha03"

Then add to your Android object:

 
// Compose
const val COMPOSE_UI = "androidx.compose.ui:ui:$COMPOSE"
const val COMPOSE_GRAPHICS = "androidx.compose.ui:ui-graphics:$COMPOSE"
const val COMPOSE_TOOLING = "androidx.compose.ui:ui-tooling:$COMPOSE"
const val COMPOSE_FOUNDATION = "androidx.compose.foundation:foundation-layout:$COMPOSE"
const val COMPOSE_MATERIAL = "androidx.compose.material:material:$COMPOSE"
const val COMPOSE_NAVIGATION = "androidx.navigation:navigation-compose:$COMPOSE_NAV"
const val COMPOSE_ACTIVITY = "androidx.activity:activity-compose:$COMPOSE_ACT"

And implement them in your Android app build.gradle.kts:

 
 dependencies {
   implementation(project(":shared"))

   implementation(Versions.Android.COMPOSE_UI)
   implementation(Versions.Android.COMPOSE_GRAPHICS)
   implementation(Versions.Android.COMPOSE_TOOLING)
   implementation(Versions.Android.COMPOSE_FOUNDATION)
   implementation(Versions.Android.COMPOSE_MATERIAL)
   implementation(Versions.Android.COMPOSE_NAVIGATION)
   implementation(Versions.Android.COMPOSE_ACTIVITY)
}

Also add a material components dependency to your Android app.
As before, add these lines to Versions.kt:

 
const val MATERIAL = "1.5.0"
const val MATERIAL_COMPONENTS = "com.google.android.material:material:$MATERIAL"

And implement it in your Android app build.gradle.kts:

 
implementation(Versions.Android.MATERIAL_COMPONENTS)

Moreover, add the following configurations to the android section of android app build.gradle:

 
buildFeatures {
   compose = true
}

composeOptions {
   kotlinCompilerExtensionVersion = "1.1.1"
}

These will just turn on the Jetpack Compose functionality.
Unluckily, it doesn’t come automatically with the Android Studio Wizard KMM Template, which is weird.
I think Jetpack Compose and KMM are meant for each other.

Sync your project files.

Now open MainActivity, which is in the androidApp module in the src folder.

open MainActivity

We don’t need the greet() anymore.
We’ll also make the MainActivity extend ComponentActivity instead of AppCompatActivity:

 
class MainActivity : ComponentActivity() {

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContent { [1]
               MaterialTheme { [2]
           }
       }
   }
}

[1] Set the content with setContent function. It basically tells Jetpack Compose where to start rendering the UI.
[2] Apply the basic MaterialTheme

Build and run the app.
You’ll see an empty screen.
Good.

Let’s add our first screen where we’ll display a list of recipes.
Add a ui package with a RecipesScreen.kt inside. This will contain our first composables. Composable is a UI component in Jetpack Compose that can be recomposed if necessary. Jetpack Compose observes the states that the composable depends on and recomposes automatically.

Add the following to RecipesScreen.kt:

 
@Composable
public fun RecipesScreen(recipeRemoteSource: RecipeRemoteSource) {
   val viewModel = remember {
       RecipeViewModel(recipeRemoteSource) [1]
   }
   val recipes by viewModel.recipes.collectAsState() [2]

  Recipes (items = recipes)[3]
}

@Composable
fun Recipes(
   items: List<RecipeResponse>
) {
   LazyColumn { [4]
           itemsIndexed(items = items,
               itemContent = { _, item ->
                   Text(text = item.title)
               })
      
   }
}

[1] We will use the shared RecipeViewModel as a source of data. Here we use the remember function so that the viewModel stays the same within recompositions. In other words, it allows you to remember state from previous recompose invocation, because we want to use the same viewModel in between the recompositions. We also need to inject the RecipeRemoteSource, which is used for network requests. Later we’ll learn how to simplify the dependency injection with a library.
[2] We’ll collect the recipes flow from our ViewModel as state. This will make Jetpack Compose observe all the changes to the recipes list and recompose the ui accordingly
[3] We’ll use the state of our recipes list from the viewModel in the Recipes composable, where we display a list of recipes
[4] LazyColumn in Jetpack compose is basically your recyclerView. For starters we’ll show a list of recipe titles. We specify the recipes as items and the content of each item will be represented by a simple Text composable.

Also add a MainScreen.kt to the ui package.

open MainActivity

We’ll use it later more intensively for navigation and other stuff.
For now it’ll be our entry point for RecipesScreen:

 
@Composable
public fun MainScreen(recipeRemoteSource: RecipeRemoteSource) {
   RecipesScreen(recipeRemoteSource)
}

Now add the MainScreen to the MainActivity as the entry point for your Composition:

 
class MainActivity : ComponentActivity() {
   val recipeRemoteSource = RecipeRemoteSource(RecipesApi(KtorApiImpl()))

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContent {
           MaterialTheme {
               MainScreen(recipeRemoteSource = recipeRemoteSource)
           }
       }
   }
}

Finally, add something we always forget to add to the AndroidManifest:

 
<uses-permission android:name="android.permission.INTERNET" />

Build and run the app.
If everything goes well, you’ll see “Pizza dough” on your screen.
Yay! It worked!

Yay

Feel free to add more recipes to your backend if you want to see a longer list.

Well, to be honest, this architecture kind of works.
But just look at this chain of injections:

 
val recipeRemoteSource = RecipeRemoteSource(RecipesApi(KtorApiImpl()))

Isn’t it hideous?
It also implies we’ll have to do something as ugly in the iOS app.
Luckily, we can avoid this and make everything look much better.
In the next step we’ll learn how to do it, using Koin dependency injection framework.

Step 12: Add a ViewModel layer to the shared KMM module

To build UI we’ll use Jetpack Compose on Android and SwiftUi on iOS. To provide our UIs with data we’ll use shared ViewModels. First we’ll have to prepare our shared code by creating a sort of a view model interface, which will allow us to use viewmodels on both platforms.

Create a util package in commonMain and add the following file there:

create KtorApi interface

Create an expect class inside:

expect abstract class CoroutineViewModel() {
   val coroutineScope: CoroutineScope

   fun dispose()
  
   protected open fun onCleared()
}

As it’s an expect class we’ll have to write actual implementations on both platforms.

First go to androidMain package and add a CoroutineViewModel to the util package.

create KtorApi interface

Our CoroutineViewModel in Android will just extend the regular ViewModel and inherit its scope and functionalities.

actual abstract class CoroutineViewModel : ViewModel() {

   actual val coroutineScope = viewModelScope
 
   actual fun dispose() {
       coroutineScope.cancel()
       onCleared()
   }
 
   actual override fun onCleared() {
       super.onCleared()
   }
}

You’ll see that some references are unresolved. This is because we haven’t imported necessary dependencies yet. Let’s add these to Versions.kt:

const val LIFECYCLE_VERSION = "2.4.1"

And to the Android object:

 
const val VIEW_MODEL = "androidx.lifecycle:lifecycle-viewmodel-ktx:$LIFECYCLE_VERSION"

Then implement it in android source set in shared build.gradle.kts under androidMain:

implementation(Versions.Android.VIEW_MODEL)

Sync your project files.

Now we can resolve all references in the actual Android CoroutineViewModel implementation.

In iosMain create a new coroutine scope for the actual CoroutineViewModel:

actual abstract class CoroutineViewModel {
   actual val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())

   actual fun dispose() {
       coroutineScope.cancel()
       onCleared()
   }

   protected actual open fun onCleared() {
   }
}

Now that we have these abstractions, we can easily use them to create shared ViewModels. When they will be used on the Android side, they will be handled as ViewModels and for ios we’ll use coroutines to dispatch jobs.

You may see a warning in CoroutineViewModel: Expected class 'CoroutineViewModel' has no actual declaration in module kmpizza.shared.jvmMain for JVM. This is there because we specified jvm() as target in the shared build.gradle.kts. However, for now we only need CoroutineViewModel for iOS and Android. So just choose the suggested action and Android Studio will create a template actual CoroutineViewModel for you in jvmMain. This will make sure the warning is gone.

create KtorApi interface

Now let’s add a RecipeViewModel to a viewmodel package in the shared module:

create KtorApi interface

Let’s take a look at our RecipeViewModel:

class RecipeViewModel (private val recipeRemoteSource: RecipeRemoteSource)
 : CoroutineViewModel() [1] {

   private val _recipes = MutableStateFlow<List<RecipeResponse>>(emptyList())
   val recipes: StateFlow<List<RecipeResponse>> = _recipes [2]

   init {
       getRecipes()
   }

   fun getRecipes() {
       coroutineScope.launch {
           _recipes.value = recipeRemoteSource.getRecipes() [3]
       }
   }
}

[1] It extends CoroutineViewModel
[2] We’ll need a variable to hold the mutable state flow of a list fo recipes. We’ll use it to display a list of recipes with Jetpack Compose in Android and SwiftUi in iOS apps.
[3] Here we simply launch our getRecipes function from the remote source which fetches data from our backend. And we’ll do it right when we init the RecipeViewModel.

To make this viewModel complete and compatible with ios we’ll need to add a function which will help us observe the recipes from the iOS side. We’ll use this function later in SwiftUi and pass an appropriate callback, which refresh the UI accordingly every time when the recipes variable is changed.

 
fun observeRecipes(onChange: (List<RecipeResponse>) -> Unit) {
   recipes.onEach {
       onChange(it)
   }.launchIn(coroutineScope)
}

Notice how we’re using the coroutineScope to observe the flow of recipes, just like we used it here in getRecipes to fetch them.

Awesome, we finished our first shared ViewModel!

Next we’ll move to Android, start building our UI with Jetpack Compose and see how to bind it to the shared RecipeViewModel.

Step 11: Add networking layer to shared KMM module with Ktor

Now that we have a backend for the recipes we’ll use KMM power to implement some shared layers, such as networking, local app storage and even viewmodels.

Let’s start with networking, where we will use all the functionality from our backend.

First, add Ktor Client dependencies to our project. Yes, we’ll be using the same Ktor we used for our backend, but it’s Client functionality this time.

Add a Common object with necessary dependencies to your Versions.kt:

object Common {
   const val KTOR_CLIENT_CORE = "io.ktor:ktor-client-core:$KTOR_VERSION"
   const val KTOR_CLIENT_JSON = "io.ktor:ktor-client-json:$KTOR_VERSION"
   const val KTOR_LOGGING = "io.ktor:ktor-client-logging:$KTOR_VERSION"
   const val KTOR_CLIENT_SERIALIZATION = "io.ktor:ktor-client-serialization:$KTOR_VERSION"
}

Alongside with the client dependency and additional features like logging and serialization we add engine dependencies for our platform sides:

object Android {
   const val KTOR_CLIENT = "io.ktor:ktor-client-android:$KTOR_VERSION"
   const val KTOR_OKHTTP = "io.ktor:ktor-client-okhttp:$KTOR_VERSION"
} 

object iOS {
   const val KTOR_CLIENT = "io.ktor:ktor-client-ios:$KTOR_VERSION"
}

Now implement these dependencies in build.gradle.kts in your shared module (not backend!). Modify the corresponding sourceSets as follows:

val commonMain by getting {
   dependencies {
       // Ktor
       implementation(Versions.Common.KTOR_CLIENT_CORE)
       implementation(Versions.Common.KTOR_LOGGING)
       implementation(Versions.Common.KTOR_CLIENT_JSON)
       implementation(Versions.Common.KTOR_CLIENT_SERIALIZATION)
   }
}

val androidMain by getting {
   dependencies {
       implementation(Versions.Android.KTOR_CLIENT)
       implementation(Versions.Android.KTOR_OKHTTP)
   }
}

val iosMain by getting {
   dependencies {
       implementation(Versions.iOS.KTOR_CLIENT)
   }
}

Also add serialization plugin to the shared module plugins, so that we can deserialize data from network responses into data classes:

plugins {
   kotlin("multiplatform")
   kotlin("native.cocoapods")
   id("com.android.library")
   kotlin("plugin.serialization") version Versions.KOTLIN_VERSION
}

Sync your project files. Now, let’s create our Ktor client. First, in the shared module in commonMain create a new package api and add a KtorApi interface:

create KtorApi interface

Here we declare the Ktor client and the methods which we’ll use for all api calls.

interface KtorApi {
   val client: HttpClient
   fun HttpRequestBuilder.apiUrl(path: String)
   fun HttpRequestBuilder.json()
}

With apiUrl we’ll set the request path and with json we’ll tell the client to receive data in form of json.

Then we add a KtorApiImpl.kt inext to KtorApi.kt and write an implementation:

class KtorApiImpl() : KtorApi {

   val prodUrl = "https://enigmatic-sands-01782.herokuapp.com/" [1]

       override val client = HttpClient {
       install(JsonFeature) { [2]
           serializer = KotlinxSerializer()
       }
   }

   override fun HttpRequestBuilder.apiUrl(path: String) {
       url {
           takeFrom(prodUrl) [3]
           encodedPath = path
       }
   }

   override fun HttpRequestBuilder.json() {
       contentType(ContentType.Application.Json) [4]
   }
}

[1] For now we hardcoded the backend urls, later we’ll see how to manage them in build configuration
[2] We’ll need to serialize and deserialize our objects to receive and send them to our backend, therefore we use kotlinx serializer feature, that we imported earlier
[3] Specify the url that should be used to fetch data: it consists of the base production url and the following endpoint path
[4] Specify the content type for all requests, here we are using json

Now let’s create our actual RecipesApi. Your commonMain should look like this by now:

create KtorApi interface

RecipeApi will extend the KtorApi and contain the recipe endpoint:

class RecipesApi(private val ktorApi: KtorApi) : KtorApi by ktorApi {
   companion object {
       const val RECIPES_BASE_URL = "recipes"
   }
}

Also add all the necessary requests for our app:

suspend fun getPizza(): String {
   return client.get {
       apiUrl("pizza") [1]
   }
}

suspend fun getRecipes(): List<RecipeResponse> {
   return client.get { [2]
       apiUrl(RECIPES_BASE_URL)
   }
}

suspend fun getRecipe(id: Long): RecipeResponse {
   return client.get {
       apiUrl("$RECIPES_BASE_URL/$id")
   }
}

suspend fun postRecipe(recipe: Recipe): Long [3] {
   return client.post {
       json() [4]
       apiUrl(RECIPES_BASE_URL)
   }
}

[1] For our test endpoint we don’t use the recipes url: instead we call the pizza url and should get a String in return
[2] Use the GET function to receive data from backend as a List of RecipeResponses
[3] When we post a recipe we get an ID of type Long in return
[4] Don’t forget to specify the content type for your post request

As you can see, we just mirrored the requests that we already have in our backend api. We also have an option to upload pictures, but we’ll see how to do it after we’re done with basic text requests.

You may be wondering, why Recipe and RecipeRepsonse are unresolved. Well, we’ve created an entity for our model, but it was in another module, the backend module.

To solve this problem, let’s copy that Entity.kt file and put it into a model folder in our shared module. Now you can resolve the references in RecipeApi.

Let’s remove the model package from our backend module and implement the shared project there instead.

To do so, in shared build.gradle.kts first add jvm() to your targets:

kotlin {
   android()
   jvm()

. . .
}

Now, implement the shared project in the backend build.gradle.kts under dependencies:

implementation(project(":shared"))

Sync your project files and fix the Exposed entities in your backend module to import corresponding classes from kmpizza.model package.

Now we have our model only in one location, and it’s extremely convenient, isn’t it?

Finally, let’s create a remote data source, which we can later use alongside a local source to get appropriate data.

Create RecipeRemoteSource class in shared/commonMain/remote.

create KtorApi interface

This class will receive recipesApi as a parameter and use it to fetch recipes with getRecipes from our RecipesApi, as well as add a recipe with postRecipe.

class RecipeRemoteSource(
   private val recipesApi: RecipesApi
) {

   suspend fun getRecipes() = recipesApi.getRecipes()
   suspend fun postRecipe(recipe: Recipe) = recipesApi.postRecipe(recipe)
}

Easy as that, with the help of Ktor we’ve just added a networking layer to our shared KMM module. This will be the only location where we implement our api requests. You’ll see how to use them on platform sides in the following steps. However, we’re not done with the shared part. In the next step we’ll look into sharing a viewmodel layer.

Step 10 (bonus): Use Procfile for deployment on Heroku

So far we’ve tested our backend with images locally, now let’s move it to Heroku! But this time we’ll make things easier for us by deploying it directly on Heroku (without having to build the jar in Android Studio and deploying it manually after with heroku deploy:jar).

First, add Procfile to the project root.

create procfile

Write the following inside the Procfile:

web: java -jar ./backend/build/libs/Backend.jar

It will tell Heroku to start a web dyno and deploy our backend.jar file.

If you haven’t done so yet, add system.properties to the project root as well.

It should contain the following settings, which will specify which module to build and what Java runtime to use:

java.runtime.version=11
rootProject.name = "backend"

Git add and commit both files: make sure system.properties is committed, because it can be ignored!

Then do git push.

Now run the following command in terminal:

heroku config:set GRADLE_TASK=":backend:build"

===== side note =====
(If you want to manually build a jar file again, you’ll need to run heroku buildpacks:clear --remote <branch-name>)

If you are deploying the heroku project on a new branch, don’t forget to specify it with –remote postfix and also remember to install a postgres plugin there, for example like this:

heroku addons:create heroku-postgresql --remote <branch-name>

===== side note =====

We need to set a buildpack for Heroku.
We’re using gradle, so we have to run the following command in terminal:

heroku buildpacks:set heroku/gradle

Finally, run to create a new release using this buildpack.

git push heroku

You will see a build failure 😱
Looks like we’ve forgotten something important.
If you look at the build logs, you’ll notice this error:

* What went wrong:
remote:        A problem occurred configuring project ':buildSrc'.
remote:        > Could not resolve all artifacts for configuration ':buildSrc:classpath'.
remote:        > Could not resolve org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.21.

Let’s add the following line to our dependencies in backend gradle.build.kts:

implementation(kotlin("stdlib"))

Run the git push heroku command again.

It will take some time for the project to build, but finally you should see something like this: heroku build success

Before serving your pizza directly from Heroku, you will need to set the necessary environment variables like this:

heroku config:set accessKey="MyAccessKeyForAWS"
heroku config:set secretKey="MySecretKeyForAWS"
heroku config:set bucketName="kmpizza"
heroku config:set region="eu-central-1"

Now, you can go to https://your-heroku-app-name.herokuapp.com/pizza and get a greeting from your backend, comfortably deployed on Heroku.

If you are hungry enough, then you can even try and add a pizza recipe with an image using Postman, just like we did previously when testing it locally!

Hey, we’re done with the backend for now! 🎉
We’ve build our basic backend for the app, so let’s move on to the world of shared KMM code!

In the next steps we’ll be using this backend while building the shared part of our future iOS and Android apps.

Step 9: Add images to KMP backend using AWS S3 Storage

There is one thing that is currently missing in our recipes:
mouthwatering pictures of pizza with lots of cheese, like this one:

pizza cheese yummy

So let’s add pictures to our backend. We’ll use Amazon S3 Storage to upload and store images.

Attention: it requires creating an AWS account and binding in to your credit card, but we’ll only use free features, so no worries about having to pay. If you want to continue without images then just skip this part.

First, we need to register. Go to AWS portal and create an AWS account.
Verifying and binding your account to your credit card may take several days, then you’ll be able to start using AWS services.

When you’re verified, go to Amazon Services -> Storage -> S3

go to aws

There create your first bucket, where you’ll store pizza pictures.

create bucket

configure bucket

You can choose an AWS rregion for your bucket. To reduce latency choose the closest to you. Also do not choose “block public access”.

cblock public access

After your create a bucket it should appear in the list like this:

list of buckets

Remember your bucketName and region for future use in the app. In this case they are: bucketName: kmpizza and region: eu-central-1

We’ll also need an accessKey and a secretKey.

Go to IAM Console. And add a new user with programmatic access

create new user

Add permissions for S3 to this user

permissions

In stage 4 it should look like this:

stage 4

When your user is created, copy your secretKey and accessKey

accessKey: ******
secretKey: ******

Now that we have Amazon Storage all set up and ready, let’s get back to our backend module.

First, add AWS SDK to Versions.kt:

const val AWS_VERSION = "2.17.102"

object Jvm {
...
const val AWS_JAVA_SDK = "software.amazon.awssdk:bom:$AWS_VERSION"
const val AWS_S3 = "software.amazon.awssdk:s3"

}

Then implement it in your backend build.gradle.kts dependencies:

implementation(platform(Versions.Jvm.AWS_JAVA_SDK))
implementation(Versions.Jvm.AWS_S3)

Now let’s create a aws package and put it next to exposed in the storage directory:

create aws package

Inside this package create a FileStorage interface with a function to save files:

import java.io.File

interface FileStorage {
   suspend fun save(file: File): String
}

Then create an AmazonFileStorage class that will implement this save function:

class AmazonFileStorage : FileStorage {

   private val client: S3Client

   private val bucketName: String = System.getenv("bucketName") [1]

   init {
       val region = System.getenv("region") [2]
       val accessKey = System.getenv("accessKey")
       val secretKey = System.getenv("secretKey")

       val credentials = AwsBasicCredentials.create(accessKey, secretKey)
       val awsRegion = Region.of(region)
       client = S3Client.builder()
           .credentialsProvider(StaticCredentialsProvider.create(credentials)) [3]
           .region(awsRegion)
           .build() as S3Client
   }

   override suspend fun save(file: File): String = [4]
   withContext(Dispatchers.IO) {
       client.putObject(
               PutObjectRequest.builder().bucket(bucketName).key(file.name).build(),
               RequestBody.fromFile(file)
           ) [5]
       val request = GetUrlRequest.builder().bucket(bucketName).key(file.name).build()
       client.utilities().getUrl(request).toExternalForm()
   }
}

[1] Get the bucket name from environment variables
[2] Also get other settings to initiate an AWS S3 client: region (optional), access and secret Keys
[3] Use specified settings to build an S3Client
[4] Implement the save function from the FileStorage Interface
[5] Use the object request to put a file into the AWS S3 bucket and get the source url

We want to use AWS functionality to upload images from the application when saving new recipes, so add FileStorage to LocalSourceImpl’s constructor like this:

internal class LocalSourceImpl(
   private val fileStorage: FileStorage,
   application: io.ktor.application.Application
) : LocalSource {
. . .
}

Just like with our local storage we want to use the AWS storage simply by injecting where we need it, so add one line to your KoinModule.kt and modify the way we create LocalSourceImpl by injecting the Amazon storage into it:

internal fun Application.getKoinModule() = module {
   single<FileStorage> { AmazonFileStorage() }
   single<LocalSource> { LocalSourceImpl(get(), this@getKoinModule) }
}

Let’s add a request to save images.
First, we’ll extend our recipes with a list of images that will show how delicious it is. Add a RecipeImage data class to your Entity.kt file

@Serializable
data class RecipeImage(
   val id: Long = 0, 
   val image: String
)

We can create recipes with images in two steps:
1) use the previously created Recipe entity to post a recipe to the backend and receive the id of newly created Recipe
2) add pictures to it.

But to receive recipes with images directly from the backend whenever we need them we’ll introduce a RecipeReponse entity:

 @Serializable
data class RecipeResponse(
   val id: Long = 0,
   val title: String,
   val ingredients: List<Ingredient>,
   val instructions: List<Instruction>,
   val images: List<RecipeImage>
)

Now we need to adjust our database. In a new image package create a RecipeImageTable: create recipe image table

 @Serializable
object RecipeImageTable : IntIdTable("recipeImages") { 
   val image = varchar("image", 1024)
   val recipe = reference(
       "recipe_id",
       RecipeTable,
       onDelete = ReferenceOption.CASCADE,
       onUpdate = ReferenceOption.CASCADE
   )
}

And a corresponding Entity:

class RecipeImageEntity(id: EntityID<Int>) : IntEntity(id) {
   companion object : IntEntityClass<RecipeImageEntity>(RecipeImageTable)
 
   var image by RecipeImageTable.image
   var recipe by RecipeEntity referencedOn RecipeImageTable.recipe
}

fun RecipeImageEntity.toRecipeImage() = RecipeImage(id.value.toLong(), image)

Finally, we need to adjust our RecipeEntity by adding a recipeImage property:

val recipeImages by RecipeImageEntity referrersOn RecipeImageTable.recipe

Add changing the converter function accordingly:

fun RecipeEntity.toRecipeResponse() = RecipeResponse(
   id.value.toLong(),
   title,
   ingredients.map { it.toIngredient() },
   instructions.map { it.toInstruction() },
   recipeImages.map { it.toRecipeImage() })

Let’s use our newly created entities.
In your LocalSource interface declare this function:

suspend fun saveImage(recipeId: Long, image: File)

Also change the return type of getRecipes and getRecipe:

suspend fun getRecipes() : List<RecipeResponse>
suspend fun getRecipe(recipeId: Long) : RecipeResponse

And in the LocalSourceImpl write the following implementations:

override suspend fun saveImage(recipeId: Long, image: File) {
   withContext(dispatcher) {
       val imageUrl = fileStorage.save(image) [1]       
           transaction {
           val recipe = RecipeEntity[recipeId.toInt()] [2]
           RecipeImageEntity.new { [3] 
               this.image = imageUrl
               this.recipe = recipe
           }
       }
   }
}

[1] Save the image in S3 storage and get the image url in return
[2] Get the recipe from the database that the image should be attributed to
[3] Create a new recipe image entity and save a reference to the recipe

And don’t forget to change the return type of getRecipes and getRecipe:

override suspend fun getRecipes(): List<RecipeResponse> = withContext(dispatcher) {
   transaction {
       RecipeEntity.all().map { it.toRecipeResponse() }
   }
}

override suspend fun getRecipe(recipeId: Long): RecipeResponse = withContext(dispatcher) {
   transaction {
       RecipeEntity[recipeId.toInt()].toRecipeResponse()
   }
}

Also, we have to create a new image recipe table in the LocalSourceImpl:

transaction {
   SchemaUtils.createMissingTablesAndColumns(
       RecipeTable,
       IngredientTable,
       InstructionTable,
       RecipeImageTable
   )
}

Finally, let’s add the routes to our api. Add this function to Routing:

addRecipeImage(localSource) 

And then implement it:

private fun Routing.addRecipeImage(localSource: LocalSource) {
   post("/recipes/{recipeId}/recipeImage") {
       val recipeId = call.parameters["recipeId"]!!.toLong()
 
       var image: File? = null

       call.receiveMultipart().forEachPart { [1]
           when (it) { 
               is PartData.FileItem -> image = it.toFile() [2]
               else -> Unit
           }
           it.dispose()
       }
       localSource.saveImage(
           recipeId, 
           image ?: throw BadRequestException("image part is missing")
       )
       image?.delete()[3]
       call.respond(HttpStatusCode.OK)
   }
}

[1] We receive the image file from the client side as a multipart form data
[2] This will help us transform part data to a file type so that we can save the image data in our database. It is unresolved for now and still has to be implemented in the next step
[3] If an error happens we throw an exception and do not save the image file

As mentioned in [2] we still need to create the toFile function that will help us transform the PartData.FileItem to a File. Create a utils package in our backend package and put an Extensions.kt file there with the following function

import io.ktor.http.content.*
import java.io.File
:
fun PartData.FileItem.toFile() = File(originalFileName!!).also { file ->
   streamProvider().use { input ->
       file.outputStream().buffered().use { output ->
           input.copyTo(output)
       }
   }
}

Now we can test uploading pictures to the recipes. First rebuild and make the project. In the terminal set local environment variables according to your AWS settings.

export bucketName=kmpizza
export secretKey=******
export accessKey=******
export region=eu-central-1
export JDBC_DATABASE_URL=jdbc:postgresql:recipecollection?user=postgres

then run the backend locally as many times before:

java -jar ./backend/build/libs/Backend.jar -port=9000

Check with postman if you still have any recipes in your local database, otherwise add the pizza dough recipe again. If your database is fresh you get you recipeId: 1, otherwise choose the id of the recipe you want to add pictures to.

Now you can add an image to your recipe. Using that id:

http://localhost:9000/recipes/1/recipeImage

post image

We get status 200 OK in response. Let’s check if the recipe has an image now with getRecipes. Indeed in our recipe we see a list of images now:

 "images": [
            {
                "id": 1,
                "image": "https://kmpizza.s3.eu-central-1.amazonaws.com/kmpizza.jpg"
            }
        ]

Yay! 🥳
We’ve built our very functional backend to store delicious recipes with images. In the next step we’ll see how to publish our backend on Heroku automatically. Soon after that we’ll move on to using the backend we built within the shared code between iOS and Android.

Step 8: Deploy KMP backend on Heroku

The backend works locally and it’s already a big step forward, but how can we deploy it?
We’ll use Heroku for it.
If you don’t have a Heroku account you first need to sign up.
You’ll also need Heroku CLI installed, here are the instructions.

To make sure that the appropriate Java Runtime version is used while deploying on Heroku we need one more configuration . Add system.properties file to the root of your project with the following lines:

java.runtime.version=11
rootProject.name = "backend"

After you’ve registered and installed Heroku CLI, you should be able to login from the terminal with the following command:

heroku login

And then create a new application with

heroku create 

You’ll get the id of your newly created app in return, like enigmatic-sands-01782

create heroku app

Then you will need to install a Heroku deploy plugin:

heroku plugins:install heroku-cli-deploy

Now deploy the jar created by shadowJar under the name of our Heroku app with this command. Don’t forget to change the app name.

heroku deploy:jar ./backend/build/libs/Backend.jar --app enigmatic-sands-01782

It’ll take some time to upload the build

create heroku app

After a while you’ll see the upload succeeded:

create heroku app

Finally, add your remote heroku git source:

git remote add heroku git@heroku.com:enigmatic-sands-01782.git 

In the future, every time you make changes you’ll need to update you remote like this:

git push heroku master

Try opening https://enigmatic-sands-01782.herokuapp.com/pizza
Don’t forget to replace the Heroku app name with your own.
You’ll see an error.
Try investigating with heroku logs --tail, you’ll find:

Caused by: org.koin.core.error.InstanceCreationException: Could not create
 instance for [Singleton:'com.example.recipecollection.backend.localSource
 LocalSource']

or

Caused by: java.lang.IllegalArgumentException: jdbcUrl is required with driverClassName.

It happens because we are missing a postgres database plugin which comes for free with Heroku. Let’s add it with

heroku addons:create heroku-postgresql

You’ll see something like this:

create heroku app

This has created your database url and should have automatically set the JDBC_DATABASE_URL environment variable in heroku. To check it, you can either use

heroku config

or

heroku run echo \$JDBC_DATABASE_URL

Let’s check out our app again with

heroku open

No errors.
Go to https://enigmatic-sands-01782.herokuapp.com/pizza.
Awesome, now we can see our application serving pizza from Heroku!

pizza!

Step 7: Add serialization and useful endpoints to save Recipes in the database

Last time we tested the local data source with a simple request.
Now let’s add some real power to it.
First, declare a bunch of useful functions in the LocalSource interface. These have to do with our future app’s functionality like adding recipes and corresponding ingredients and instructions.

internal interface LocalSource {
   suspend fun getPizza(): String
   suspend fun addIngredient(recipeId: Long, ingredient: Ingredient): Long
   suspend fun addInstruction(recipeId: Long, instruction: Instruction): Long
   suspend fun addRecipe(recipe: Recipe): Long
   suspend fun getRecipes() : List<Recipe>
   suspend fun getRecipe(recipeId: Long) : Recipe
}

Some of them return Long, because we’ll add new entries to the database, which will assign an id to each of them and return that id as Long.

Now implement these functions in LocalSourceImpl:

override suspend fun addIngredient(recipeId: Long, ingredient: Ingredient) = withContext(dispatcher) {
   transaction {
       val recipe = RecipeEntity[recipeId.toInt()] [1]
       IngredientEntity.new { [2]
           name = ingredient.name
           amount = ingredient.amount.toBigDecimal()
           metric = ingredient.metric
           this.recipe = recipe
       }.id.value.toLong()
   }
}

override suspend fun addInstruction(recipeId: Long, instruction: Instruction) = withContext(dispatcher) {
   transaction {
       val recipe = RecipeEntity[recipeId.toInt()]
       InstructionEntity.new {
           order = instruction.order
           description = instruction.description
           this.recipe = recipe
       }.id.value.toLong()
   }
}

override suspend fun addRecipe(recipe: Recipe) = withContext(dispatcher) {
   withContext(dispatcher) {
       val recipeId = transaction {
           RecipeEntity.new {
               title = recipe.title
           }.id.value.toLong()
       } [3]

       recipe.ingredients.forEach{
           addIngredient(recipeId, it)
       } [4]

       recipe.instructions.forEach{
           addInstruction(recipeId, it)
       }
       recipeId
   }
}

override suspend fun getRecipes() : List<Recipe> = withContext(dispatcher) {
   transaction {
       RecipeEntity.all().map { it.toRecipe() }
   }
}

override suspend fun getRecipe(recipeId: Long): RecipeResponse = withContext(dispatcher) {
   transaction {
       RecipeEntity[recipeId.toInt()].toRecipeResponse()
   }
}

These are basic functions that will fill up our database with recipes, ingredients and instructions.

[1] When creating an ingredient or instruction, we have to specify recipeId which this item belongs to and get the appropriate recipe
[2] Then we’ll create a new ingredient or instruction entity and attach it to our recipe
[3] When adding a new recipe to the database we first create a new recipe entry
[4] Then add all the instructions and ingredients to it

Now let’s bind those functions to our backend. Just like we did with getPizza, we’ll add some more routes for adding and getting recipes in Api.kt:

internal fun Routing.api(localSource: LocalSource) {
   pizza(localSource)
   addRecipe(localSource)
   getRecipes(localSource)
   getRecipe(localSource)
}

Implement them in the same file:

private fun Routing.addRecipe(localSource: LocalSource) {
    post("/recipes") {
        val recipeId = localSource.addRecipe(call.receive())
        call.respond(recipeId)
    }
}

private fun Routing.getRecipes(localSource: LocalSource) {
    get("/recipes") {
        localSource.getRecipes().let {
            call.respond(it)
        }
    }
}

private fun Routing.getRecipe(localSource: LocalSource) {
    get("/recipes/{recipeId}") {
        val recipeId = call.parameters["recipeId"]!!.toLong()
        localSource.getRecipe(recipeId).let {
            call.respond(it)
        }
    }
}

[1] When adding a Recipe we specify the Recipe data class as the body input and get a newly created recipe entity id from the database
[2] To get a recipe by id we need to lookup for the recipe with the id, that we specified in API route

One last thing before we test it: we want to be able to serialize our classes between the client and the server. To do so, we’ll need kotlinx.serialization plugin. Let’s adjust our dependencies.

First, add this to the Versions.kt:

const val KOTLIN_VERSION = "1.6.10"

In the backend module add the serialization plugin to build.gradle.kts:

plugins {
   kotlin("jvm")
   application
   id("com.github.johnrengelman.shadow") version Versions.SHADOW_JAR_VERSION
   kotlin("plugin.serialization") version Versions.KOTLIN_VERSION
}

You may also need to adjust the project build.gradle.kts so that it matches the latest Kotlin version you specified, for example like so:

dependencies {
        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.KOTLIN_VERSION}")
        classpath("com.android.tools.build:gradle:7.1.0-alpha10")
    }

Sync the project. Then add @Serializable annotation to all our model data classes like this:

@Serializable
data class Recipe(
   val id: Long = 0,
   val title: String,
   val ingredients: List<Ingredient>,
   val instructions: List<Instruction>

)

You may need to do a manual import:

import kotlinx.serialization.Serializable

Now let’s test this in action. Use Postman for it. Rebuild and make the backend again and run it locally as before:

export JDBC_DATABASE_URL=jdbc:postgresql:recipecollection?user=postgres
java -jar ./backend/build/libs/backend.jar -port=9000 

Then try adding a Pizza Dough recipe with Postman. Send a POST request to our API endpoint http://localhost:9000/recipes. You can use the following body:

{
   "title":"Pizza dough",
   "ingredients": [
       {
           "name": "flour",
           "amount": 2.0,
           "metric": "cup"
 
       },
       {
           "name": "water",
           "amount": 1.0,
           "metric": "cup"
 
       },
       {
           "name": "flour",
           "amount": 2.5,
           "metric": "cup"
 
       },
       {
           "name": "salt",
           "amount": 1.0,
           "metric": "teaspoon"
 
       },
       {
           "name": "sugar",
           "amount": 1.0,
           "metric": "teaspoon"
 
       },
       {
           "name": "dry yeast",
           "amount": 7.0,
           "metric": "gram"
 
       },
       {
           "name": "olive oil",
           "amount": 3.0,
           "metric": "tablespoons"
 
       }
 
   ],
   "instructions":[
       {
           "order": 1,
           "description": "Mix water, sugar and yeast. Let it froth for 10 minutes"
       },
       {
           "order": 2,
           "description": "Mix dough, salt, olive oil. Add water mixture from previous step."
 
       },
       {
           "order": 3,
           "description": "Knead the dough for 10 minutes"
 
       }
 
   ]
}

We get 500 Internal Server Error 😱 Don’t be upset! We’ll find out why.

To help us investigate the cause we can install StatusPages in Application.module() and let it return the cause of the error like this :

install(StatusPages) {
   exception<Throwable> { cause ->
       call.respond(cause.toString())
   }
}

Now if you rebuild the backend and try the same request again you’ll see

io.ktor.features.CannotTransformContentToTypeException: Cannot transform this request's content to com.example.recipecollection.backend.model.Recipe

This happens because we forgot to install ContentNegotiation between the client and the server to help us serialize objects. Let’s do it now, add this to you application:

install(ContentNegotiation) {
   json()
}

Build the backend and try sending the request again.
Voilà!
We get the recipe id in return.

Now try posting the Recipe again. Then try the GET /recipesendpoint and you’ll get our Pizza Dough recipe in return.
Time to start cooking! 🧑‍🍳

Step 6: Setup a local database with PostgreSQL and HikariCP

By now we’ve prepared everything to setup our local database. We’ll use postgreSQL for it. If you don’t have it installed already, follow the instructions here.

Then create a new database for our app with the following sequence of commands from the terminal:

psql -U postgres
CREATE DATABASE recipecollection;

(If you later need to delete the database, use DROP DATABASE recipecollection;)

Pay attention to “;”!

Then switch to the newly created database:

\c recipecollection

You should see the following message: You are now connected to database recipecollection as user postgres.

​​Congrats, you are a proud owner of a local database! It’s empty for now, but you don’t need to create the tables manually, as our exposed backend will take care of it with the help of all the tables we’ve already defined in our app.

We’ll use JDBC (Java Database Connectivity) to manage connection to our database and execute requests. A lightweight JDBC library HikariCP will help us create a connection pool that can be reused when we make a new request to the database. Let’s add missing dependencies to our app:

Add these to your Versions.kt:
const val POSTGRESQL_VERSION = "42.3.1"
const val HIKARI_VESRION = "5.0.0"

...

object Jvm {
const val HIKARI_CONNECTION_POOL = "com.zaxxer:HikariCP:$HIKARI_VESRION"
const val POSTGRESQL = "org.postgresql:postgresql:$POSTGRESQL_VERSION"
}

And then use the dependencies in the backend module:

implementation(Versions.Jvm.POSTGRESQL)
implementation(Versions.Jvm.HIKARI_CONNECTION_POOL)

Sync your project dependencies.

Now let’s go back to the app and actually create a local source implementation so that we can connect it to our REST api. First, create a file LocalSource.kt in the exposed directory and define an interface:

internal interface LocalSource {
}

Later on, we’ll think about what methods we want to use here to access the data in our database. For now we’re more interested in implementing and configuring it. So let’s create an implementation for our LocalSource. Add a LocalSourceImpl.kt next to LocalSource.kt. Here we’ll use HikariCP functionality to configure our database connection and afterwards fill up the database with the tables we specified before within the Exposed framework:

   @OptIn(ObsoleteCoroutinesApi::class)
   internal class LocalSourceImpl(application: io.ktor.application.Application) : LocalSource {
      private val dispatcher: CoroutineContext

      init {
         val config = application.environment.config.config("database") [1]
         val url = System.getenv("JDBC_DATABASE_URL") [2]
         val driver = config.property("driver").getString()
         val poolSize = config.property("poolSize").getString().toInt()
         application.log.info("Connecting to db at $url")

         dispatcher = newFixedThreadPoolContext(poolSize, "database-pool") [3]
         val hikariConfig = HikariConfig().apply {
            jdbcUrl = url
            maximumPoolSize = poolSize
            driverClassName = driver
            validate()
         }

         Database.connect(HikariDataSource(hikariConfig)) [4]

         transaction { [5]
            SchemaUtils.createMissingTablesAndColumns(
                  RecipeTable,
                  IngredientTable,
                  InstructionTable
            )
         }

      }
   }

[1] Here we specify some of the configuration parameters. Therefore we’ll also need to extend our application.conf file with the database driver and poolSize. Add this to application.conf:

ktor {
 deployment {
   port = 8080
   port = ${?PORT}
 }
 application {
   modules = [dev.tutorial.kmpizza.backend.MainKt.module]
 }
}

database {
 driver = "org.postgresql.Driver"
 poolSize = 20
}

[2] We will get the URL for our database from environment variables. To test the database locally we can set this variable in our terminal, which I’ll show you how to do later
[3] Create a coroutine execution context with the fixed-size thread-pool
[4] Connect to our exposed database using Hikari Configuration
[5] Create missing tables from our definitions

Now that we’ve set up a backend and a local database, let’s bind them together. We’ll use Koin dependency injection for it. First, add these to Versions.kt:

const val KOIN_VERSION = "3.1.2"

object Jvm {
const val KOIN_KTOR = "io.insert-koin:koin-ktor:$KOIN_VERSION"

}

Add the dependency to your backend build.gradle.kts:

implementation(Versions.Jvm.KOIN_KTOR)

Sync your dependencies.

Now we can create a Koin Module. Add KoinModule.kt to a new directory “di” (as in “dependency injection”) in your backend module.

add koin module

Create a module with a single local source, adding other necessary imports:

import io.ktor.application.*
import org.koin.dsl.module

internal fun Application.getKoinModule() = module {
   single<LocalSource> { LocalSourceImpl(this@getKoinModule) }
}

Then add Koin to Application module in your Main.kt:

import org.koin.ktor.ext.Koin

internal fun Application.module() {

   install(Koin) {
       modules(getKoinModule())
   }

   install(Routing) {
       api()
   }
}

Ok, let’s test how our local source works with the backend. We’ll add one test function to the localSource:

internal interface LocalSource {
   suspend fun getPizza(): String
}

And implement this function in LocalSourceImpl:

override suspend fun getPizza(): String = withContext(dispatcher) {
 "Pizza!"
}

Now let’s change our routing. First, change the Routing.hello() function:

private fun Routing.pizza(localSource: LocalSource) {
   get("/pizza") {
       localSource.getPizza().let {
           call.respond(it)
       }
   }
}

And change Routing.api():

internal fun Routing.api(localSource: LocalSource) {
   pizza(localSource)
}

To finally connect all together we need to inject local source in our backend application and use it with Routing. Change Application.module() to look like this:

internal fun Application.module() {
   install(Koin) {
       modules(getKoinModule())
   }

   val localSource by inject<LocalSource>()

   install(Routing) {
       api(localSource)
   }
}

Time to test! First, rebuild and make the project. Then in the terminal add the local environment variable to your environment:

export JDBC_DATABASE_URL=jdbc:postgresql:recipecollection?user=postgres

And then run our backend locally:

java -jar ./backend/build/libs/Backend.jar -port=9000

Go to localhost:9000/pizza and if you get a pizza craving from this request, then we’re on the right track!

pizza!

Step 5: Create tables and entities with Jetbrains Exposed framework

Next challenge: let’s prepare a database that will store all our recipes. We’ll use a Jetbrains Exposed ORM framework coupled with a postgres SQL for it.

First, add necessary dependencies for Exposed to your project. Add the exposed maven repository to your project level build.gradle.kts:

allprojects {
   repositories {
       google()
       mavenCentral()
       maven("https://dl.bintray.com/kotlin/exposed")
   }
}

In you Versions.kt declare relevant dependencies:

object Versions {
const val EXPOSED_VERSION = "0.36.2"
. . .
object Jvm {
const val JETBRAINS_EXPOSED_CORE = "org.jetbrains.exposed:exposed-core:$EXPOSED_VERSION"
const val JETBRAINS_EXPOSED_DAO = "org.jetbrains.exposed:exposed-dao:$EXPOSED_VERSION"
const val JETBRAINS_EXPOSED_JDBC = "org.jetbrains.exposed:exposed-jdbc:$EXPOSED_VERSION"
}

And then use them in your backend module:

implementation(Versions.Jvm.JETBRAINS_EXPOSED_CORE)
implementation(Versions.Jvm.JETBRAINS_EXPOSED_DAO)
implementation(Versions.Jvm.JETBRAINS_EXPOSED_JDBC)

Don’t forget to sync your project. Now that all dependencies are implemented, let’s get down to designing our database.

Our SQL database will have three tables: Ingredients, Instructions and Recipes. Every Entity in Exposed DAO has a table which extends IntIdTable and defines the relationships between variables. Then we can use those relationships to create Entities and transform them into classes which we can later use in our application.

First, let’s create tables for each class. Add a new package called storage to your backend package. Then add a package called exposed inside the storage package. Prepare your project structure for the changes to look like this:

add exposed package

Then add the following tables in relevant files and perform necessary import from org.jetbrains.exposed along the way:

internal object IngredientTable : IntIdTable("ingredients") { [1]
   val name = varchar("name", 100) [2]
   val amount = decimal("amount", 2, 1) [3]
   val metric = varchar("metric", 100)
   val recipe = reference( [4]
       "recipe_id",
       RecipeTable,
       onDelete = ReferenceOption.CASCADE,
       onUpdate = ReferenceOption.CASCADE
   ) [5]
} 

internal object InstructionTable : IntIdTable("instructions") {
   val order = integer("order") [3]
   val description = varchar("description", 100)
   val recipe = reference(
       "recipe_id",
       RecipeTable,
       onDelete = ReferenceOption.CASCADE,
       onUpdate = ReferenceOption.CASCADE
   )
}

internal object RecipeTable : IntIdTable("recipes") {
   val title = varchar("title", 100) 
}

[1] We create a table which gets Integer ids automatically and name it “ingredients”. This table holds such properties as name, amount, metric (like in our data class that we defined before). Similarly, the instruction table holds previously defined properties. Recipe table only has a title (read on to find out what to do with instructions and ingredients).
[2] When we need to create a text property, we use varchar.
[3] For decimal/double values we use decimal and integer for integer values.
[4] Now, our recipes actually should have a list of ingredients and instructions. In this simplified database every “ingredient” and “instruction” comes up only in one recipe, but a recipe has a number of ingredients and instructions. Therefore we define a reference in our “ingredients” and “instructions” tables that will point to the recipe where they are used.
[5] onDelete and onCascade properties tell us how these entries in the table will behave if our parent gets removed. In this case, if the recipe is deleted, the ingredients and instructructions that reference that recipe also get deleted.

Now we can build our entities based on these relations. Add the following code to IngredientEntity.kt:

class IngredientEntity(id: EntityID<Int>) : IntEntity(id) { [1]
   companion object : IntEntityClass<IngredientEntity>(IngredientTable)

   var name by IngredientTable.name
   var amount by IngredientTable.amount
   var metric by IngredientTable.metric
   var recipe by RecipeEntity referencedOn IngredientTable.recipe [2]

}

fun IngredientEntity.toIngredient() = Ingredient( [3]
   id.value.toLong(),
   name,
   amount.toDouble(),[4]
   metric
)

[1] Create an Entity based on the relations in IngredientTable. The Long id property is generated automatically.
[2] Here we specify the Recipe Entity where the instruction comes up, using a reference to the Recipe table that we’ve created
[3] Now we can map our database Entity to our class that we defined earlier so that we can later use it in the application
[4] The amount is stored as BigDecimal in the database, but we want to use the Double, so we need to convert it

As you can see, now our data classes don’t really match with the entities because of ids. Let’s complete our data classes with ids as follows:

data class Recipe(
    val id: Long = 0,
    val title: String,
    val ingredients: List<Ingredient>,
    val instructions: List<Instruction>
)

Add an id to Ingredient and Instruction as well.

data class Ingredient(
    val id: Long = 0,
    val name: String,
    val amount: Double,
    val metric: String
)

data class Instruction(
    val id: Long = 0,
    val order: Int,
    val description: String,
)

Do the same procedure for Instructions. Don’t worry if some things are not resolved at first, we’ll get back to them later.

class InstructionEntity(id: EntityID<Int>) : IntEntity(id) { 
   companion object : IntEntityClass<InstructionEntity>(InstructionTable)

   var order by InstructionTable.order
   var description by InstructionTable.description
   var recipe by RecipeEntity referencedOn InstructionTable.recipe 
}

fun InstructionEntity.toInstruction() = Instruction( 
   id.value.toLong(),
   order,
   description 
)

We only need to create RecipeEntity itself now. Here we’ll need to use those relationships that we’ve defined in Ingredients and Instructions.

class RecipeEntity(id: EntityID<Int>) : IntEntity(id) { 
   companion object : IntEntityClass<RecipeEntity>(RecipeTable)

   var title by RecipeTable.title
   val ingredients by IngredientEntity referrersOn IngredientTable.recipe [1]
   val instructions by InstructionEntity referrersOn InstructionTable.recipe
}

fun RecipeEntity.toRecipe() = Recipe(
   id.value.toLong(),
   title,
   ingredients.map{it.toIngredient()}, [2]
   instructions.map{it.toInstruction()}
)

[1] Here we collect all the ingredients and instructions that reference this recipe and create a SizedIterable<IngredientEntity> or SizedIterable<InstructionEntity>
[2] When transforming our entity to a data class, we need to map our SizedInterable to a List

Fix missing imports in all tables and entities, if there are any left - this will make all previous unresolved references go away.

So far so good! We’ve made the necessary preparations to set up our local database.
In the next step we’ll proceed with postgresSQL.

Step 4: Build and test Ktor application with shadowJar

By now you must be very excited to try it out. At least I am. Teaching yourself software development always leaves room for surprise. What if it doesn’t work? 😰

To build and test our backend we’ll use a jar file, which we will create with the help of shadowJar library.

First, let’s add the Shadow Jar version to the Versions.kt:

const val SHADOW_JAR_VERSION = "7.1.1"

And then to configure it in backend build.gradle.kts add the following to the plugins section:

id("com.github.johnrengelman.shadow") version Versions.SHADOW_JAR_VERSION

This will automatically add a shadowJar task to our project and configure it to include the sources from the main sourceSet. We will also configure the task to set the name and output extension. To do so, first add an import to the top of your build.gradle.kts:

import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar

And then add the configuration after your dependencies in build.gradle.kts:

tasks.withType<ShadowJar> {
   archiveBaseName.set("Backend")
   archiveClassifier.set("")
   archiveVersion.set("")
   isZip64 = true
}

Before we can test our backend let’s implement simple routing. In the Application function install the routing:

import io.ktor.application.*
import io.ktor.routing.*

internal fun Application.module() {
   install(Routing) {
       api()
   }
}

Immediately create a separate Api.kt file, where we’ll list all the routes like this:

import io.ktor.application.*
import io.ktor.response.*
import io.ktor.routing.*

internal fun Routing.api() {
   hello()
}

private fun Routing.hello() {
   get("/hello") {
       call.respondText("Hello!")
   }
}

Now
(1) rebuild
(2) make the project

rebuild and make

If you get a junit error, then just delete the test folder from the backend module - we will not need it. And try again.

Then check the build folder: here it is, our Backend.jar 🤩

backend jar

Let’s try to start the backend from the command line. First, open the terminal window and go to your KMP project. As mentioned above, we can always pass required parameters to our server application through environment variables. For example, we can override the port that we specified in application.conf:

java -jar ./backend/build/libs/backend.jar -port=9000

Then open your web browser at localhost:9000/hello. If it greets you, then it’s a good sign!

hello

Congrats, we’ve created a simple backend REST API with our first get function!
You can reward yourself with a slice of pizza now.

Step 3: Add a Recipe model + set up a Ktor server

Now that we’ve made sure the app is alive, let’s start building up our backend. We’ll have a recipe collection, so we’ll need a recipe, which consists of a list of ingredients and a list of instructions.

A simplified model of our recipes will look like this:

data class Recipe(
   val title: String,
   val ingredients: List<Ingredient>,
   val instructions: List<Instruction>
)

data class Ingredient( 
   val name: String,
   val amount: Double,
   val metric: String
)

data class Instruction( 
   val order: Int,
   val description: String
)

Let’s create a model directory in our backend module and add our data classes in a file called Entity.kt.

add models

That’s good enough of a model. Realistic, but not too complicated.
What’s next?
recipe

Next we will set up a simple, but powerful server using the Ktor framework. Ktor is easy to use and has great multiplatform capabilities, which will benefit us a lot in the future when we get down to writing our networking client for the apps. First, let’s define Ktor dependencies in Versions.kt.

object Versions {
   const val KTOR_VERSION = "1.6.7"

   object Jvm {
       const val KTOR_AUTH = "io.ktor:ktor-auth:$KTOR_VERSION"
       const val KTOR_WEB_SOCKETS = "io.ktor:ktor-websockets:$KTOR_VERSION"
       const val KTOR_CLIENT_APACHE = "io.ktor:ktor-client-apache:$KTOR_VERSION"
       const val KTOR_SERVER_NETTY = "io.ktor:ktor-server-netty:$KTOR_VERSION"
       const val KTOR_SERIALIZATION = "io.ktor:ktor-serialization:$KTOR_VERSION"
   }
}

After syncing your gradle files, go to the backend module and if you have build.gradle instead of build.gradle.kts, change the extension, as we’re going to use Kotlin DSL throughout the project. Then add Ktor dependencies. Your build.gradle.kts should look like this:

plugins {
   kotlin("jvm")
}

dependencies {
   // Ktor
   implementation(Versions.Jvm.KTOR_CLIENT_APACHE)
   implementation(Versions.Jvm.KTOR_SERIALIZATION)
   implementation(Versions.Jvm.KTOR_SERVER_NETTY)
   implementation(Versions.Jvm.KTOR_AUTH)
   implementation(Versions.Jvm.KTOR_WEB_SOCKETS)
   implementation(Versions.Jvm.KTOR_CLIENT_APACHE)
}

Now we are ready to create our server. We’ll use embeddedServer and load configuration settings from the application.conf HOCON file, which we’ll place in the application resources. With Ktor you can configure various server parameters. When you create your server with embeddedServer you’ll need to specify the parameters in the constructor. Add Server.kt file to your backend and write the following:

import io.ktor.server.engine.commandLineEnvironment
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty

fun main(args: Array<String>) {
   embeddedServer(Netty, commandLineEnvironment(args)).apply {
       start()
   }
}

Now let’s add an application.conf file to resources directory in backend/main (you won’t need other resources in your backend module, so you can easily delete all other directories that were created automatically, like the drawables and mipmaps, as well as test, androidTest, AndroidManifest and proguard-rules):

add application conf

Add the following to your application.conf:

ktor {
deployment {
  port = 8080
  port = ${?PORT}
}

application {
  modules = [dev.tutorial.kmpizza.backend.MainKt.module]
}
}

Here we configure our port: we’ll use 8080 by default for local development and in the future will be able to set the PORT variable from our environment with port = ${?PORT}.

Pay attention to the module path matching your package name!

Now add Main.kt to your backend, next to Server.kt. This file will contain one function, corresponding to the module that we’ve just specified in our application.conf.

import io.ktor.application.*

internal fun Application.module() {
 
}

You will also need to specify your Server in the build.gradle.kts. Now your backend build.gradle.kts file should look as follows:

application {
   mainClass.set("dev.tutorial.backend.ServerKt")
}

plugins {
   kotlin("jvm")
   application
}

dependencies {
   // Ktor
   implementation(Versions.Jvm.KTOR_CLIENT_APACHE)
   implementation(Versions.Jvm.KTOR_SERIALIZATION)
   implementation(Versions.Jvm.KTOR_SERVER_NETTY)
   implementation(Versions.Jvm.KTOR_AUTH)
   implementation(Versions.Jvm.KTOR_WEB_SOCKETS)
   implementation(Versions.Jvm.KTOR_CLIENT_APACHE)
}

As always, sync the dependencies.

Now we’re done with the boring part: this basic Ktor setup will be enough to begin with. Soon we’ll be able to test our backend.

Step 2: Add a Backend module + buildSrc for dependency management

What do we need to serve our pizza?

Right! A backend.

And you know what, we can do that with Kotlin. So let’s start.

Add another module to our basic setup: backend. Click on the project and choose New -> Module. Name it “backend”. Choose “No Activity” when asked to create one.

add backend module name backend module

If you check out the project settings.gradle.kts, you will see that the backend module is included in the project now.

rootProject.name = "kmpizza"
include(":androidApp")
include(":shared")
include(":backend")

As our recipe collection is going to grow into a big project for multiple platforms, we’ll need to keep our dependencies clean and organised. To do so, we’ll setup buildSrc for dependency management. Add a new directory to the root folder and name it “buildSrc”.

add buildSrc

In this module create Versions.kt and build.gradle.kts to match the following file structure: add versions

In Versions.kt file you wil keep all the dependencies for different modules in this project.

Add the following to the build.gradle.kts file:

plugins {
   `kotlin-dsl`
}

repositories {
   gradlePluginPortal()
   maven("https://dl.bintray.com/kotlin/kotlinx/")
}

To make sure we haven’t broken anything yet, sync the project, choose androidApp configuration and run the app.

test androidApp

You’ll see a “Hello, Android!” on your device, which is an initial setup that comes with the KMM Application Android Studio Project.

If you want to try it in XCode, open iosApp.xcworkspace.

test iosApp

Then run it on a simulator and you’ll see a “Hello, iOS!” screen. Great, everything is still working 😉

Or is it? If you try running it on a real device you may get an error Signing for "iosApp" requires a development team. Select a development team in the Signing & Capabilities editor. Let’s quickly fix it. Select the project name in XCode and go to Signing&Capabilites in the project editor. There change your team to Personal Team, something like this:

signing and capabilities

Now you may see another error if you try running the app again: Command PhaseScriptExecution failed with a nonzero exit code 😱

No worries, we’ll fix it too. First, you need to sync the gradle project in Android Studio, then go to the iosApp directory in your terminal and run pod install there.

Now try running again. If you get a warning like this

trust developer ios

Then go to Settings -> General -> Device Management and choose to trust your developer account.

Run the app again. Finally we see the greeting on a real device!

And just like this, one step at a time we’ll continue building our multiplatform app. 🍕

Step 1: Create a Kotlin Multiplatform project in Android Studio

Everyone likes pizza, so let’s create a recipe app, where you can save all the yummy pizza recipes that you’ve tried and would like to recreate sometime in the future!

Our first step will be to create a Kotlin Multiplatform project.

  1. Open Android studio, go to File -> New -> New Project and create a KMM Application. create a KMM applicarion

  2. Choose a name for the application and the package, define your save location and click Next. choose a name

  3. In the following screen you’ll have to name your Android Application, iOS Application and shared Module. You don’t need to customize anything here, so just leave it as is and click Finish. app structure

Now we’ve got a basic setup for our future KMM project. This way we can keep all our project related Kotlin code in the same place. As you can see, it has an androidApp, where you can work on your Android App just as you usually would. You also have an iosApp that holds your ios application, which you can work with in XCode. Moreover, you have a shared module now, which is divided into androidMain, commonMain and iosMain. CommonMain will be the source of all your shared logic within the app, and the other two will hold platform specific implementations for expect functions and classes.