FAQ¶
This is a list of frequently asked questions about Metro. Consider also searching the issue tracker and discussions section of the Metro repo for anything not covered here!
Compiler plugins are not a stable API, is Metro safe to use?¶
Yes!
While it’s true that compiler plugins often require strict version alignment, Metro is explicitly architected to avoid this with a robust built-in compatibility system.
Metro maintains forward compatibility on a best-effort basis (typically at least N+.2 minor versions, often more) rather than locking you to a specific patch version. This allows a single version of Metro to support a broad range of Kotlin releases and IDE plugin versions simultaneously.
Example
The current release of Metro (0.10.1 at the time of writing) supports Kotlin 2.2.20 all the way through 2.3.20-Beta1 (and will support 2.3.20 final).
That’s five Kotlin versions (2.2.20, 2.2.21, 2.3.0, 2.3.10, and 2.3.20) plus various intermediate versions bundled in the Kotlin IDE plugin.
You can upgrade your Kotlin version (including across minor updates) or use different IDE versions without needing to wait for a matching Metro release. See the compatibility docs for the full support matrix.
In short — it’s a wide, moving window of support that periodically raises the minimum, which isn’t terribly dissimilar from any other tool.
Metro is not a stable API, is Metro safe to use?¶
Yes, Metro is functionally stable and ready for production use. Its runtime and Gradle plugin APIs are not yet stabilized, which is not the same as being unstable for use
See the stability docs for more details.
Why doesn’t Metro support kotlin-inject-style @IntoMap bindings?¶
Some technical context
kotlin-inject allows you to provide key/value pairs from an @IntoMap function rather than use @MapKey annotations.
This allows some dynamism with keys but has some downsides. A few different reasons Metro doesn’t use this approach
- Duplicate key checking becomes a runtime failure rather than compile-time.
- It breaks the ability to expose
Map<Key, Provider<Value>>unless you start manually managingProvidertypes yourself. - You allocate and throw away a
Pairinstance each time it’s called.
Will Metro add support for dagger-android features or dagger-android interop?¶
No.
Dagger/Hilt FAQ¶
In Dagger I could make declarations internal and it worked, why doesn’t that work in Metro?¶
In short, it “works” in Dagger in the same way that any other access of Kotlin internal types “works” in Java sources. These types are always public (albeit sometimes with mangled names). This led to patterns of internal providers, internal Dagger modules, etc. This almost certainly also led to some degree of incremental compilation issues since internal declarations are usually excluded from a given module’s ABI unless you use @PublishedApi.
This pattern doesn’t fly in Metro because Metro is an all-Kotlin system, meaning it fully understands the Kotlin language’s visibility system and must also abide by it. It’s necessary for both correctness and incremental compilation.
Test source sets are able to do this because of a separate compiler mechanism called “associated compilations” that allow them to declare external compilations as “friend paths”, and that must be configured at the build system level (it’s done automatically by KGP for main/test source sets). Metro does respect associated compilations, and will use internal symbols from other modules IFF they are designated “friend” modules.
Why doesn’t Metro support @Reusable?¶
Some technical context
@Reusable works almost identically in code gen as scoped types, it just uses SingleCheck instead of DoubleCheck. It’s basically like using lazy(NONE) instead of lazy(SYNCHRONIZED).
A few different reasons Metro doesn’t have it
- I think it risks being like
@Stablein compose where people chase it for perceived performance benefits that they have not profiled or would not actualize if they did. Basically it becomes a premature optimization vector- Ron Shapiro (the author of it) even said you shouldn’t use it or scoping in general [for performance reasons] unless you’ve measured it: https://medium.com/@shapiro.rd/reusable-has-many-of-the-same-costs-as-singleton-c20b5d1ef308
- Most people don’t really know when to use it. It doesn’t really strike a balance so much as blurs the line for limited value (see: the first bullet).
- It invites people to make unclear assumptions. It’s pretty simple to assume something stateful is always a new instance or always the same scoped instance. It is harder to envision scenarios where you have stateful types where you don’t care about knowing if it’s shared or not. You could say this should only be for stateless types then, but then you’re deciding…
- Do you want to limit instances? Just scope it
- Do you not care about limiting instances? Don’t scope it
- What’s the expected behavior if you have a
@ReusabletypeThingand then request aLazy<Thing>elsewhere? Currently, MetroDoubleCheck.lazy(...)’s whatever binding provides it at the injection site, which would then defeat this. To undo that, Metro would need to introduce some means of indicating “what kind” ofLazyis needed, which just complicates things for the developer.
Will Metro add support for Hilt features or Hilt interop?¶
Metro is largely inspired by Dagger and Anvil, but not Hilt. Hilt works in different ways and has different goals. Hilt is largely focused around supporting android components and relies heavily on subcomponents to achieve this.
Some features overlap but just work differently in Metro:
- Instead of
@UninstallModulesand@TestInstallIn, Metro graphs can exclude aggregations and contributed bindings can replace other bindings. - Hilt has support for injecting
ViewModels, but this is entirely doable without Hilt as well by just creating a multibinding. See the android-app sample for an example. - Hilt has support for aggregation with
@InstallIn, Metro uses@Contributes*annotations.
Some features are focused around injecting Android framework components. There are two arguably better solutions to this and one not-better solution.
- (Not better) Expose injector functions on a graph to do member injection directly from the graph.
- (Better) Constructor-inject these types using
AppComponentFactory. This does require minSdk 28. When Hilt was first released in 2020, this was a relatively new API. However, 2020 was a long time ago! minSdk 28+ is much more common today, making this much more feasible of a solution. - (Best) Use an app architecture that better abstracts away the surrounding Android framework components, making them solely entry points.
The rest of Hilt’s features focus on gluing these pieces together and also supporting Java (which Metro doesn’t support).
How can I replicate Hilt’s @HiltAndroidTest?¶
Some technical context
Hilt’s @HiltAndroidTest and associated rule allow tests to “replace” bindings in a target graph even if it’s compiled in another project.
Metro supports dynamic replacements via a similar feature called dynamic graphs.
Can Metro do Hilt’s automatic aggregation of transitive dependencies’ aggregated bindings?¶
Some technical context
Hilt can automatically aggregate transitive dependencies’ contributed bindings even if they are not explicitly visible to the consuming project.
In short: no. The fact that Hilt does this is a bad thing in my opinion. It essentially defeats the purpose of incremental compilation because you have to write your own version in a dedicated classpath scanning Gradle task that then generates stub sources for all the stuff you were trying to hide upstream.
In Metro, you must play by kotlinc’s native incremental compilation rules. If you want to aggregate bindings from transitive dependencies, you must expose them in that project (i.e. Gradle api dependencies) or explicitly declare those dependencies in the consuming project.