Chapter 2: Koin and KMongo

Kotlin Multiplatform Mobile

link Koin and KMongo

Introduction

Now that we have created a working Ktor GraphQL service, it's time to introduce our backend service's real architecture. This service will be organized into four components, which will interact with one another over a shared network. The goal here is to keep our backend service organized and modular, which supports testing and making changes on the fly.

We are creating what is known as "Service Oriented Architecture," albeit on a much smaller scale.

Service-oriented architecture aims to build applications more quickly in response to new business opportunities.

Source: IBM

Let's take a look at how this applies to our project with the following diagram:

At every level, each module acts as an independent part of the overall application. These modules rely on external libraries to perform operations and power the application. Once we start to build services, this process will become easier to understand.

So far, we already covered Ktor and KGraphQL. Now it is time to introduce Koin and KMongo.

link Insert Koin

Koin is a Dependency Injection library that is both simple and effective. Let's take this opportunity to understand Koin and Dependency Injection in more detail.

Why DI?

Dependency injection, or DI allows developers to call modules only when they are needed. As they say in Hollywood, "Don't Call Us, We'll Call You."

When using DI it is important to bear in mind the different types of injection. Let's see some examples:

  1. No DI
class Car {

    private val engine = Engine()

    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val car = Car()
    car.start()
}

Source: Dependency injection in Android

In the above example, we initialize an instance of the Car class's Engine each time it is created.

The problem with this implementation is that the Engine becomes tightly coupled to the Car class, and will prevent us from testing the Car class in isolation. We cannot create a mock Engine class for unit testing.

  1. Manual DI
class Car(private val engine: Engine) {
    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val engine = Engine()
    val car = Car(engine)
    car.start()
}

Source: Dependency injection in Android

In the above example, the app creates an instance of Engine and uses it construct an instance of Car. This is known as Constructor Injection, because we pass the dependencies of a class to its constructor.

  1. DI Library

Based on whichever DI library you choose, your milage may vary. In our backend project, we will utlize Koin, so the Car sample would look like the following:

/**
 * Car Class
 * use Engine
 */
class Car : KoinComponent {

    // Inject Engine
    val engine: Engine by inject()

    // display our data
    fun start() { 
        engine.start()
    }
}

With Manual DI or a DI library, we can inject modules and remove tightly-coupled classes.

The following graph illustrates the relationship between Injectors, Clients and Services.

In our backend project, Koin will act as the injector and provide the networking client for all of our services.

Benefits of DI

Some of you may be wondering what are the benefits of this approach, so here they are:

  • Testability: Objects are isolated from their dependencies
  • Readability: DI can create patterns with less boilerplate
  • Flexibility: Reusable modules are extensible and testable
  • Maintainability: Easier to test modules and collaborate

Why Koin?

Koin provides us with the ability to describe our app, instead of annotating or generating code for it.

Getting started with Koin is a two-step process.

  1. Start a KoinApplication using startKoin:
// start a KoinApplication in Global context
startKoin {
    // declare used logger
    logger()
    // declare used modules
    modules(coffeeAppModule)
}

Source: Starting Koin

In the above code, startKoin will initialize an entry point for all of our application modules and register it in Koin's GlobalContext.

  1. Use the module function to declare a Koin module. A Koin module is the space to declare all your components:
val mainModule = module {
   // your dependencies here
}

We will use Koin to initialize a global client for our database, powered by MongoDB Atlas. Before getting started with Koin, let's introduce KMongo.

link KMongo

We will use KMongo to handle connecting to a live database. KMongo allows us to retrieve or modify data stored in a database provided by MongoDB Atlas.

In order to proceed, first you will need to create a free MongoDB Atlas account.

MongoDB Atlas

We recommend creating a free tier database cluster at MongoDB Atlas. Sign up for MongoDB Atlas and follow this simple tutorial to create a free cluster.

MongoDB Atlas is the global cloud database service for modern applications. Deploy fully managed MongoDB across AWS, Azure, or GCP. Best-in-class automation and proven practices guarantee availability, scalability, and compliance with the most demanding data security and privacy standards. Use MongoDB’s robust ecosystem of drivers, integrations, and tools to build faster and spend less time managing your database.

