Chapters

Hide chapters

Dagger by Tutorials

First Edition · Android 11 · Kotlin 1.4 · AS 4.1

A. Appendix A: The Busso Server
Written by Massimo Carli

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

In this book, you learned everything you need to use Dagger and Hilt. You did this by working on several different apps, with Busso being the most complex of them. As mentioned in the first chapter, Busso needs a server: the BussoServer. This is a web application implemented using Ktor, which is the framework for implementing web services with Kotlin. In this last chapter, you’ll look at how:

  • The BussoServer works.
  • To implement dependency injection on the server using Koin.

Koin is a dependency injection framework implemented in Kotlin. A complete description of the Koin framework would require another book. In this case, you’ll just introduce dependency injection with Koin into the simple BussoServer app, to give you a look at a different approach.

The BussoServer app

As mentioned in the introduction, BussoServer is a Ktor app with the architecture in Figure 20.1:

Figure 20.1 — BussoServer’s High Level Architecture
Figure 20.1 — BussoServer’s High Level Architecture

To understand how this works, use IntelliJ and open the BussoServer project from the starter folder in the materials for this chapter. You’ll see the structure in Figure 20.2:

Figure 20.2 — BussoServer’s Project Structure
Figure 20.2 — BussoServer’s Project Structure

Now, open Application.kt in the main package and look at its content:

// 1
fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)

@KtorExperimentalLocationsAPI
@Suppress("unused") // Referenced in application.conf
fun Application.module() { // 2
  // 3
  install(Locations)
  // 4
  install(ContentNegotiation) {
    gson {
      setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
    }
  }
  // 5
  routing {
    get("/") { // 6
      call.respondText("I'm working!!", contentType = ContentType.Text.Plain)
    }
    // Features // 7
    findBusStop()
    findBusArrivals()
    myLocation()
    weather()
  }
}

This simple code is quite standard for Ktor. Here:

  1. You define main() for the app by instantiating the engine that implements all the routing logic for the server. Here, you’re using Netty.
  2. The server engine needs some configuration that you put into a module. You usually provide this information using a module() extension function.
  3. Ktor allows you to install and use different plugins for different purposes. In this case, you install the Locations feature. Despite its name, that feature has nothing to do with locations on a map. Rather, it gives you a type-safe way to define routing. You’ll see this in action later. Locations is experimental and requires the @KtorExperimentalLocationsAPI annotation.
  4. You use the ContentNegotiation feature, which allows you to manage JSON as the transport protocol. In this case, you use Gson as the parser.
  5. The routing is the logic that maps a specific URI pattern to some logic. You define routings in a routing block.
  6. Just for testing, if the server is up and running, you usually define a very simple endpoint for the root path /. In this case, you just return a simple text. Note how you send back content using respondText() on a call you get by invoking get() for a given path.
  7. The remaining function allows you to install other endpoints for different paths. You’ll see how to do this soon.

To launch the server locally, you just need to click on the run icon, as shown in Figure 20.3:

Figure 20.3 — Run BussoServer locally
Figure 20.3 — Run BussoServer locally

You’ll get an output ending with a log message similar to this:

2021-01-08 00:43:20.647 [main] INFO  Application - No ktor.deployment.watch patterns specified, automatic reload is not active
2021-01-08 00:43:21.393 [main] INFO  Application - Responding at http://0.0.0.0:8080

Now, open the browser and access http://127.0.0.1:8080. You’ll get the testing message that tells you that everything works, as in Figure 20.4:

Figure 20.4 — BussoServer is working
Figure 20.4 — BussoServer is working

The routing is responsible for mapping a request path to some logic the server needs to execute to return a valid response. The /findBusStop endpoint is a good example of how this works.

How /findBusStop works

The /findBusStop endpoint is one of four endpoints BussoServer provides. It’s the one responsible for returning the bus stop nearest to a given location. To see how it works, open FindBusStop.kt in the api package and look at the following code:

const val FIND_BUS_STOP = "$API_VERSION/findBusStop/{lat}/{lng}" // 1
private val busStopRepository = ResourceBusStopRepository() // 2

@KtorExperimentalLocationsAPI
@Location(FIND_BUS_STOP) // 3
data class FindBusStopRequest(
  val lat: Float,
  val lng: Float
)

