Let's move on everyone. So we have to keep the schedule and to keep the pace for all
our viewers online and the rest of FOSDEM. So for this next talk we are welcoming Romain
We are going to talk about dependency injections. Please give Romain a warm welcome.
So thank you for having me. I'm really glad to be here with you. I'm Romain Boacelle and
today I will guide you through our journey of improving the use of codeine and dependency
injection library using KSP. So you already seen that commercial art so I'll make it
really quick. I'm really happy to work with Salomon on advocating as well as providing
services and trainings on codeine and I'm really grateful that TreadBrains is rewarding
us years after years. So let's talk about the real subject here, open source. So we are
maintaining a set of codeine multi-platform independent tools that are compatible on
every target on which we can compile codeine and obviously today we're going to talk about
dependency injection with codeine. So let's team up and see what are our problems today
and how we are trying to solve them with KSP. First a little bit of context on why we are
using dependency injection in our applications. So let's assume that I have a view model that
needs multiple instances of use cases. So I will need to initialize every one of those
use cases and their dependencies and so on. So to avoid managing those initializations
we often use the dependency injection pattern. In dependency injection the DI container has
the responsibility to initialize every instances we need to make our objects works and so we
can lazily retrieve them with a simple function call. So here we see that we used a generic
function that is called instance or we often see get inject or whatever. In some other
libraries our framework we often see a huge amount of annotations. So that sounds like
magic to a lot of people. Another problem with the generic instance function is that
I don't really know what is behind it. Is it a single term, a factory, do I need to pass
an argument? I don't really know. Another problem comes with the DI binding declaration.
As a maintainer I know what's behind all that but it can be confusing for newcomers. And
on top of that there is no compilation checks. So this means that if you missed, if you forgot
some bindings you will probably know it when your application crashes or in most cases in
your dashboard. So did we just create a monster? Not quite. But there is still room for improvement.
So let's do what we do best and refactor everything to get a better API and improve the use of
code in. So let's welcome KSP. It stands for Kotlin Symbol Processor. A Kotlin Symbol Processor
is a metaprogramming tool that allows us to generate code generally based on some existing
code base. Symbol Processor are generally used with annotations that are used to mark our
code that needs a special treatment by the processor. In short terms it's a lightweight
compiler plugin that because it can just generate code and not modify some. A quick example
here I have a full interface that is annotated to be processed. So a Kotlin Symbol Processor
should be able to generate a concrete class of this full interface overriding the check
function. So back to our initial problem. What do we need to improve the Kotlin API? First
we need a readable and typed API to be able to avoid the use of this generic function
instance everywhere. As we cannot easily have compile checks, at least we want an easy way
to check the dependency container consistency with a few lines of unit tests. Note that
the API you'll see today are still working progressive. They may change a little before
landing on the release. So the main idea is that you should be able to declare one or
more interfaces that represents the dependencies you need in your applications and that's the
one that you need to retrieve at least. So after that we need to annotate our interface
so that the Kotlin Symbol Processor should be able to generate some code to interact
with the DI container to retrieve your dependencies in a typed way. So let's introduce the result
annotation for that. Once we have annotated our interface, the Kotlin Symbol Processor
should be able to detect and generate code for us. So in that case, you will see that
we need a parameter to create a browser service so it will know that we need to interact with
a factory that needs a string as parameter to get a browser service instance as well
as the controller functions needs to return a simple instance depending on the context.
Here is what the Kotlin Symbol Processor will generate for us. A concrete class implementing
our app dependencies interface and that needs a DI container as input. So the DI container
will be used to retrieve the proper instances here either a factory because it can detect
that we need a parameter or a simple instance for the controller. Now let's see how we
can use it in our application code. So first we need to declare our DI bindings so our
implementation should be able to interact with. For that, we still use our current API
of Kotlin's DSL or it will improve it a little bit but we will keep it as well as it is.
Why? Because we have history with solutions like daggers that have gone wild with annotations.
Even so, it's where Kotlin was created in the first place to avoid that forest of annotations.
So we didn't want to introduce tons of annotations again and go full circle. Finally, we think
our binding declaration API is good enough to express our dependencies. So here are the
dependencies we need to meet the app dependencies interface contract. We need factories that
take a string and return a service and a single tons that return an instance of a controller
that itself needs an instance of a service. Welcome to that later. So now we can instantiate
the generated class with our DI container and retrieve our dependencies with a truly
typed API. As we don't want our user to know how we are generating our function or class
implementation, sorry, we also introduced an extension function that helps us instantiate
the app dependencies for us. So now that we are able to retrieve our dependencies with
a truly typed API, let's see how we can check their consistency. For that, we introduced
a DI resolver interface that only needs to respect the contract of a check function.
So now we can combine the DI resolver interface with an atresolve annotation. In an ideal
world, the atresolve annotation should be able to add that DI resolver type to our interface
itself. But as we are using Kotlin symbol processor, we can just generate some code
and not modify existing one. So now that we are fully packed with our annotation and our
DI resolver interface, the code in the symbol processor will be able to generate the override
and function check and create a requirement for everyone of the function or accessor we
have defined in our app dependency interface. Before, with that code, you will have taken
the risk to go in production without easily knowing if you forgot some bindings. No more.
Now we just add a test and as we saw before, we instantiate our app dependencies interface
with a concrete class and just call the check function. That way, if you missed a binding,
your test suite will warn you instantly with a proper message. So here, we saw that we
missed a factory binding that returns a broad service and takes a string argument as input.
One more thing. Earlier, we saw that the code in binding DSL was impacted with the use of
those instance functions. So let's see how we can improve this user as well. Let's take
this example. When I need a controller that needs itself a use case, that also needs a
service. Here is the binding we will have defined to meet our architecture expectation.
So you probably have seen me coming with those instance functions. In the explicit world,
we will have written those functions with their targeted type so we saw that we need
a service and a use case. So why not using a type API and get this instance directly?
This is not that simple because in the code in DSL, this is a type DI builder. So it doesn't
know anything about the app dependencies API. Thus, the code in the symbol processor will
also generate a new function for us that is called off with the name of the app dependencies
contract we have. Thus, it creates a new DI builder that is aware of the DI building API
and the app dependencies. Allowing us to call straight functions to get our instances as
long as they are part of the app dependencies API, obviously. So as a result of type dependencies,
I can now easily check the consistency of my DI container or retrieve dependencies in
my application. So I feel the tension and excitement in the room, right? No? Same question
on everybody's mind. Is this live? So I can spoil it earlier. It's still a work in progress
and we still have some corner cases to crack, like how to use tags, how to manage modules,
how to declare and handle scopes and context is and a few more. Here is a sneak peek of
some ideas we have to solve those problems. An easy one is the tag API when we can retrieve
a dependencies by passing a tag parameter. We could add an annotation with that tag and
easily retrieve our dependencies without having to pass or know the tag that is needed behind.
For the module management, we could just provide two ways to interact with dependencies either
with a fully packed DI container or a DI module or add some parameters or annotations or either
create a new annotations again. We'll see. Maybe not. A more complicated use case is how
to handle scopes. With code ends, we can define some bindings that are with some life cycle
depending on the scope or context and retrieve them with their own function. So to define
a scope with a KSP, we could define that a contract is scoped entirely and then be able
to retrieve our dependencies with the right context. Maybe this rings a bell. It sounds
like context receivers, right? But unfortunately, context receivers are not available in Kotlin
multi-platform yet, so we'll have to find another way. Also, we have plenty of ideas
to make this work. This is an open source project, so obviously, if you want to contribute,
you're welcome. That's it for me. Thank you for hearing me. If you have some questions,
I think we have time for it. Thank you very much again. We do have quite a lot of time
for questions now, so please raise your hand if you have any. Just shout it and you will
have to repeat it. So the question is, is there some support
in the IDE? So as for the mocking library, you will need to build your code the first
time to be able to have the right APIs, like the new function and the off-app dependencies
function, for example. No. All right. I think it's clear.
One question. With this dependency injection framework, are all dependencies compiled statically,
like the dependency injection part, or is that still dynamic runtime?
So the dependencies injection are resolved statically or at runtime? Is this your question?
So they are resolved at runtime, but as we saw, we provide tools to check that all your
bindings are well-defined with your test suite, but they are resolved at runtime. There is
no reflection like Salomon showed you earlier. That's how Dagger works, for example.
So it's basically the best focus between Spring and Dagger.
There was another. I think it's a bit of an obvious question. The question is how does
it compete with coin? I think we have taken different routes with coin. I think Arnault
will present you this afternoon. It provides an API using annotation, more that we are
doing. We prefer keeping the binding declaration as much explicit as possible. After that,
I think it's more some internal implementation that does not really compete, I think.