We are going to be using an instance of the MongoDB Atlas database.

  1. Navigate to the MongoDB Atlas page
  2. Click "Start Free" and sign up for the MongoDB account
  3. On the "Projects" page click on "New Project" give it a name and create
  4. Add Members. You’re already a member -> hit continue
  5. Build Cluster -> Select Free Tier
  6. Select Cloud Provider & Region and Create Cluster

Builder Book

  1. After the cluster was initialized click on "connect"

Builder Book

  1. Choose a connection method -> Select Connect Your Application and select Node.js

  2. Copy and save your connection string, which should look like the following:

mongodb+srv://<dbname>:<password>@cluster0-yvwjx.mongodb.net/<dbname>?retryWrites=true&w=majority

Keep that connection string saved somewhere handy. Later on, we will store the connection string as a new environment variable called MONGO_URI.

link Koin and KMongo

In this section, we will continue developing with Koin, a dependency injection library.

Start by creating a new directory called di in the src directory. Inside the di directory, create a file named MainModule.kt and add the following:

import org.koin.dsl.module
import org.litote.kmongo.KMongo

val mainModule = module(createdAtStart = true) {
    factory { KMongo.createClient(System.getenv("MONGO_URI") ?: "") }
}

In the above code, we create a global KMongo client that will connect to a MongoDB database. We use the factory function provided by Koin to provide a factory bean definition. A factory bean is a pattern to encapsulate object construction logic in a class. In this case it will be used to encode the construction of a KMongo client in a reusable way.

We are now ready to insert our MONGO_URI connection string as an environment variable. At the root of the project, create a filed name .env and insert the following:

MONGO_URI=mongodb+srv://<dbname>:<password>@cluster0-yvwjx.mongodb.net/<dbname>?retryWrites=true&w=majority

Be sure to replace the example with your actual connection string.

You may also need to configure the environment variable as JVM args in IntelliJ.

link Start Koin

To start Koin in the context of our application, add the following to src/Application.kt:

fun Application.module(testing: Boolean = false) {

    startKoin {
        modules(mainModule)
    }

    install(GraphQL) {
        // ...
    }
}

Now that the Koin application context has been created, we can begin to create services for our project. Let's begin with making some updates to our RepositoryInterface.

link Repository Interface Updates

One thing that always comes to mind when coding is the DRY principle: Don't Repeat Yourself. During this project, I wanted to make it a point to omit any duplicate code snippets.

To omit duplicate code snippets for our repositories, I added the logic to create, read, update and destroy models in the RepositoryInterface.

Let's add the same CRUD logic and reduce the code we have to write later on.

Back in src/repository/RepositoryInterface, make the following changes:

import com.example.models.Model
import com.mongodb.client.MongoCollection
import org.litote.kmongo.eq
import org.litote.kmongo.findOne
import org.litote.kmongo.updateOne
import java.lang.Exception

interface RepositoryInterface<T> {

    var col: MongoCollection<T>

    fun getById(id: String): T {
        return try {
            col.findOne(Model::id eq id)
                    ?: throw Exception("No item with that ID exists")
        } catch (t: Throwable) {
            throw Exception("Cannot get item")
        }
    }

    fun getAll(): List<T> {
        return try {
            val res = col.find()
            res.asIterable().map { it }
        } catch (t: Throwable) {
            throw Exception("Cannot get all items")
        }
    }

    fun delete(id: String): Boolean {
        return try {
            col.findOneAndDelete(Model::id eq id)
                    ?: throw Exception("No item with that ID exists")
            true
        } catch (t: Throwable) {
            throw Exception("Cannot delete item")
        }
    }

    fun add(entry: T): T {
        return try {
            col.insertOne(entry)
            entry
        } catch (t: Throwable) {
            throw Exception("Cannot add item")
        }
    }

    fun update(entry: Model): T {
        return try {
            col.updateOne(
                    Model::id eq entry.id,
                    entry
            )
            col.findOne(Model::id eq entry.id)
                    ?: throw Exception("No item with that ID exists")
        } catch (t: Throwable) {
            throw Exception("Cannot update item")
        }
    }
}