@KtorExperimentalLocationsAPI
fun Route.findBusStop() { // 4
  get<FindBusStopRequest> { inputLocation -> // 5
    // If there's a radius we add it as distance
    val radius = call.parameters.get("radius")?.toInt() ?: 0
    call.respond(
      busStopRepository.findBusStopByLocation( // 6
        inputLocation.lat,
        inputLocation.lng,
        radius
      )
    )
  }
}
const val FIND_BUS_ARRIVALS = "$API_VERSION/findBusArrivals/{stopId}"
private val busArrivalRepository = RandomBusArrivalRepository()
private val busStopRepository = ResourceBusStopRepository()
// ...

Using Koin in BussoServer

Koin is a dependency injection framework completely implemented in Kotlin that doesn’t have any type of code generation. It defines a domain-specific language (DSL) for managing dependency injection in different kinds of applications: Kotlin, Android, Ktor and others.

Installing Koin dependencies

Open build.gradle in Figure 20.5:

Figure 20.5 — Gradle file for BussoServer
Yugehe 64.4 — Rvibgi deyo tuw RiqyeYidxut

// ...

dependencies {
  // ...

  implementation "org.koin:koin-core:$koin_version" // 1
  implementation "org.koin:koin-ktor:$koin_version" // 2

  testImplementation "org.koin:koin-test:$koin_version" // 3

  // ...
}
// ...

The repository implementations

BussoServer is a very simple app that uses the repository pattern. Look at the repository and repository.impl packages to see the definition in Figure 20.6:

Figure 20.6 — Repositories dependency diagram
Zagano 71.5 — Fijuzihoyeut hifenvidqp yuenzug

Creating a Koin module

Think back to the definition of a Dagger @Module: a fundamental concept you use to tell Dagger how to create objects for a given type. Koin modules are similar.

val repositoryModule = module { // 1
  single<BusStopRepository> { ResourceBusStopRepository() } // 2
  single<BusArrivalRepository> { RandomBusArrivalRepository() } // 3
}

Koin initialization in Ktor

Now, you need to tell Koin to use the bindings you just defined in repositoryModule. Open Application.kt and add the following definition:

@KtorExperimentalLocationsAPI
@Suppress("unused") // Referenced in application.conf
fun Application.module() {

  install(org.koin.ktor.ext.Koin) { // 1
    modules(repositoryModule) // 2
  }
  // ...
}
// ...

Injecting the repository implementation

Earlier, you installed the module with the bindings for BusStopRepository and BusArrivalRepository. But how do you inject them? That’s simple. Open FindBusStop.kt in the apis package and apply the following change:

const val FIND_BUS_STOP = "$API_VERSION/findBusStop/{lat}/{lng}"
// private val busStopRepository = ResourceBusStopRepository() // DELETE 1

// ...

@KtorExperimentalLocationsAPI
fun Route.findBusStop() {

  val busStopRepository: BusStopRepository by inject() // 2

  get<FindBusStopRequest> { inputLocation ->
    // ...
  }
}
const val FIND_BUS_ARRIVALS = "$API_VERSION/findBusArrivals/{stopId}"
// private val busArrivalRepository = RandomBusArrivalRepository() // DELETE 1
// private val busStopRepository = ResourceBusStopRepository() // DELETE 1
// ...
@KtorExperimentalLocationsAPI
fun Route.findBusArrivals() {

  val busStopRepository: BusStopRepository by inject()  // 2
  val busArrivalRepository: BusArrivalRepository by inject() // 2

  get<FindBusArrivalsRequest> { busStopInput ->
    // ..
  }
}
Figure 20.7 — Verify BussoServer is working
Kinowo 15.8 — Mizujb FoqfaHaxzev uc nitcugh

Adding other dependencies: Logger

In the previous section, you saw how to inject the implementation for two simple interfaces, BusStopRepository and BusArrivalRepository, which don’t have dependencies.

Adding the Logger abstraction

You just want to see how dependency injection works with Koin, so all you need is a simple abstraction for the Logger. Start by creating a new package named logging in the src folder for main and create a new file named Logger.kt in it with the following code:

interface Logger {

  fun log(msg: String)
}
class StdLoggerImpl : Logger {
  override fun log(msg: String) {
    println(msg)
  }
}

