Building a subscription tracker Desktop and iOS app with compose multiplatform - Configuring Notion
Tags: kotlin, kmp, compose, tutorial
Published: 2024-06-25 08:46:45

> Photo by <a href="https://unsplash.com/@allvar?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Carl Tronders</a> on <a href="https://unsplash.com/photos/a-dog-running-through-a-field-of-tall-grass-tJnG4sgZL6k?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Unsplash</a>
>
> If you want to check out the code, here's the repository:
https://github.com/kuroski/kmp-expense-tracker
 
## Introduction

In the [previous part](https://dev.to/kuroski/building-a-subscription-tracker-desktop-and-ios-app-with-compose-multiplatform-5feg), we bootstrapped our application and did some basic tweaking, if you followed along, you should have a static screen that lists our expenses.

![List screen result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eoi2moex7kt6829y8c87.png)

In this part, we will be adding some "sauce" by making things dynamic with Notion's databases.

Let's start!

## Configuring Notion

#### Creating the table

Notion will be our "database", so make sure to [sign up](https://www.notion.so/signup), for the next steps:

- Create a new page (there is a link on the sidebar menu for that)
![Create new page image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q3btd068cns3uwmefg3z.png)
- Then type `/database` and select the `Database - inline` option
  ![Notion database-inline command](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ekpw16y76349blt3gman.png)
- Now, we can edit the database `properties`
  ![Notion properties menu](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oawlpwqosl5lt0gxwd9l.png)
	- Rename the column `Name` to `Expense`
	- Remove `Tags` column
	- Create a new property and name it `Amount`, make sure it is a `Number` type, and select your currency in the `Number format` option
	  ![Amount column options](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tck3np7qu8j1vfuzrp42.png)

Great, now we have a place to fill in our data.
> In case you are having problems adding icons, you can right-click an item, and there is an `icons` property in the context menu
> ![Notion, adding icon through context menu](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o3w33p3bdonwg2oiog9y.png)

But, adding icons through this method is tedious 😅

To make things easier, you can create a template for new items, this way you can have an icon set by default, making new entries easier to create.

- Click on the `New` blue button arrow and select `New template`
  ![Notion "new template" menu](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g9pbytiyj43ull2t6w25.png)
  - Then you can provide some values, in this case, just provide a random icon with some placeholder text
    ![Notion template form](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l017dkt7981ji2ew3izl.png)
- From the template list, click on the "..." menu of the template you have just created and select the "Set as default" option
  ![Setting our template as default](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vns4jgjouwow8y1pfiwc.png)
- Now, every time you create a new entry, the icon will be already there and make your life easier when handling new data
  ![Creating a new entry with our new template](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/77mcytc2y797p56alrvk.png)

#### Generating access keys

Great, our database configuration is done, and you can already use this table to manage your expenses with Notion.

To use our database through Notion API, you will need an access key.

- Open https://developers.notion.com/
- Click on `View my integrations` menu, which is located at the top-right on the page
![View my integrations menu](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xaggo51snjxrxcczl1zf.png)
- After logging in, you will see a "My Integrations" page
- Click on the "Create new integration" option
- Select your workspace (it should be already pre-selected)
- Give it a name
- Make sure you gave `Read/Update/Insert content` capabilities (enter in the `Capabilities` menu

In the end, you should have something like this
![Notion integration](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f6a3vwz56b4poq20ed27.png)

![Capabilities menu](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nmvaui10jx5xjmkndy1d.png)


Finally, you can find your API token on the `Secrets` page, we will be using that to make the requests.

![Secrets page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y1pn12oxtk2brhgkkc68.png)

#### Testing out

First of all, we have created the integration, we still must "connect it" to the database.

To do that:
- Open the database we have created previously
  ![Expenses database](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/phixuasjtkfep5a492sm.png)
- Then connect it to the `Expenses` integration we have created
    ![Connecting the integration into the database](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bxtxk5cc2bdeqqn2z9dh.png)

Great, now that it is connected, we can test it by using Notion's [database query endpoint](https://developers.notion.com/reference/post-database-query).

- Open any HTTP client, like [IntelliJ IDEA HTTP Client plugin](https://www.jetbrains.com/help/idea/http-client-in-product-code-editor.html) or [Postman](https://www.postman.com/)
- Create a new POST HTTP request
- Set the Authorization Bearer Token to be the API token of the integration you created in previous steps
- Make sure you have the following headers set as well
  - `Notion-Version: "2022-06-28"`
  - `Content-Type: "application/json"`
- And fill in the URL
  `https://api.notion.com/v1/databases/<your-database-id>/query`
	- To find the database id, just go to the database page (like I showed on the previous step)
	- You will be able to see the database ID through the URL
	  `https://www.notion.so/<database-id>?v=non-interesting-things-here`

> If you got stuck, please check Notion's documentation about [setting up Authorization](https://developers.notion.com/docs/authorization)
  
![Querying databases with Postman](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a2laiw97gkddgcep1pc5.png)

Finally, we have our data 🎉

![Database query result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zonk0438c8f81egjpy1k.png)

#### Integrating Notion API with our app

To make requests to Notion's API, we will work with [Ktor](https://ktor.io/) to instantiate an HTTP client on our app.

``` kotlin
// composeApp/src/commonMain/kotlin/api/APIClient.kt

package api

import io.ktor.client.*
import io.ktor.client.engine.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.auth.*
import io.ktor.client.plugins.auth.providers.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.logging.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.utils.io.core.*
import kotlinx.serialization.json.Json

/**
 * Each platform has its own http client engine
 * I will give more details on this later
 */
expect fun clientEngine(): HttpClientEngine

/**
 * Here we have our main APIClient class.
 * 
 * Closeable is a Ktor interface indicating something
 * that must have a "close" method.
 * 
 * We will need that since we are instantiating an httpClient
 * and need to make sure it can be closed to avoid memory leak or further problems.
 */
class APIClient(
    private val token: String,
) : Closeable {
	/**
	 * Here we are defining our Ktor http client
	 * It has a delegated "lazy" property, to make sure
	 * its value is only computed on first access.
	 */
    private val httpClient: HttpClient by lazy {
        HttpClient(clientEngine()) {
			/**
			 * Notion API requires you to provide a "Notion-Version" header
			 */
            defaultRequest {
                header(NOTION_HEADER, NOTION_HEADER_VERSION)
                contentType(ContentType.Application.Json)
            }
            install(Logging) {
                logger = Logger.SIMPLE
                level = LogLevel.ALL
            }
            install(ContentNegotiation) {
                json(
                    Json {
                        ignoreUnknownKeys = true
                        prettyPrint = true
                    },
                )
            }
            install(Auth) {
                bearer {
                    loadTokens {
                        BearerTokens(accessToken = token, refreshToken = token)
                    }
                }
            }
        }
    }

    init {
        require(token.isNotEmpty()) { "Notion API token is required" }
    }

    companion object {
        const val NOTION_HEADER: String = "Notion-Version"
        const val NOTION_HEADER_VERSION: String = "2022-06-28"
        const val API_BASE_URL: String = "https://api.notion.com/v1"
    }

    override fun close() = httpClient.close()
}


```

Nice, we now must define the `expect-actual` declaration to getting the client engine.
For more information about it, you can [check it here](https://kotlinlang.org/docs/multiplatform-expect-actual.html), but to give an overview:

> `expect-actual` declaration allows you to access platform-specific API from KMP modules.
> In our case, each platform needs a different http engine; then we can define an "expected" function to be present on the "actual" platform
> This way, when we execute `clientEngine()` function here, the call will be made on the respective platform the code is running in.
>
> In other words, expect defines that we need an `HttpClientEngine`.
> And with actual, KMP wires this up with a platform-specific instance. 
>
> No need for the equivalent of compiler directives that you may see in languages like C++ or C#. KMP handles this platform-specific implementation for you.

Let's implement the `actual` functions on iOS and Desktop modules.
``` kotlin
// composeApp/src/desktopMain/kotlin/api/APIClient.jvm.kt

package api

import io.ktor.client.engine.HttpClientEngine
import io.ktor.client.engine.cio.CIO

actual fun clientEngine(): HttpClientEngine = CIO.create()

// composeApp/src/iosMain/kotlin/api/APIClient.ios.kt

package api

import io.ktor.client.engine.HttpClientEngine
import io.ktor.client.engine.darwin.Darwin

actual fun clientEngine(): HttpClientEngine = Darwin.create()
```

This is, in my opinion, one of the coolest things about KMP. 
Mostly, when working with other tooling, integration with other platforms is a pain. 
But here, you can work with platform-specific code in a seamless way.

Fleet will even show us some useful information about it
![Fleet info on expect-actual](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ze2a180qo69k2isisjeu.png)

Now we can finally make requests, let's first create a helper function to query Notion databases.

``` kotlin
// composeApp/src/commonMain/kotlin/api/APIClient.kt

/**
 * You can think of this like a branded type.
 *
 * This is not required for our use case, feel free to just handle `DatabaseId` as plain `String` if you wish, but I thought it would be interesting to bring this up.
 *
 * More details below.
 */
@Serializable
@JvmInline
value class DatabaseId(private val value: String) {
    override fun toString(): String {
        return value
    }
}

// ...

class APIClient(
    private val token: String,
) : Closeable {
	// ...
	
	suspend fun queryDatabaseOrThrow(
        databaseId: DatabaseId,
        query: QueryDatabaseRequest = QueryDatabaseRequest(),
    ): QueryDatabaseResponse =
        httpClient
            .post("$API_BASE_URL/databases/$databaseId/query") {
                setBody(query)
            }
            .body<QueryDatabaseResponse>()

    override fun close() = httpClient.close()
}
```

Since we used `DatabaseId` as an inline value class, remember to change the `ExpenseId` as well.

``` diff
// composeApp/src/commonMain/kotlin/Model.kt

- typealias ExpenseId = String
+ @Serializable
+ @JvmInline
+ value class ExpenseId(private val value: String) {
+     override fun toString(): String {
+         return value
+     }
+ }
```

{% details ℹ️ Why have DatabaseId and ExpenseId as inline value class?? %}

I thought it could be interesting to add `DatabaseId` as a branded type, as an example of how you can narrow your application types.


This is not particularly noticeable in our use case since we are dealing with fewer entities, but I think it could be useful to have an example in case you wish to expand this concept.

First of all, let's understand what branded types are:
> They allow us to create distinct types based on an existing underlying type. 
> This helps improve type safety, makes your code more explicit, and safe.

Let me illustrate better with an example.

Let's say we have two functions, one to search users by their ID and another one for expenses’ ID.

``` kotlin
fun searchUser(id: String) {
    // ...
}

fun searchExpense(id: String) {
    // ...
}
```

![Auto completion for searchUser and searchExpense](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8wz6muwuiue7t9yupqwf.png)

This is an innocent example, but normally we may have more functions to search several more entities.

The main issue here is that those functions accept any `String` parameter as the `id`, meaning scenarios like the following might occur.

``` kotlin
// somewhere in the code
val user: User = //....
val expense: Expense = // .....

searchExpense(expense.id) // success, this is a valid operation

searchExpense("Oops") // this is valid, but it will break, since "Oops" does not exist as id (I hope)
searchExpense(user.id) // this is valid as well, but we are going to return a expense that has the user id
```

At first glance, it seems like an obvious error, but along the years, I faced several problems that occurred due to cases like this.

We can improve this by adding branded types.

``` kotlin
@JvmInline
value class UserId(private val value: String)

@JvmInline
value class ExpenseId(private val value: String)

data class User(val id: UserId, val name: String)

fun searchUser(id: UserId) {
    // ...
}

fun searchExpense(id: ExpenseId) {
    // ...
}

init {
    val authenticatedUser = getAuthenticatedUser() // returns a `User`
    searchUser(authenticatedUser.id) // ok
    searchExpense(authenticatedUser.id) // error
}
```

![Branded types example](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i6tpa6q9qhnkdiwbz80k.png)

By simply creating a `UserId` and `ExpenseId` branded type, we now cannot send incorrect parameters.

{% enddetails %}

Since we are using the Kotlin serialization library, we must map out our requests and responses.

When building client side applications, personally, I prefer to keep API requests/responses on their own data classes and not try re-using them inside my applications. 

{% details ℹ️ Why having this "intermediate" model and not just directly map API results into our app model?? %}

In the client, I think API shouldn't be generally shaped for a specific screen nor directly used as your app domain.

I think it is nicer if we have a layer that would make sure the API result is what we expect. 

Furthermore, we can convert that data to the actual internal modeling of our app (even if the data is similar or equal).

Having this separation is useful because
- Your endpoint shouldn't be necessarily shaped FOR a specific screen 
	- I had several cases where one endpoint was built for screen X, but later on it was also used on screens Y and Z 
	- Every screen has its own requirements, the endpoint and|or screen are likely to change
- If the endpoint changes, you don't have to refactor your screen, do whatever you need, and pass down properties to follow the defined screen contract

I chose to use Notion for this purpose 
- their API is far from ideal 
- it is verbose
- you don't have control over it 
- it was built to be generic, and for our case it makes little sense to try creating a "generic" model to comport Notion's API
- they might change it, and you only have to adapt one thing from the application

This same rule goes to screens
- If you have an endpoint that returns an "Expense" with its details, it doesn't mean you should use this model to shape the "Edit expense" screen
- Form field values are nullable 
- The form might touch different entities other than the expense
- The shape of the form generally is different from your app modeling, a `name` field in the Expense class will always exist, but a `name` in the form is nullable
- Trying to generalise this might give you a hard time instead of having this separation

{% enddetails %}

Given that, we will have the following flow

1. Receive Notion API response
2. Serialize it into a "Response" data class
3. Convert the response data class into our actual App state

First, let's handle the `request` encoding and `response` decoding.

``` kotlin
// composeApp/src/commonMain/kotlin/api/QueryDatabaseRequest.kt

package api

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

/**
 * https://developers.notion.com/reference/post-database-query
 */
@Serializable
data class QueryDatabaseRequest(
    @SerialName("start_cursor")
    val startCursor: String? = null,
    @SerialName("page_size")
    val pageSize: Int? = 100,
) {
    init {
        pageSize?.let {
            require(it in 1..100) { "Illegal property, pageSize must be between 1 and 100" }
        }
    }
}

// composeApp/src/commonMain/kotlin/api/QueryDatabaseResponse.kt

package api

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class QueryDatabaseResponse(
	// https://developers.notion.com/reference/page
    val results: List<ExpensePageResponse>,
    @SerialName("next_cursor")
    val nextCursor: String? = null,
    @SerialName("has_more")
    val hasMore: Boolean,
)
```

The Notion database `results` property returns a list of [`pages`](https://developers.notion.com/reference/page).
Later on we will have to handle `page` request/response for the new/edit screens, so we will already create a `ExpensePageResponse` data class.

``` kotlin
// composeApp/src/commonMain/kotlin/api/ExpensePageResponse.kt

package api

import ExpenseId
import api.model.ExpensePageProperties
import api.model.IconProperty
import kotlinx.serialization.Serializable

@Serializable
data class ExpensePageResponse(
    val id: ExpenseId,
    val icon: IconProperty? = null,
    val properties: ExpensePageProperties,
)
```

And now comes the tricky part.

Notion Databases entries are pages, which are generic. 

In our case, they contain: 
- `id`
- `icon` that is an [`emoji object`](https://developers.notion.com/reference/emoji-object)
- and a [`map of properties`](https://developers.notion.com/reference/page-property-values).

Since we are requesting our `Expenses` database, we know there are only two properties

![Database properties](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eflr5wwyjhfyefckqjbe.png)

- `Expense` which is a [`title property`](https://developers.notion.com/reference/page-property-values#title)
- `Amount` which is a [`number property`](https://developers.notion.com/reference/page-property-values#number)

``` kotlin
// composeApp/src/commonMain/kotlin/api/model/IconProperty.kt

package api.model

import kotlinx.serialization.Serializable

/**
 * https://developers.notion.com/reference/emoji-object
 */
@Serializable
data class IconProperty(
    val emoji: String? = null,
    val type: String? = "emoji",
)

// composeApp/src/commonMain/kotlin/api/model/ExpensePageProperties.kt

package api.model

import api.serializers.MoneySerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class ExpensePageProperties(
    @SerialName("Expense")
    val expense: TitleProperty,
    @SerialName("Amount")
    val amount: NumberProperty,
)

@Serializable
class TitleProperty(val id: String, val title: List<Value>) {
    @Serializable
    data class Value(
        @SerialName("plain_text")
        val plainText: String,
    )
}

@Serializable
data class NumberProperty(
    @Serializable(with = MoneySerializer::class)
    val number: Int,
)

```

To finish up, `number property` from Notion uses float numbers, which is not what we want to work inside our app.

To address that, we can create a custom serializer.

``` kotlin
// composeApp/src/commonMain/kotlin/api/serializers/MoneySerializer.kt

package api.serializers

import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder

object MoneySerializer : KSerializer<Int> {
    override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Money", PrimitiveKind.INT)

    override fun serialize(
        encoder: Encoder,
        value: Int,
    ) = encoder.encodeFloat(value.toFloat() / 100)

    override fun deserialize(decoder: Decoder): Int = (decoder.decodeFloat() * 100).toInt()
}
```

Unfortunately, because of Notion limitation, there are no DB Integers, so we might still face rounding issues. 

But even though, I chose to do the conversion to at least work with integers inside the app.

Regarding modeling our API, we are done for now, we will have to revisit those files later on.

#### Injecting our client with Koin

Now it's time to use our `APIClient`, we could instantiate one in our ViewModel and move on, which is totally fine for our case. 

Still, we will have more places where `APIClient` is needed, so we would need to instantiate it in all those places as well.

We also have to provide the authentication token for the `APIClient`, which would require us to manually pass this parameter whenever necessary.

This is where `Koin` can help us out, from their website, Koin is a
> The pragmatic Kotlin & Kotlin Multiplatform Dependency Injection framework


{% details ℹ️ What is this dependency injection (DI)?? %}

Well... DI is nothing more than moving the responsibility of creating something to somewhere else, just like function params.

For example, let's say we have a function called every time we want to save a user.

``` kotlin
fun saveUser(user: User) {
	val service = MyApiClient(
		apiKey = "abc", 
		logger = KotlinLogging.logger {}
	)
	service.saveUser(user)
	// ... do some processing
}
```

This is simple enough, but `saveUser` function must know how to build `MyApiClient` instance, which can be something non-trivial and maybe requiring us to pass none relevant parameters to this function.

``` kotlin
fun saveUser(user: User, apiKey: String, logger: KLogger) {
	val service = MyApiClient(
		apiKey = apiKey,
		logger = logger
	)
	service.saveUser(user)
	// ... do some processing
}
```

In this case, `apiKey` and `logger` are something that doesn't seem to belong to `saveUser` function.
- imagine that maybe `saveUser` might make use of more services, which would require us to send more parameters
- Or other functions might also want to use `MyApiClient`, which would require us to inject `apiKey` and `logger` everywhere

DI is just moving this responsibility to somewhere else

``` kotlin
fun saveUser(user: User, service: MyApiClient) {
	service.saveUser(user)
	// ... do some processing
}
```

💥 you just did it, no need of "fancy jargon's" and concepts.

One other example is in cases where you want to write some test for this function, now we can even expand this.

``` kotlin
interface MyApiClient {
	fun saveUser(user: User): Unit
}

// --

fun saveUser(user: User, service: MyApiClient) {
	service.saveUser(user)
	// ... do some processing
}

// -- APP
class MyKtorApiClient(apiKey: String, logger: KLogger): MyApiClient {
  // ....
}

val myClient = MyKtorApiClient(
	apiKey = "abc",
	logger = KotlinLogging.logger {}
)
saveUser(
	user = User(), 
	service = myClient
)

// -- TEST

class MyInMemoryApiClient: MyApiClient {
  // ....
}

val myClient = MyInMemoryApiClient()
saveUser(
	user = User(), 
	service = myClient
)
```

There are libraries that help us out do fancier stuff with that, but in general... that's it!

{% enddetails %}

### Refactor `ExpensesScreenViewModel`

First of all, `ExpensesScreenViewModel` will receive an `APIClient`.

``` diff
// composeApp/src/commonMain/kotlin/ui/screens/expenses/ExpensesScreenViewModel.kt

- class ExpensesScreenViewModel : StateScreenModel<ExpensesScreenState>(
+ class ExpensesScreenViewModel(apiClient: APIClient) : StateScreenModel<ExpensesScreenState>(
    ExpensesScreenState(
        data = listOf(),
    ),
) {
```

### Handle environment variables

We can't hardcode our Notion API Token and Database id in the application, we need to provide them through a safer way.

There are a few ways we can handle environment variables, I like `dotenv` files, and we already injected them in our `composeApp/build.gradle.kts` through `buildkonfig`.

The neat part is that `BuildKonfig` generates a file at compile time with those variables that were configured there, it happens automatically when the kotlin compile task is triggered. 

In case you wish to manually generate it, run the task `./gradlew generateBuildKonfig`.

``` kotlin
// composeApp/build/buildkonfig/commonMain/org/expense/tracker/BuildKonfig.kt

package org.expense.tracker

import kotlin.String

internal object BuildKonfig {
  public val NOTION_TOKEN: String = "MY_TOKEN"

  public val NOTION_DATABASE_ID: String = "MY_DB_ID"
}
```

A great benefit is that it will work for any platform. 

Another approach would be to work with `expect-actual` functions, and handle that on each platform specifically.

I like creating my own utility function to get environment variables (despite having access to `BuildKonfig`).

I am doing this mostly because I am also learning about this technology, and I found little content over how people handle env variables. 

At least creating this kind of utility layer would allow me to tweak how I fetch my env vars and change at one place in case I find a different approach over `BuildKonfig`.

``` kotlin
// composeApp/src/commonMain/kotlin/utils/Env.kt

package utils

import api.DatabaseId
import org.expense.tracker.BuildKonfig

object Env {
    val NOTION_TOKEN: String
        get() {
            val notionToken = BuildKonfig.NOTION_TOKEN
            require(notionToken.isNotBlank()) { "You must provide a NOTION_TOKEN env variable" }
            return notionToken
        }

    val NOTION_DATABASE_ID: DatabaseId // hey, this is our branded type =D
        get() {
            val notionDatabaseId = BuildKonfig.NOTION_DATABASE_ID
            require(notionDatabaseId.isNotBlank()) { "You must provide a NOTION_DATABASE_ID env variable" }
            return notionDatabaseId
        }
}
```

Don't forget to add your `dotenv` files and configure gitignore. 
> Never ever commit them!

``` diff
// .gitignore

!*.xcworkspace/contents.xcworkspacedata
**/xcshareddata/WorkspaceSettings.xcsettings
+.env
```

``` bash
# .env.sample
NOTION_TOKEN=
NOTION_DATABASE_ID=

# .env
NOTION_TOKEN=secret_YOUR_SECRET
NOTION_DATABASE_ID=your_DB_ID
```

Then, we need to configure Koin itself.

``` kotlin
// composeApp/src/commonMain/kotlin/Koin.kt

import api.APIClient
import org.koin.dsl.module
import ui.screens.expenses.ExpensesScreenViewModel
import utils.Env

object Koin {
    val appModule =
        module {
	        /**
			 * Here we are creating a Koin module and asking
			 * > Hey Koin, when someone asks for you an `ApiClient`, please provide the return of this function, and make it a singleton.
			 */
            single<APIClient> { APIClient(Env.NOTION_TOKEN) }

	        /**
			 * Our list screen ViewModel won't be a singleton, we will always re-create it once the user navigates to the screen
			 * So in this case we are asking Koin to instantiate a `ExpensesScreenViewModel` every time someone asks Koin for it.
			 * The interesting bit here is that `apiClient` parameter will be resolved from the singleton we defined.
			 * Koin relies on the type and identifies which thing to inject.
			 */
            factory { ExpensesScreenViewModel(apiClient = get()) }
        }
}

```

To make it clear...
> all the dependencies are resolved at compile time and based on the typing 

Making an analogy if you are used to something like PHP, what we did would be like this in a Laravel app:

``` php
<?php

namespace App\Providers;

// ...imports

class MyServiceProvider extends ServiceProvider
{
	public function register(): void	
	{
		$this->app->singleton(
			APIClient::class, 
			fn (Application $app) => new APIClient(env('NOTION_TOKEN')
		);
		
		// Adds a singleton `Connection`
		$this->app->bind(
			ExpensesScreenViewModel::class, 
			fn (ContainerInterface $container) => new ExpensesScreenViewModel($container->get(APIClient::class));
		);
	}
}
```

And now we have to integrate Koin with our app

``` diff
// composeApp/src/commonMain/kotlin/App.kt

@Composable
fun App() {
+    KoinApplication(
+        application = {
+            modules(Koin.appModule)
+        },
+    ) {
        AppTheme {
            Surface(
                modifier = Modifier.fillMaxSize(),
                color = MaterialTheme.colorScheme.background,
            ) {
                Scaffold {
                    Navigator(ExpensesScreen) { navigator ->
                        SlideTransition(navigator)
                    }
                }
            }
        }
    }
+}
```

``` diff

// composeApp/src/commonMain/kotlin/ui/screens/expenses/ExpensesScreen.kt

- import cafe.adriel.voyager.core.model.rememberScreenModel
+ import cafe.adriel.voyager.koin.getScreenModel

object ExpensesScreen : Screen {
    @Composable
    override fun Content() {
-        val viewModel = rememberScreenModel { ExpensesScreenViewModel() }    
+        val viewModel = getScreenModel<ExpensesScreenViewModel>()
        val state by viewModel.state.collectAsState()
```

And finally, we can refactor `ExpensesScreenViewModel` to actually use the `APIClient`

``` kotlin
package ui.screens.expenses

import Expense
import api.APIClient
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.launch
import utils.Env

private val logger = KotlinLogging.logger {}

data class ExpensesScreenState(
    val data: List<Expense>,
) {
    val avgExpenses: String
        get() = data.map { it.price }.average().toString()
}

class ExpensesScreenViewModel(apiClient: APIClient) : StateScreenModel<ExpensesScreenState>(
    ExpensesScreenState(
        data = listOf(),
    ),
) {
    init {
        screenModelScope.launch {
            logger.info { "Fetching expenses" }
            val database = apiClient.queryDatabaseOrThrow(Env.NOTION_DATABASE_ID)
            val expenses = database.results.map {
                Expense(
                    id = it.id,
                    name = it.properties.expense.title.firstOrNull()?.plainText ?: "-",
                    icon = it.icon?.emoji,
                    price = it.properties.amount.number,
                )
            }
            mutableState.value = ExpensesScreenState(
                data = expenses
            )
        }
    }
}
```

If you run the app, we finally have data coming from Notion.
![Dynamic list](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2if09mhkavula54f8046.png)

We are finally fetching dynamic data, but there are quite a few things left to do in our list screen.

In the next part of this series, we will handle user feedback by handling loading/error/success states and tweaking the UI a bit to display monetary values properly.

Thank you so much for reading, any feedback is welcome, and please if you find any incorrect/unclear information, I would be thankful if you try reaching out.

See you all soon.

![Time to write the third part](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4p6vm3wj18u1ez6yidxa.png)