In the above code, we added a KMongo collection and interface that takes a generic type definition. A few key notes:

  1. The KMongo collection col is responsible for connecting to the database.
  2. We use the base Model interface to abstract out any logic related to specific models. This makes the interface methods reusable across model classes.

You may begin to see the benefits of the base Model interface. Using Kotlin type reflection, we can pass in a generic model <T>. As long as a model inherits from the Model interface, we can pass in the id property and retrieve or modify data.

link Dessert Repo Updates

Back in src/repository/DessertRepository.kt, the client will connect to a MongoDB database and get an existing collection. The rest of the CRUD logic is handled by the RepositoryInterface.

import com.example.models.Dessert
import com.mongodb.client.MongoClient
import com.mongodb.client.MongoCollection
import org.litote.kmongo.*
import kotlin.Exception

class DessertRepository(client: MongoClient) : RepositoryInterface<Dessert> {
    override lateinit var col: MongoCollection<Dessert>

    init {
        val database = client.getDatabase("test")
        col = database.getCollection<Dessert>("Dessert")
    }
}

The above code no longer uses a local in-memory database, so we can safely delete src/data/database.kt.

link Dessert Service

The goal of each service is to define what operations to perform on our database. For desserts, we will define several functions and create an instance of our DessertRepository.

Create a new directory called services and create a new file inside services called DessertService.kt and add the following:

import com.example.models.Dessert
import com.example.models.DessertInput
import com.example.repository.DessertRepository
import com.mongodb.client.MongoClient
import org.koin.core.KoinComponent
import org.koin.core.inject
import java.util.UUID

class DessertService: KoinComponent {
    private val client: MongoClient by inject()
    private val repo: DessertRepository = DessertRepository(client)

    fun getDessert(id: String): Dessert {
        return repo.getById(id)
    }

    fun createDessert(dessertInput: DessertInput, userId: String): Dessert {
        val uid = UUID.randomUUID().toString()
        val dessert = Dessert(
                id = uid,
                userId = userId,
                name = dessertInput.name,
                description = dessertInput.description,
                imageUrl = dessertInput.imageUrl
        )
        return repo.add(dessert)
    }

    fun updateDessert(userId: String, dessertId: String, dessertInput: DessertInput): Dessert {
        val dessert = repo.getById(dessertId)
        if (dessert.userId == userId) {
            val updates = Dessert(
                    id = dessertId,
                    userId = userId,
                    name = dessertInput.name,
                    description = dessertInput.description,
                    imageUrl = dessertInput.imageUrl
            )
            return repo.update(updates)
        }
        error("Cannot update dessert")
    }

    fun deleteDessert(userId: String, dessertId: String): Boolean {
        val dessert = repo.getById(dessertId)
        if (dessert.userId == userId) {
            return repo.delete(dessertId)
        }
        error("Cannot delete dessert")
    }
} 

Dessert Schema Updates

The DessertSchema now has a dependency on the DessertService, which in turn relies on the DessertRepository. Update src/graphql/DessertSchema.kt as follows:

import com.apurebase.kgraphql.Context
import com.apurebase.kgraphql.schema.dsl.SchemaBuilder
import com.example.models.Dessert
import com.example.models.DessertInput
import com.example.services.DessertService

fun SchemaBuilder.dessertSchema(dessertService: DessertService) {

    inputType<DessertInput> {
        description = "The input of the dessert without the identifier"
    }

    type<Dessert> {
        description = "Dessert object with attributes name, description and imageUrl"
    }

    query("dessert") {
        resolver { dessertId: String ->
            try {
                dessertService.getDessert(dessertId)
            } catch (e: Exception) {
                null
            }
        }
    }

    mutation("createDessert") {
        description = "Create a new dessert"
        resolver { dessertInput: DessertInput, ctx: Context ->
            try {
                val userId = "abc"
                dessertService.createDessert(dessertInput, userId)
            } catch (e: Exception) {
                null
            }
        }
    }

    mutation("updateDessert") {
        description = "Updates a dessert"
        resolver { dessertId: String, dessertInput: DessertInput, ctx: Context ->
            try {
                val userId = "abc"
                dessertService.updateDessert(userId, dessertId, dessertInput)
            } catch (e: Exception) {
                null
            }
        }
    }

    mutation("deleteDessert") {
        description = "Deletes a dessert"
        resolver { dessertId: String, ctx: Context ->
            try {
                val userId = "abc"
                dessertService.deleteDessert(userId, dessertId)
            } catch(e: Exception) {
                false
            }
        }
    }
}

