Dagger vs. Hilt vs. Koin vs. Metro vs. <what co...
# multiplatform
m
Dagger vs. Hilt vs. Koin vs. Metro vs. <what comes next>. Serious question: Are there any compelling reasons to switch from Koin to Metro in a Compose Kotlin multiplatform project?
🍿 1
g
Haven't tried Metro yet, but as Koin is switching from KSP to become a compiler plugin, like Metro, I see less reasons to switch than before, and the added value I see in Koin is actually their integration with their platform Kotzilla
5
z
Not really gonna wade into which is better since I’m obviously biased 🙃 but do want to point out that Koin’s KSP/compiler today is not doing the same thing as Metro’s. They have mentioned future plans for full compile-time graph validation, but currently it does not do that and is instead more focused on DSL usage validation and transforming some specific call-sites to more optimal code
j
Metro encourages actual dependency injection through inversion of control which means things like reuse and testing become dramatically simpler. Most of the Koin projects I see are using service location, not dependency injection, and thus their classes are intrinsically tied to having a fully wired locator.
4
JetBrains migrated the KotlinConf app and the PR was called "Switch DI from Koin to Metro" but if you actually looked at the diff they were not actually doing dependency injection with Koin and thus not reaping its benefits.
t
Here's the PR for the curious ones like me: https://github.com/JetBrains/kotlinconf-app/pull/581/files
👀 3
j
If you want to do service location you're probably better off just making a few `object`s with your deps and passing those around. No library needed. If you want to dependency injection then you should be using Metro.
v
Said PR looks like a misuse of Koin to me. What changes I see for the Metro implementation looks very alike to a Koin KSP implementation
e
There’s also kotlin-inject https://github.com/evant/kotlin-inject always skipped koin because it didn’t have compile time safety
j
It's a great project. kotlin-inject walked so Metro could run, though.
❤️ 2
s
Here's the PR for the curious ones like me:
Interesting code stats,
+565-255
, so about twice the amount of code, it seems.
o
What would be a good self-contained example to show to folks who would like to see the differences between service-locator use of a DI framework and „real“ IoC DI in action?
1
👀 1
p
I believe the
by inject
pattern is service locator but something like constructor injection is more traditional DI. I'm not an expert though so 🧂
Fwiw in our application we use both because constructor injection is hard in some spots so
by inject
is a nice escape hatch. That being said we prefer constructor injection for most code. I've been following metro with interest though, maybe someday we'll migrate
o
Like Jake said, for service locators you could just use some objects and omit a DI library. IoC DI is mostly found in complex applications, which take time to digest, so I'd love to see a smaller, self-contained but realistic example (with tests) implemented in both variants, so whenever the question comes up, I could point folks there, so they could experience the difference.
👍 1
👍🏻 1
j
Well, for what it's worth, IoC DI is also something you can do very easily without a library or tool. It's "just" passing things to other things. It's glue code. You write stateless value objects and stateful services and then you write a bunch of glue to wire them together. At some point, that glue gets tedious and that's where the tools take over.
m
I'd be interested to learn more about the differences between these two concepts. Especially why one is supposed to be better than the other and why you need a tool in the end. DI tooling has a long history and every few months someone comes to the conclusion that anybody else before has done it wrong. From a users point of view this does not really help to bild up some trust in this kind of tooling.
👍🏻 1
z
Said PR looks like a misuse of Koin to me. What changes I see for the Metro implementation looks very alike to a Koin KSP implementation
I'd be genuinely curious to hear which part you consider a misuse of Koin there. We had a couple hacky points of dynamically registering / fetching things from Koin, but most of it was just the usual Koin setup and then ViewModel injections.
> Here's the PR for the curious ones like me:
Interesting code stats,
+565-255
, so about twice the amount of code, it seems.
If you look at it in detail, I think a lot of the lines added are just imports and new injection-related annotations on ViewModels. The majority of actual new code in there was about setting up the graph class for each platform, which essentially replaced the previous per-platform modules with Koin. I'll highlight this though: Metro made me think about how to properly wire dependencies because it didn't allow my lazy runtime hacks anymore. Of course, further feedback is always welcome on our DI setup, if something's suboptimal in the Metro version now, I'd love to improve it.
k
@Michael Paus I have a couple of KMP/CMP projects using koin, and 99% of injections are done through constructor parameters, so I’ve got all the advantages of IoC DI when testing. If you don’t use
by inject()
or similar (usually forced at platform entry/boundary points) then most of your code will be IoC DI. The wiring part in koin seems to be a service locator (all those
get()
calls in modules), but that doesn’t seem to bring issues. Other platform boundaries force “service locator”-ish calls even for dagger/hilt; you call
hiltViewModel()
in composables just like
koinViewModel()
- state hoisting and making those default arguments in composable functions help in both cases. So at the end of the day personal preferences will matter on this topic.
👍 2
z
I'll bite. I think it's largely better to think of the frameworks you listed as two categories that solve different problems in different ways. Category 1: Dependency Injection This includes Metro, Dagger, Hilt, Anvil, and kotlin-inject. They are all descendants of giants before them like Dagger 1, Guice (ish, API inspiration but guice was runtime), and the original JSR 330 spec. Anvil and Hilt are really just sort of extensions to Dagger and kotlin-inject, like dagger-android before them. Manual DI (i.e. just constructors and whatnot) also falls in this category, the tools above are just generating all that wiring you'd otherwise handwrite. These libraries are built with a laser focus on being true and pure dependency injectors. This means true inversion of control with a focus on ease of testing, simple constructor injection, and a generally low-touch runtime API that allows you to use the types they manage without the framework. This is what makes them great for testing and isolation. Metro, Dagger, and kotlin-inject also do true compile-time dependency graph validation. If it compiles, it works and will never fail at runtime. That is a safety guarantee that is extremely valuable, especially in large teams and modularized codebases. If you add mobile to that consideration, it becomes even more important because mobile developers cannot immediately deploy fixes to production at the speed that backend or web developers can. Lastly, the static validation these perform unlock two extra benefits 1. They can generate extremely performant code because they know the exact shape of the graph at build-time. Metro and Dagger will generate significantly different code for the same binding depending on how the bindings in the graph are actually used in your consuming code. 2. They are inherently analyzable because of this in-memory, compile time model. Dagger allows introspection of this via SPI, Metro does via reports and tracing. They're not without their drawbacks though. • In exchange for this added safety and performance, they ask you to be more intentional and explicit with your code. Some people find this irritating or tedious or hard to understand. ◦ Annotations are the easiest way for this in Kotlin and Java, though Metro as a compiler plugin is branching out of these since it can transform IR directly and already has some features that are not annotation based like dynamic graphs. • That build-time validation is also not free, though it's rarely the framework running slow and rather kapt/ksp's overhead that's incurring the real amortizing cost. Importantly, the currency of these costs are measured in developer productivity, and those can always be improved. Build toolchains get faster, Metro's biggest appeal isn't that its API is so much better (it isn't) or that it supports KMP (so does kotlin-inject), it's that it's so damn fast as a compiler plugin. Customers/users/etc are never paying this cost, and if you're a company that's exceedingly important. It's why tools like Dagger, regardless of people's papercuts with it over the past decade+, it remains the industry standard that people trust and use. Category one automates construction of the graph. It's an O(1) act at runtime because the code gen pushes the dependencies down and fails only at compile-time. Category 2: Service Locators This includes Koin, Kodein, Guice-ish, Spring DI, Compose composition locals,
Application.getInstance()
, etc. Some of these support JSR-330-esque behaviors for convenience. They are sometimes nameless conventions in a codebase or framework. IntelliJ platform has
getInstance()
all over the place, when I worked at Flipboard we had a magic
FlipboardManager.instance
, etc etc. They arise naturally if you're not using an intentional DI system because we as programmers generally try to organize our code 🙂. Their focus and value prop is on ease of use to developers.
Application.getInstance()
,
by inject()
, etc will always be easier reaches. The fact that they are runtime-only by default means there is no build costs and your builds are always faster! Your code may fail at runtime if you're missing a dependency but at least it won't quietly do the wrong thing or fail with some obscure NPE. You don't really have to think about how that dependency got here, there's an implicit trust system. That trust system can work really well in a tight code base with serious alignment across the contributors to it. That is alignment that naturally degrades with codebase and team size, no matter how well intentioned. Jake mentioned this in a panel we did with JB a few months back - those large team graph explosions creep up on you when you're no longer seeing every PR that comes through. But, if you're a backend team that can very quickly deploy a fix to prod? That runtime cost risk significantly lower, your clients can see your errors and gracefully degrade. If you're running on a beefy AWS instance, you aren't nearly as concerned about runtime reflection performance as an obfuscated mobile app running on a battery. And hey, sometimes we just generally agree that the developer-cost to doing it the IoC way is just too high to write code the way we want, and using a little bit of de-risked service locating unlocks powerful declarative code patterns (i.e. Compose UI and composition locals). There are other factors worth considering, but in my experience they're somewhat secondary to the above concerns. • Testing in isolation is often much harder and requires using the framework's test harness. Or, as is the case in frankly most SL shops I've heard of, you just write significantly less or no tests • Reflection allows you to do powerful, dynamic replacement strategies or framework integrations. At the same time, it is slower at runtime and nigh-impossible to use safely with a code optimizer or obfuscator. Category two automates retrieval of the graph. It's a runtime O(n) lookup of dependencies, fails at runtime, and you have to request the dependencies
Where the endless debates about DI vs SL break down is a mix of legit and contrived issues. The Legit • Sometimes a SL framework's risks are totally acceptable for a given team. Sometimes it's not. Different teams estimate these risks differently too, based on personal experience/preference/bias/etc • The build costs of compile-time DI are real, but also changing rapidly • Multiplatform support is a real value prop • Migration costs are real • There is a difference between the underlying patterns • The developer productivity bill comes due in different ways. Whether it's test harness boilerplate, when/where you fix DI issues (build time or prod), runtime performance, etc. ◦ IMO, the biggest bill in category 1 comes in the form of an extra few seconds of build time, and the biggest bill in category 2 might come in a 2am pagerduty alert. The Contrived_ is better because _ is worse. This comes up a lot with Koin vs Dagger tbh, and isn't really a high-signal signal for anyone evaluating. It's weirdly political/tribal in a space that should value measurable impact (build times, production hotfixes, etc) over vibes. • __ is DI because it deals with dependencies! This comes up a lot in SL vs DI debates. I mean, a SL is strictly not DI. But this also isn't the hill to die on. If you're finding yourself trapped on the nomenclature taking issue with someone pointing out that SL is not DI, you're missing the point that the person pointing it out is trying to emphasize on why that distinction matters (i.e. everything I wrote above!) There are other little things I haven't touched on because this is already long, like IDE experience/kotlin compatibility across versions, etc. But I think the above highlights are the most important ones. A heavy nit about what "compile-time validation" means One thing I do wanna nit about is the recurring, conflated usages of "compile-time validation". I mentioned this in my higher up message, but I think it's an important distinction. When someone says "Koin is switching from KSP to become a compiler plugin, like Metro, I see less reasons to switch than before", it's a fundamental misunderstanding of what is happening at compile time and implies that they're different tools accomplishing the same thing. Koin's KSP/compiler plugin do a few things, namely • Validation of certain APIs' correctness • Cross-module aggregation (similar to hilt/anvil/metro aggregation for large projects) • Optimize some IR expressions That first one gets tossed around a lot but it's worth being specific that it's more like a linter. Metro, Dagger, Anvil, etc also do all this in the form of just usage checks, but they are fundamentally not the same thing as compile-time dependency injection. Koin's docs have explicitly said that it's planned in the future, but not what it currently does and it's important enough to be worth not conflating. Community Most of us in this space know each other and talk often. I've known the Anvil, Dagger, kotlin-inject, and Guice people in the industry for years. I've contributed to them and they've advised or contributed or both to Metro. I met Arnaud in person finally at Droidcon London last year and he's a nice guy, I think it's super cool that Koin is exploring the compiler plugin space because I think it's a powerful tool for developers to leverage. We've been on a couple calls and docs about ideas of how we (DI people in the ecosystem) can make it all work better for the community too. These frameworks are also still regularly borrowing things that work well from others. Koin clearly felt the value of some degree of compile-time validation and added its KSP (and now compiler plugin) to do some of this. I haven't seen much angst from the Koin community about adding back a build system here, because it was obviously valuable. Similarly, Dagger/Anvil/kotlin-inject clearly saw value in better accommodating kotlin-first approaches, building infra on top of KSP and native support of kotlin language features like Koin and Kodein did. That's healthy.
My 2c for IoC True DI is just an IoC pattern people can adopt. Like any other pattern, there's an initial learning curve and then it becomes automatic. Compose, coroutines, FP, FRP, Spring, etc are all no different. Some have friction points with scale or build systems or both, and we generally treat those as solvable, engineering problems. This is how software ecosystems go. • Guice wanted to be an implementation of JSR330 • Dagger 1 wanted to be Guice but without the runtime risk or reflection performance cost • Dagger 2 wanted to be Dagger 1 but with zero reflection and tighter semantics around object graphs (i.e. components) • dagger-android was an extension to Dagger 2 to make it easier to use in Android framework types at the cost of being a little magic • dagger-hilt, motif, and anvil were parallel extensions to Dagger 2 to make it easier to work in large, multi-module codebases. ◦ Anvil also was the first to really scratch the build time itch here with its factory-gen-only mode as an alternative to Dagger • kotlin-inject was a greenfield, multiplatform-friendly kapt/ksp implementation of DI that intentionally tried new kotlin-first APIs. kotlin-inject-anvil ported anvil's aggregation features to it • Metro was a greenfield, multiplatform-friendly, compiler plugin implementation of DI that took heavy inspo from kotlin-inject's API and dagger's code gen, and opted to make Anvil's features a first party API • Metro is almost certainly not the last new DI framework for kotlin 🙂 Every iteration here has moved the needle a bit in different ways, but arguably the underlying IoC pattern here is ~80% unchanged over nearly two decades. Anvil's aggregation, multiplatform, kotlin-inject's kotlin-first semantics, etc were all natural evolutions and we were more than overdue for something that brought them all under one roof. I think Metro's success has been less about any of its own technical value and more that the community that valued all these things were clearly itching for something like it to come into existence. If the values I described above are what's important to your team, then it's the best type of tool for your team. If the tradeoff is too high, or the risk cost of runtime validation not significant enough, then service locators are probably fine for you. The best DI framework is the one you have. You don't migrate because another one is better, you migrate because the one you're using is not satisfying your requirements. When I write a new simple project, I almost never use a DI framework out the gate. But I do write manual DI still. Then after a certain level of complexity I find myself annoyed with the wiring and adopt a DI framework. I did this with my Field Spottr app last year. The underlying pattern is the same.
mind blown 10
(will prolly dump this all out into a blog at some point, ended up longer than I thought but hopefully not too much of a wall of text)
❤️ 15
thank you color 3
j
That's more than 2 cents. That's like 8.99
😹 8
z
1950s 2c
📈 8
c
so uh. whos going to turn this into a blog post?
b
Please post that on sub-stack lol don't let it die in a slack thread.
💯 2
z
One aspect that I'd love to here more (yes, even more 😄) on is the contents of the A heavy nit about what "compile-time validation" means section.
When someone says "Koin is switching from KSP to become a compiler plugin, like Metro, I see less reasons to switch than before", it's a fundamental misunderstanding of what is happening at compile time and implies that they're different tools accomplishing the same thing.
It is indeed easy to see these as being syntactically and functionally "very similar" as someone not deeply involved in the area.
[Validation of certain APIs' correctness] gets tossed around a lot but it's worth being specific that [in Koin] it's more like a linter. Metro, Dagger, Anvil, etc also do all this in the form of just usage checks, but they are fundamentally not the same thing as compile-time dependency injection. Koin's docs have explicitly said that it's planned in the future, but not what it currently does and it's important enough to be worth not conflating.
I get pretty lost here in the second sentence. I have some idea of what usage checks mean in Metro and others. But I don't know what's not the same thing as compile-time DI. And then I'm also not sure what Koin's docs say is planned for the future - is it the same kind of usage checks?
d
Wow, what a great morning read 👏
z
I've also prepped a simple comparison of a Metro vs Koin annotations (with compiler plugin) setup here, intentionally keeping the diff as close as possible, just to demonstrate what I think people mean when they say that these are "very similar" https://github.com/Kotlin/KMP-App-Template/pull/59/changes Both of these setups in this sample have: • Something static-ish that they initialize at app startup for dependencies ◦
createGraph
/`startKoin` • A class that provides non-user types as dependencies, for example providing
Json
and
HttpClient
AppGraph
with
@Provides
methods /
AppModule
with
@Singleton
methods • Classes registered as dependencies at their declaration like
MuseumRepository
@Inject
with
@SingleIn
/
@Singleton
• Classes bound to interfaces at their declaration like
KtorMuseumApi
and
InMemoryMuseumStorage
@SingleIn
with
@ContributesBinding
/
@Singleton
• ViewModels annotated to receive dependencies ◦
@ContributesIntoMap
and
@ViewModelKey
/
@KoinViewModel
• ViewModels looked up in Compose code with a convenience function ◦
metroViewModel()
/
koinViewModel()
What I do clearly see as a difference though is that if I remove the provided
HttpClient
or the annotations from
MuseumRepository
, Metro fails the build as expected while Koin crashes at runtime 😱. Which actually confuses me greatly, because if I expected something from moving to annotations with Koin, it was certainly the compile-time safety aspect.
💯 1
z
in short - by usage checks I mean like (for example) reporting an error if you annotate a
@Provides
function with two different scope annotations. Basically basic static analysis that checks that you didn't write anything obviously broken. In a compiler plugin for example, this is all the stuff that goes in FIR checkers. By compile-time graph validation, I mean that it actually resolved all graph dependencies from roots to providers/classes, did full cycle checking and topological sorting, broke valid cycles, and validated that what you declared in source can be satisfied. Basically exactly the case you mention with HttpClient. Metro builds that graph at compile-time done the graph analysis and ensured that HttpClient is reachable from whatever downstream binding requested it. koin's current compile-time validation is just making sure you wrote a valid DSL, but it doesn't actually (fully?) validate your dependency graph as far as I know. I don't have the blog post/doc handy but I remember seeing that full compile-time graph validation was planned for the future, but right now remains runtime.
👍 1
d
What I do clearly see as a difference though is that if I remove the provided
HttpClient
or the annotations from
MuseumRepository
, Metro fails the build as expected while Koin crashes at runtime 😱
This runtime uncertainty is why we are looking into switching over to Metro. It has happened once that we almost shipped an app with a broken dependency, and also another time that it got merged to main w/o someone noticing.
2
k
I completely see the value of compile-time validation for massive, multi-module graphs. At the same time I wonder if the current complexity of DI features/responsibilities is the result of the natural evolution of the frameworks used in stateful, lifecycle-driven environments like mobile apps 🤔 As it was written above - the idea is pretty simple - passing things to other things, the DI layer could be a simple boilerplate-removal tool. But now we can have scopes, parent-child relationships, can tell the DI framework to keep a piece of data “while the user is logged in” or “while this screen in on navigation backstack”, and so on. I would appreciate if any of you guys have a reference to some interesting “historical” articles and how we got here.
m
I am not sure whether I fully understood the concept of compile-time validation. Doesn't this imply that you have to re-compile your code for every configuration change? If I follow my gut feeling, I would prefer a solution that collects and evaluates the configuration data when the program starts and then immediately shows me a possible error, rather than later when I happen to reach some obscure corner of my software. With such a fail-early solution I'd tend to prefer run-time checks over compile-time validation because of its seemingly greater flexibility.
d
One more thing to add is that people nowadays expect to have one liners like
metroViewModel
- this is (or was, I haven't checked recently) one of the main problems why kotlin-inject wasn't adopted by smaller teams - they don't want to have to maintain ViewModelFactories manually and come up with their utility function to access a view model from Compose (for example). And probably for a good reason, library authors most likely will write better code and maintain it better. It would be superb if Metro gets to be one of the first KMP DI libraries that sheds some more light on Kotlin viewmodel injection into SwiftUI. Many teams will prefer using KMP without CMP and being able to easily access a viewmodel in SwiftUI similar to how you would do it in Compose is huge imo. Even if it's just a section on the Metro docs with recipe files it would fill a huge gap imo.
o
Doesn't this imply that you have to re-compile your code for every configuration change?
The configuration is Kotlin code. How would you change it without recompiling?
m
@Oliver.O Of course. My understanding is that in order for compile-time validation to work you have to compile all code where DI is used whereas in the other case you just have to compile the configuration itself. Maybe this understanding is wrong. I haven't used Metro yet, so I know nothing about it.
o
Zac can shed more light on what needs to be recompiled in which scenario, but I do know that Metro cares about incremental compilation, as this is where now and then Metro and TestBalloon meet on compiler issues. 🙂
🤗 1
j
Your graph can still be conditional on runtime values and vary those at runtime. This is like owning more than one car and choosing which one to drive to the store with. Metro validates that all the necessary roads exist during compilation, whereas other solutions do not, thus allowing you to turn left into a lake.
🚣‍♂️ 1
🚣🏾‍♂️ 1
o
As there are a lot of smart people here discussing very interesting things for me, could I hijack this thread a bit and ask your opinion on where Java’s ServiceLoader sits, in relation to DI and Service Locators, and on their applicability in library and app development? kodee floating (And, as it's a #C3PQML5NU channel, let's imagine that we have a Multiplatform equivalent of JVM's ServiceLoader) P.S. I could delete a message if you feel it's significantly off-topic.
o
Good point. A use case I remember for a KMP ServiceLoader-like approach is library-driven injection where client code doesn't (need to) know that it happens. So I'd say it's a kind of "push-model undercover DI" with a useful, but narrow range of applicability compared to the usual pull-model DI and Service Locators discussed above.
thank you color 1
j
I can't think of a single usage of service loader that I've been happy about. Behavior that's based on classpath content and order is just a nightmare for deterministic behavior and testing. In Kotlin I have argued against their usage in coroutines for dispatcher loading and datetime for tzdb loading. The JDKs use for java.time is also annoying because implementations (i.e., Android) can choose to leave out support. They save maybe 5 lines of explicit code in exchange for years of headaches and testing frustration. Never use service loaders.
o
Note that the case mentioned above was not about saving lines of code. It would prevent forgetting to set things up, then seeing it perform as expected initially, but running into concurrency nightmares later on. I agree that such approaches are not advisable if they introduce ordering issues.
j
Oops yeah sorry I was responding to Oleg. I didn't actually see your reply! I think you got your push and pull flipped around, though! Service loader is pull, DI is push.
o
Maybe because I was looking from the library side? One library pushes stuff via ServiceLoader into another library's configuration. With DI it is the app pulling in components into its own dependency graph. Does that make sense?
j
True. I guess it's all a matter of perspective whether you're the supplier or consumer of the values.
o
Yep. But I guess we're already making progress on nomenclature in this thread. 😃
o
Behavior that's based on classpath content and order is just a nightmare for deterministic behavior and testing.
Agree, but what if we can build on the generic idea of implicit service loading, rather than just the Java implementation? For example, if we have only static build-time service discovery, then when you compile your application, the compiler already knows all the providers for the main dispatcher or TZDB that will be used. So that the compiler can suggest removing some dependency (or exclude via some compiler option), or even force you to use those "5 lines of explicit code" to resolve ordering or to disambiguate. Yes, it will not be a full ServiceLoader like in Java, but it will cover many use cases, right? Will it somehow change your perspective about "Never use service loaders"?
j
In some respects that's akin to the module discovery and aggregation that Anvil and Metro (and maybe Hilt?) do in order to create the graph. I personally think that's something which should only be scoped to a multi-module project and not done across external dependencies, though. So I'm okay with modules defined in multiple modules which are all pulled into the final 'app' module being aggregated to build and validate the graph, for external dependencies like coroutines or datetime I don't want that mechanism used. I want to explicitly reference
AndroidMainDispatcher
or
SystemTzdb
in a single location and then have that instance propagated explicitly (or via a dependency injector) to everywhere that needs it.
thank you color 1
c
"Your graph can still be conditional on runtime values and vary those at runtime." I'm having a hard time wrapping my head around the use-case for that. Is there some trivial example you could give where having a conditional graph would be useful? I'm assuming this wouldn't be helpful in a prod release? but an internal/dog food release maybe for some internal tools?
j
How about whether or not the app is running under an instrumentation test, which then could be used to vary things like in-memory db vs. persisted, real crash reporting vs. fake, real third-party SDK initialization vs. fake, etc.
But even just things like the API endpoint you're connected to or the logged in user ID are values which themselves can be injected into the graph
If you're doing a white-label app you might conditionally enable and disable whole features through the graph. Or with feature flags you might change the binding for some service from an old version to a new version.
k
This is what I meant by putting too much on DI framework. I really liked the definition you gave earlier - IoC DI is “just” passing things to other things.
c
cool. i was going to say endpoints maybe (we ship okhttp mock-server in internal builds in a debug drawer) and allow the user to swap between things, but we don't actually do it conditionally via di. i might have to look into that.
j
When you have a team of 1-5 you can rely on it intermittently. When you have a team of 50-100 it's nice to have everything in the graph and it be the way of passing things. Especially since you can put tooling on the graph validation to enforce policy such as correct layering.
agree 2
d
At work, we have an app where we combined sweet-spi and Kodein. We have an
@Service Plugin
interface and an
@ServiceProvider
-annotated implementation of this in each of our plugin modules. That interface defines two members: a
val dependencies: List<Plugin>
and a
fun DI.Module.contribute()
. The first allows us to do a topological sort of the plugins to contribute to the DI in the right order and the second allows to contribute to the DI. I believe I could completely replace both sweet-spi and Kodein with Metro, using the module aggregation, am I right? I am also interested in how I could have plugins discovered at runtime in platforms other than JVM/Android. Is this even possible? (I know this is a bit far from the original OP's question, but the discussion seems to have deviated to these subjects.) (BTW, is there a Metro channel on slack? I've seen the GitHub discussions are enabled, but maybe this is too formal for my questions...)
o
I am also interested in how I could have plugins discovered at runtime in platforms other than JVM/Android. Is this even possible?
Yes, it's somehow possible. But, it's not really straightforward to do, as it will involve working with C ABI (for Native), JS ABI (for js/wasm-js), and WIT (for wasm, in the future, when Wasm Component Model will be supported by Kotlin). If you are interested, I can share (here, in DMs, or in a blog post) some POCs I've done based on my work on sweet-spi. kodee floating
❤️ 1
d
Yes, it's somehow possible. But, it's not really straightforward to do [...]
Yeah, after thinking a bit more, I realized I would need to open the library, lookup for my entry point symbol, reinterpret it as a function and execute it... I would be glad for any POC you can share on this! 😁
z
A small update on this: I've updated both branches (and therefore the comparison PR) for the latest versions of Metro and Koin. The diff is even smaller now, thanks to the new implicit class keys on the Metro side. At the same time, Koin now provides "proper" compile-time safety (as per their docs). I don't know the extent of this, but it does indeed break the build as expected if I remove one of the dependencies from the graph. So now more than ever I'd love for someone to do a detailed comparison of the features of the two solutions 😄 The actual usage - at least in a small project like in the PR here - looks nearly identical.
🙌 1
Okay, I see that not everything is validated with Koin, this is a stack overflow at runtime for example
Copy code
@Singleton
class A(val b: B)

@Singleton
class B(val a: A)
👍 1
d
To me this compile time safety and relying on proof of the graph being correct is the gist. I'd hate to find out corner cases like this at runtime in prod.
nod 1
d
In the general case, I think you need both compile-time and runtime validation. In fact in almost all of my use-cases for DI, I need to be able to assemble features either at the last build stage by assembling artifacts without any code changes, or even at runtime directly. As far as I can tell, neither Koin nor Metro can do that (yet?).
z
I'm not sure exactly what > at the last build stage by assembling artifacts without any code changes means, but generally disagree you need any runtime validation. Metro's primary value prop is that it's 100% compile-time validation, including full graph validation (not just reachability)
d
Meaning, literally assembling libraries in a product, and being able to derive variants of a product by putting all or part of the artifacts. For instance, in one of our app, we have application variants with only the necessary tools for a specific partners, each partner having a different set of tools. We produce these only with different build scripts selecting a different set of dependencies artifacts we already built.
but generally disagree you need any runtime validation.
Not all applications are fully static in terms of features. Some also require some form of extensibility after being built. And that may happen through dynamic linking of new artifacts providing new features. (I mean imagine we had to statically build a single variant of the linux kernel, so that it contains all that is needed by all its users. Thank god, the kernel has a module system and a dynamic version of it...)
Metro's primary value prop is that it's 100% compile-time validation, including full graph validation (not just reachability)
Don't get me wrong, I love compile-time validation. If Metro had a way to validate both the different connected components (each artifact individually) and then provide a quick check at runtime that the available components fit and nothing is missing, then I would use it. Unfortunately, Metro doesn't cover all the DI use cases, but only the ones where you can account for all the modules of a product beforehand. Again, I would love to have more build-time check, but unfortunately Metro only covers the case of fully statically linked applications.
z
Android apps solved that pattern using statically validated DI frameworks a long time ago tbh, there are many solutions and it's sort of orthogonal to this thread :). Quick toss of “dagger multi variant Android app” into the search engine or chat bot of your choice should yield a lot of prior literature
d
> "dagger multi variant Android app" Well, I only get write-ups about how to use Dagger in a multi-module setup.
Anyway, thanks a lot for your answers and your time! I will keep digging on how to do this.
856 Views