Creating a module for the Logger

To create a module for the Logger, create a new file named LoggerModule.kt in di with the following code:

val loggerModule = module { // 1
  factory<Logger> { StdLoggerImpl() } // 2
}

Installing the module for the Logger in Ktor

Defining loggerModule doesn’t install it in Ktor, but you already know how the installation works. Open Application.kt and add the following:

@KtorExperimentalLocationsAPI
@Suppress("unused") // Referenced in application.conf
fun Application.module() {

  install(org.koin.ktor.ext.Koin) {
    modules(repositoryModule)
    modules(loggerModule) // HERE
  }
  // ...
}

Creating dependencies

Suppose you now want to use the Logger in ResourceBusStopRepository and RandomBusArrivalRepository. To do this, you need to define the dependency by using — of course — constructor injection.

class ResourceBusStopRepository constructor(
  private val logger: Logger // 1
) : BusStopRepository {

  private val model: BusStopData

  init {
    logger.log("Initializing ResourceBusStopRepository: $this") // 2
    val jsonAsText = this::class.java.getResource(BUS_STOP_RESOURCE_PATH).readText()
    model = Gson().fromJson(jsonAsText, BusStopData::class.java).apply {
      items.forEach { butStop ->
        this@apply.stopMap[butStop.id] = butStop
      }
    }
  }

  override suspend fun findBusStopByLocation(
    latitude: Float,
    longitude: Float,
    radius: Int
  ): List<BusStop> {
    logger.log("findBusStopByLocation on $this with lat:$latitude lon: $longitude") // 3
    return mutableListOf<BusStop>().apply {
      (2..10).forEach {
        add(model.items[it])
      }
    }.sortedBy { busStop -> busStop.distance }
  }


  override suspend fun findBusStopById(budStopId: String): BusStop? =
    model.stopMap[budStopId]
}
// ...
/**
 * Number of arrivals for line
 */
fun arrivalNumberRange() = 0..nextInt(3, 10)
fun arrivalGroupRange() = 0..nextInt(1, 4)
// private val busStopRepository = ResourceBusStopRepository() // DELETE 1

/**
 * Implementation for the BusArrivalRepository which returns random values
 */
class RandomBusArrivalRepository constructor(
  private val busStopRepository: BusStopRepository, // 2
  private val logger: Logger // 3
) : BusArrivalRepository {
  override suspend fun findBusArrival(busStopId: String): List<BusArrivalGroup> {
    logger.log("Invoking findBusArrival for id: $busStopId on $this") // 4
    val busStop = busStopRepository.findBusStopById(busStopId)
    if (busStop == null) {
      return emptyList()
    }
    return mutableListOf<BusArrivalGroup>().apply {
      arrivalGroupRange().forEach {
        add(
          BusArrivalGroup(
            lineId = "1",
            lineName = lines.random(),
            destination = destinations.random(),
            arrivals = generateRandomBusArrival()
          )
        )
      }
    }
  }

}

Providing dependencies in modules

To resolve the dependencies, open RepositoryModule.kt in di and apply the following changes:

val repositoryModule = module {
  single<BusStopRepository> { ResourceBusStopRepository(get()) } // 1
  single<BusArrivalRepository> { RandomBusArrivalRepository(get(), get()) } // 2
}
Initializing ResourceBusStopRepository: com...ResourceBusStopRepository@6456c628
findBusStopByLocation on com...ResourceBusStopRepository@6456c628 with lat:1.0 lon: 2.0
findBusStopByLocation on com..ResourceBusStopRepository@6456c628 with lat:1.0 lon: 2.0
findBusStopByLocation on com..ResourceBusStopRepository@6456c628 with lat:1.0 lon: 2.0

Key points

  • BussoServer is Busso’s server app. It’s a Ktor app.
  • You can use dependency injection on a Ktor server using Koin, a fully Kotlin solution without code generation.
  • Like Dagger, Koin allows to install the definition of modules you need as a Ktor feature.
  • Using inject() as a property delegate, you can inject dependencies into a dependency target.
  • Using get(), you can manage transitive dependencies between different objects.
  • This chapter only scratches the surface of Koin as an example of an alternative framework for dependency injection in Kotlin.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now