Application Updates

To wire up the updated DessertSchema.kt, return to src/Application.kt and insert the following:

import com.example.services.DessertService

// ...

startKoin {
    modules(mainModule)
}

install(GraphQL) {
    val dessertService = DessertService()
    playground = true
    schema {
        dessertSchema(dessertService)
    }
}

The backend now supports a new repository structure: Application -> Schema -> Service -> Repository.

At this point, make sure the application is able to build and run successfully.

link Pagination

Let's take this opportunity to introduce a new feature: Pagination.

Specifically, we are going to implement limit-offset pagination. This will reduce the initial load for the frontend, and increase the speed of subsequent responses.

In order to implement a new response, we will work our way backwards: Repository -> Service -> Schema.

DessertPage and PagingInfo

Starting in src/models/Dessert.kt, we will add two new models to handle paging desserts:

// 1
data class PagingInfo(var count: Int, var pages: Int, var next: Int?, var prev: Int?)

// 2
data class DessertsPage(val results: List<Dessert>, val info: PagingInfo) 

Here's what the above code defines:

  1. The PagingInfo data class provides the attributes for paged responses.

  2. The DessertsPage data class is a wrapper for returning a list of desserts and paging info.

Dessert Repository

Now that we added the PagingInfo and DessertsPage classes, we can start to implement the paginated response.

Return to src/repository/DessertRepository.kt, and insert the following:

fun getDessertsPage(page: Int, size: Int): DessertsPage {
    try {
        val skips = page * size
        val res = col.find().skip(skips).limit(size)
                ?: throw Exception("No desserts exist")
        val results = res.asIterable().map { it }
        val totalDesserts = col.estimatedDocumentCount()
        val totalPages = (totalDesserts / size) + 1
        val next = if (results.isNotEmpty()) page + 1 else null
        val prev = if (page > 0) page - 1 else null
        val info = PagingInfo(totalDesserts.toInt(), totalPages.toInt(), next, prev)
        return DessertsPage(results, info)
    } catch (t: Throwable) {
        throw Exception("Cannot get desserts page")
    }
}

Source: mongodb.com

The above code handles skipping over database nodes and fetching the number of requested desserts. We can also return a PagingInfo object with the following attributes: totalDesserts, totalPages, next, prev.

Dessert Service

In order to call the getDessertsPage function, we will wire it up to our DessertService.

Return to src/repository/DessertService.kt, and insert the following:

fun getDessertsPage(page: Int, size: Int): DessertsPage {
    return repo.getDessertsPage(page, size)
}

Nothing fancy here, just sending the necessary arguments along to the repository. Where exactly are these two arguments coming from?

Dessert Schema Updates

We will wire up the arguments for a paginated request in the DessertSchema.

Return to src/graphql/DessertSchema.kt and insert the following:

query("desserts") {
    resolver { page: Int?, size: Int? ->
        try {
            dessertService.getDessertsPage(page ?: 0, size ?: 10)
        } catch (e: Exception) {
            null
        }
    }
}

Both page and size are optional arguments, so we can reference them or pass in default values.

Test Mutations

Fire up the project and visit the playground at localhost:8080/graphql.

Before testing paginated queries, we will need to create new dessert data.

Run the createDessert mutation several times and create at least five new desserts.

Test Queries

Insert the following paginated query:

query GetDesserts($page: Int, $size: Int) {
  desserts(page: $page, size: $size) {
    results {
      id
      name
      description
      imageUrl
    }
    info {
      count
      pages
      next
      prev
    }
  }
}

In the query variables section, insert the following:

{"page": 0, "size": 10}

If you were able to fetch a paginated list of desserts, well done!

Today we successfully implemented new services and Koin components.

Up next, we will introduce a new reviews feature.

link References

format_list_bulleted
help_outline