A.
Appendix A: The Busso Server
Written by Massimo Carli
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:
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:
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:
- You define
main()for the app by instantiating the engine that implements all the routing logic for the server. Here, you’re using Netty. - The server engine needs some configuration that you put into a module. You usually provide this information using a
module()extension function. - 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.
Locationsis experimental and requires the@KtorExperimentalLocationsAPIannotation. - You use the ContentNegotiation feature, which allows you to manage JSON as the transport protocol. In this case, you use Gson as the parser.
- The routing is the logic that maps a specific URI pattern to some logic. You define routings in a
routingblock. - 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 acallyou get by invokingget()for a given path. - 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:
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:
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
)
)
}
}
The structure of this code is similar to that of the other endpoints. Here, you:
-
Define
FIND_BUS_STOPwith the path of the URL you invoke to access the endpoint. You pass the latitude and longitude in the input with thelatandlngquery parameters. -
Create
ResourceBusStopRepository, which is the object you need to query the bus stop for a specific location. It’s an implementation of theBusStopRepositoryinterface that simply reads the result from a JSON file in the resources folder. At the moment, the query always returns the same result, but that doesn’t matter for your purposes. :] -
Use
@Locationto defineFindBusStopRequestto encapsulate the input for the query. -
Define
findBusStop(). This is what you invoke in Application.kt to define the endpoint. -
When you receive a request, the input parameters are encapsulated into
FindBusStopRequest, which you receive as an implicit parameter forget(). -
Invoke
findBusStopByLocation()onbusStopRepositoryto get the result you send back to the client usingrespond()oncall.
Other than the specific logic, something might not look right in this code: In 2, you create an instance of ResourceBusStopRepository. This is a composition relationship, which you’ve learned is something to avoid. Even worse, you have the same relationship in FindBusArrival.kt.
Check that out by opening FindBusArrival.kt to find the following:
const val FIND_BUS_ARRIVALS = "$API_VERSION/findBusArrivals/{stopId}"
private val busArrivalRepository = RandomBusArrivalRepository()
private val busStopRepository = ResourceBusStopRepository()
// ...
How can you improve this? A very simple option is to use Koin.
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.
As mentioned, an in-depth exploration of Koin is outside the scope of this book. In this chapter, you’ll just use it to fix the repository dependency. To do this, you need to:
- Install the dependencies for Koin.
- Look at the repository implementations you want to inject.
- Create a module with the objects you want to inject.
- Initialize Koin for Ktor.
- Inject the repository implementation into
FindBusStopandFindBusArrival.
This is a simple example, but it gives you an idea about how to use Koin and how it differs from Dagger and Hilt.
Installing Koin dependencies
Open build.gradle in Figure 20.5:
Inside, add the following definition:
// ...
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
// ...
}
// ...
Here, you just install the:
- Core Koin library.
- Koin library with some utilities for Ktor.
- Koin testing library.
koin_version is already available in gradle.properties, along with other version variables. It’s important to note that Koin doesn’t need an annotation processor because it doesn’t generate any code.
After you sync the BussoServer project with the Gradle file you just updated, you’re ready to use Koin.
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:
In this dependency diagram, note that:
- There are two different repository abstractions:
BusStopRepositoryandBusArrivalRepository. -
ResourcesBusStopRepositoryis the implementation ofBusStopRepository.RandomBusArrivalRepositoryis the implementation ofBusArrivalRepository. -
FindBusStopdepends onBusStopRepository. -
FindBusArrivaldepends onBusStopRepositoryandBusArrivalRepository.
It’s important to note that, at this point, the relationships at points 3 and 4 are compositions. Your goal is to use dependency injection to make those dependencies loosely coupled.
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.
To see this for yourself, create a new package named di and create a new file named RepositoryModule.kt in it with the following code:
val repositoryModule = module { // 1
single<BusStopRepository> { ResourceBusStopRepository() } // 2
single<BusArrivalRepository> { RandomBusArrivalRepository() } // 3
}
This code is very simple. It shows how to:
- Define a module for the repositories in BussoServer by passing a block to the
modulewhere you define the bindings. - Create a binding between
BusStopRepositoryandResourceBusStopRepository. Usingsingle, you tell Koin that you only have a single instance ofResourceBusStopRepositorybound to theBusStopRepositorytype. - Do the same for the
BusArrivalRepository. Every time you inject an object of typeBusArrivalRepository, you’ll use the instance ofRandomBusArrivalRepositoryyou created here.
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
}
// ...
}
// ...
The code is straightforward. Here, you:
- Use
installto register the Koin feature with Ktor, just as you did for theLocationsandContentNegotiationfeatures. It’s important to use org.koin.ktor.ext as the Koin class’ package. - Declare which modules to use. In your case, you use
modules(), passing the reference to therepositoryModuleyou defined above.
Now that the implementations of the repositories are in BussoServer’s dependency graph, you just need to inject them when needed.
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 ->
// ...
}
}
In this code, you need to:
- Delete the initialization of the
busStopRepositoryproperty with the instance ofResourceBusStopRepositorythat you now need to inject. - Use
inject()to initialize thebusStopRepositorylocal variable of typeFindBusStopRequest.
In this way, you inject the instance of ResourceBusStopRepository, which you defined in the module in repositoryModule, into FindBusStop.
Now, open FindBusArrivals.kt in the same apis package and add the following code:
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 ->
// ..
}
}
In this code, you do the same for RandomBusArrivalRepository. Here, you:
- Delete the initialization of
busArrivalRepositoryandbusStopRepositorywith instances ofRandomBusArrivalRepositoryandResourceBusStopRepository, respectively. - Use
inject()to inject the instances fromrepositoryModule, just as you did forFindBusStopbefore.
Finally, run BussoServer to see that everything works as expected. To verify this, open the browser and access a URL like http://localhost:8080/api/v1/findBusStop/1.0/2.0.
You’ll get what’s shown in figure 20.7:
In this case, injecting the repositories is simple because ResourceBusStopRepository and RandomBusArrivalRepository don’t have other dependencies. Next, you’ll see a slightly more complicated example.
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.
Suppose you now want to add a simple logger to check that the instances you’re using in the app are singletons or, using Koin language, are single. To do this, you just need to:
- Add a new
Loggerabstraction with a simple implementation. - Create a
modulefor theLogger. - Install the
modulefor theLoggerin Ktor. - Add the dependency to the
LoggerinResourceBusStopRepositoryandRandomBusArrivalRepository. - Provide dependencies in modules.
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)
}
Now, you just need a very simple implementation. So in the same package, create a new file named StdLoggerImpl.kt and add the following code:
class StdLoggerImpl : Logger {
override fun log(msg: String) {
println(msg)
}
}
This implementation uses the built-in function print() to write the log message to the standard output.
As you learned, you now need a module.
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
}
In this case, you:
- Define
loggerModuleas a Koinmodulein the same way you did for the repositories. - Used
factoryinstead ofsingleto define the binding of theLoggertype to an instance ofStdLoggerImpl.
You could have used single again but you’re using factory to show something different. In this case, you’ll get a different instance of Logger every time you inject one.
Now, you need to install this module in Ktor.
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
}
// ...
}
Here, you simply install loggerModule using modules(), as you did for repositoryModule earlier.
Great! Now, BussoServer knows that there’s a Logger somewhere — but it doesn’t know how to use it or which classes need it.
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.
Open ResourceBusStopRepository.kt in repository.impl and apply the following change:
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]
}
// ...
In this code, you:
- Create the dependency on
Loggerby adding a parameter in the primary constructor. - Use
loggerwhen you initializeResourceBusStopRepository. - Log a message every time you invoke
findBusStopByLocation.
In all the logs, you also print the specific instance of ResourceBusStopRepository you’re using. You’ll use this to prove that ResourceBusStopRepository is actually a singleton.
Now, you need to do the same for the other repository. Open RandomBusArrivalRepository.kt and apply similar changes, like this:
/**
* 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()
)
)
}
}
}
}
In this code, you also see something you didn’t know before. RandomBusArrivalRepository actually needs a BusStopRepository — so it depends on it. That’s because you:
- Delete the initialization of
busStopRepositorywith a new instance ofResourceBusStopRepository, which must be injected. - Define the dependency on
BusStopRepositoryby adding a parameter of the same type to its primary constructor. - Do the same for
Logger. - Use the logger to print a message every time you use
RandomBusArrivalRepository.
Build now and you’ll get an error. You just added dependencies to ResourceBusStopRepository and RandomBusArrivalRepository, but Koin doesn’t know who’s providing those dependencies. You need to fix RepositoryModule.
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
}
All you need to do is add get() every time you need to resolve a dependency. In this code, you use:
-
get()to resolve the dependency fromResourceBusStopRepositoryandLoggerit requires as primary constructor parameter. -
get()twice to resolve the dependency betweenRandomBusArrivalRepositoryand the two primary constructor parameters. Koin is smart enough to understand the parameters’ types and to check if there’s a module that provides a binding for them.
Now, build and run, checking that everything works as expected, as you did in Figure 20.7.
More interesting is to prove that the instance of the repositories is always the same at each request. Check in Logcat and you’ll see something like this:
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
As you see, ResourceBusStopRepository is a singleton.
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.