https://kotlinlang.org logo
Join Slack
Powered by
# dependency-injection
  • z

    Zac Sweers

    07/07/2025, 3:26 PM
    set the channel description: A channel for general discussion on dependency injection libraries, design patterns, etc.
    ❤️ 4
  • m

    Marshall Mann-Wood

    07/08/2025, 3:32 AM
    Rick just went on leave for a month, so idk if he's going to want to join until he's back. To kick things off, the general gist of our discussions at droidcon centered on making DI lazy by default. There are multiple ways to do this, and the mechanism I described centered on a "pull" model for dependency resolution instead of a "push" model. I described it as "ServiceLocator with compile - time guarantees". The common counter was that Dagger (and similar) allow for lazy resolution using Provider/Factory, but it must be manually requested at individual call sites. This doesn't jive with my "funnel of success" mindset, so we discussed a potential modification to Metro to allow some type of flag to perform transparent Provider wrapping at compile time. My goal of requesting this chat is to hopefully provide a common ideation ground for ideas around DI, a place to raise awareness of common concerns and approaches, and a potential way for closed source and open source contributors to discuss learnings.
  • z

    Zac Sweers

    07/08/2025, 4:28 AM
    I think that's a little abstract and would benefit from a simple code sample? I understand a request to make all injections lazy (and to Jesse's idea, it may be possible to do this automatically in a compiler plugin), I don't really follow how that relates to push/pull designs though. I'd also argue that a service locator with compile-time guarantees still suffers some of the usual issues with service locators, namely in that you have to fake the service locator itself somewhere to test the thing that's injected.
  • r

    Rick Ratmansky

    07/08/2025, 9:04 PM
    Hey as @Marshall Mann-Wood said, I'm actually on leave from work for a month, but that doesn't mean I can't join an ad hoc outside of work chat such as this 😉. I'm happy to be here and to contribute to any discussions as best as I can! I believe Marshall gave everyone a bit of an introduction as to who I am, but just in case, I'll give a tl;dr. I'm the tech lead/owner for the dependency injection framework at Meta and have been for the better part of the last 6 years (took over in early 2019), I've been at Meta almost 11. I redesigned our DI framework from an API that looked a lot like what dagger/hilt looks like today to something completely different (not sure I can get into specifics). One of the core design principles I settled on for our API, which has worked really well for us, is to make the framework as explicit as possible. Rather than something like "@Inject Foo foo", without any knowledge of things like scopes, the framework looks more like "Foo foo = Scope.get(someScopeKey)". Again, sadly, I'm pretty sure I can't get more specific than that here. I'd say that over the years, I've dealt with all things DI from concurrency issues, correctness, scoping, performance, and whole host of other stuff. I can definitely talk about these things from a principle and high level. Anyway, I'm happy to be here and to join in on the fun!
  • m

    Marshall Mann-Wood

    07/09/2025, 12:48 AM
    When I say "push" design, I mean that something other than the injected object needs to manage when the injection happens, "pushing" the dependencies into it. This is normally done via constructor injection, but it could be done by field injection as well. By "pull" design, I mean that the object itself asks for the dependencies it needs whenever it wants. It could do so eagerly similarly to how other eager DI works, or it could do it lazily, by requesting dependencies when they are needed. An example of a (probably bad) hybrid architecture would be to have some kind of god-factory that can be used to get everything, and you could inject that with constructor injection
    Copy code
    class Foo(@Inject private val factory: GodFactory) {
      private val bar = factory.get<Bar>()
    
      fun doWork() {
        val baz = factory.get<Baz>()
        baz.doThing()
      }
    }
    Another option would be to remove the injection of the
    GodFactory
    and make it available through some other mechanism, such as static availability.
    Copy code
    class Foo {
      private val bar = GodFactory.get<Bar>()
    
      fun doWork() {
        val baz = GodFactory.get<Baz>()
        baz.doThing()
      }
    }
    Making it statically available starts to look very similar to a Service Locator
  • m

    Marshall Mann-Wood

    07/09/2025, 12:53 AM
    > namely in that you have to fake the service locator itself somewhere to test the thing that's injected. Broadly, this is a problem with any dependency resolution framework and testing. • Without DI of any kind you'll have to fall back to static mocking with powermock/mockk • With DI you have to have some kind of mock/fake/configuration that precursors every test AFAIK there's no getting around this. That said, the beauty of a compile-time framework is that you can take advantage of your compile time to make a real injector for you, and then create some kind of test-only override function for it to provide mocks for your test cases Something like
    Copy code
    class FooTest {
     
      @Before fun setUp() {
        GodObject.overrideDependency(Bar::class, mock()) // doesn't exist in release builds
      }
    
      @After fun tearDown() {
        GodObject.removeOverride(Bar::class) // doesn't exist in release builds
      }
    }
    I'm not super familiar with how Dagger/Hilt do this, but I can't imagine it's much different to what I described.
  • z

    Zac Sweers

    07/09/2025, 1:38 AM
    I think it might be worthwhile to read up on how Guice/Dagger/KI/Metro handle this, as they abide heavily by what's called the "hollywood principle" — don't call us we'll call you.
    Copy code
    // Guice/Dagger/KI/Metro
    class HttpClient @Inject constructor(private val cache: Cache)
    
    @Test fun example() {
      val httpClient = HttpClient(Cache.NONE)
    }
    No mocking frameworks necessary, let alone static mocking. No precursors either, you're just working with the standard language construction features. vs
    Copy code
    // service locator
    class HttpClient {
      private val cache: Cache by inject()
    }
    
    @Test fun example() {
      // You have to know about the service locator implementation detail of the class to safely stand it up
      ServiceLocator.set(Cache::class.java, Cache.NONE)
      val httpClient = HttpClient()
    }
    Perhaps what you're thinking of is the notion/existence of a component (Dagger 2, KI) or graph (Dagger 1, Metro) instance that acts as a holder of objects at a certain scope. Injectable types do have to have a path in the binding graph back to a root in this class, but you generally abstract this away pretty quickly in whatever architecture you're using (ViewModelProvider.Factory, activity providers, etc). Hilt brings a lot of these as first party conveniences too.
  • z

    Zac Sweers

    07/09/2025, 1:40 AM
    Member injection takes a bit more ceremony too, but we generally avoid them and in modern android versions they're usually not necessary anymore
  • z

    Zac Sweers

    07/09/2025, 1:41 AM
    There are some benefits in the approach you've described, namely that you can lazily get whatever you need from your DI graph vs at construction. That said,
    Lazy
    handles this fine enough for performance-critical areas. In Metro we could explore something like Jesse described at the compiler level to automatically transform these if we really wanted.
  • z

    Zac Sweers

    07/09/2025, 1:43 AM
    another thing I'm inferring is that the injected types can request types from any scope at their call-site. I'm curious how you enforce hierarchy with this, for example ensuring that something that lives at app-scope doesn't request something at a narrower scope like logged-in-scope (and thus risk leaking it outside the lifecycle of that scope)
  • m

    Marshall Mann-Wood

    07/10/2025, 1:09 AM
    This does appear to be an advantage that constructor injection/push-style injection has over pull-style. However, I would ague that the advantage is not great. Both approaches require some mechanism to declare/create all dependencies for the all injected objects. Constructor injection is done via standard language constructs, as shown, which may be advantageous to more abstract or ceremonial constructs. I also agree that, in the ServiceLocator approach, any implementor/test does need to know about the implementation details of the injected class, in that it uses a ServiceLocator. However, given the incredible level of detail such an implementor/test already has to have (literally all constructor arguments), I don't see adding one to that list as a major issue. The advantage of any injection based system is that this song and dance shouldn't be necessary for a test anyway. If I want t to get an HttpClient to test, I could just ask the DI system for it. And any competent DI system should allow for customization before initialization using whatever mechanism feels best. For Dagger/Metro, that might mean creating a custom graph for each test, and for a ServiceLocator-like (I don't actually think Meta's really is a ServiceLocator, just the API is semi-shaped like one, so it's the language I'll use) it can come from setting the overrides on the
    GodFactory
    .
  • m

    Marshall Mann-Wood

    07/10/2025, 1:14 AM
    FWIW, I think we're also not quite on the same baseline when it comes to overall API design. I was surprised/shocked when Jesse showed me how the component graph is manually built during early app startup in his sample app. This seems like incredible overhead to me, given that the build system itself is a graph that includes all the types, and could be leveraged to automatically generate such a graph. Given that, there is no reason that either approach should require manual creation of any non-mocked object in the test graph. In order for the test to run, it must be built, and for it to be built there must be a build graph. The beauty of not having to explicitly declare all injections (either by using field injection, or a ServiceLocator-like) is that you can leverage the system to handle everything you don't explicitly override.
  • m

    Marshall Mann-Wood

    07/10/2025, 1:16 AM
    I'm curious how you enforce hierarchy with this
    I'm going to leave @Rick Ratmansky to decide if/how we can answer this, as it gets into "I don't know exactly how much I can say." Closed source do be fun sometimes
  • r

    Rick Ratmansky

    07/12/2025, 12:19 PM
    Without knowing what was already said, I'm going to try to leave this a bit vague for what we do, but it should be enough to understand the concept. Given we're talking about android, there are a few things we can assume such as a singleton scope of some kind, you might have activity scoping, and then you can have an unscoped injection (new instance every time). For the sake of argument, let's include a fragment scope to give us 3 tiers for the hierarchy (unscoped doesn't count and can be injected everywhere): Singleton -> Activity -> Fragment
  • r

    Rick Ratmansky

    07/12/2025, 12:21 PM
    This leaves us with the following truth chart, if you will: -> means can inject Unscoped -> Singleton, Unscoped Fragment -> Singleton, Unscoped, Fragment, Activity (assume fragments can only be used in an activity) Activity -> Singleton, Unscoped, Activity Singleton -> Singleton, Unscoped Unscoped can be injected everywhere because a new instance occurs every time.
  • r

    Rick Ratmansky

    07/12/2025, 12:23 PM
    With a pull like setup though, where things are explicit, the way you'd inject something that is Activity scoped is by providing the activity that the scope is keyed off of. In other words ActivityScope.injectSomeObject(someActivity). The thing is, to make this work and to "enforce" the hierarchy, it also means activities can't be injectable, same with fragments. You can inject things into them, but you can't inject them elsewhere.
  • r

    Rick Ratmansky

    07/12/2025, 12:24 PM
    There's a bit more to this, like how you would propagate a given scope's cache key, but that is the overall idea
  • g

    gildor

    07/18/2025, 3:22 AM
    Anvil is deprecated, all hail Metro 🎉 https://github.com/square/anvil/issues/1149
    🚇 3
  • m

    mattinger

    07/25/2025, 7:10 PM
    Has anyone been using anvil-ksp (the forked version from Zac Sweers)? I'm running into some odd errors when i update to using the kotlin 2.2.0 compiler plugin (we're still language level 1.9, we have not changed that yet):
    Copy code
    e: [ksp] Found conflicting entry point declarations. Getter methods on the component with the same name and signature must be for the same binding key since the generated component can only implement the method once. Found:
        anvil.component.com.xfinity.digitalhome.container.digitalhomecomponent.MergedCommerceAppComponent_0044d15d.Factory anvil.component.com.xfinity.digitalhome.container.digitalhomecomponent.MergedCommerceAppComponent_0044d15d.ParentComponent.commerceComponentFactory()
        com.xfinity.dh.commerce.buyflow.di.CommerceAppComponent.Factory com.xfinity.dh.commerce.buyflow.di.CommerceAppComponent.FactoryCreator.commerceComponentFactory()
    e: [ksp] @Component.Factory types must have exactly one abstract method. Already found: com.xfinity.digitalhome.container.DigitalHomeComponent com.xfinity.digitalhome.container.DigitalHomeComponent.Factory.create(com.xfinity.digitalhome.app.DigitalHomeApplication, com.xfinity.digitalhome.utils.manager.LocaleManager, com.xfinity.digitalhome.config.PartnerConfigSettings, com.xfinity.digitalhome.features.launch.logging.LaunchTrackingManager)
    This is using a very standard
    MergeComponent
    along with a
    ContributesSubcomponent
    along with a contributed interface to the app scope to get the factory:
    Copy code
    @ContributesSubcomponent(
        parentScope = AppScope::class,
        scope = CommerceAppScope::class
    )
    interface CommerceAppComponent {
        @ContributesSubcomponent.Factory
        interface Factory {
            fun create(): CommerceAppComponent
        }
    
        @ContributesTo(AppScope::class)
        interface FactoryCreator {
            fun commerceComponentFactory(): Factory
        }
    I'm not sure what's going on, but i also get a lot of starvation when i try to run our CI tasks which is compiling 6 different branding flavors in parallel. This seems to be related to parallism, because when i run with --max-workers=1 it seems to not occur
  • m

    mattinger

    07/25/2025, 8:25 PM
    We're also seeing somewhat related errors with the 2.0.x compiler plugin just from using the anvil fork:
    Copy code
    java.nio.file.FileAlreadyExistsException: /Users/xxxxxxxx/Work/comcast/xfinity-android/app/build/generated/ksp/comcastDebug/java/com/xfinity/digitalhome/features/launch/login/NoAppAuthBrowserFragment_MembersInjector.java
        at java.base/sun.nio.fs.UnixFileSystem.copy(Unknown Source)
        at java.base/sun.nio.fs.UnixFileSystemProvider.copy(Unknown Source)
        at java.base/java.nio.file.Files.copy(Unknown Source)
        at com.google.devtools.ksp.common.IncrementalUtilKt.copyWithTimestamp(IncrementalUtil.kt:80)
        at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension.updateFromShadow(KotlinSymbolProcessingExtension.kt:493)
        at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension.doAnalysis(KotlinSymbolProcessingExtension.kt:391)
  • m

    mattinger

    07/25/2025, 8:33 PM
    Hate to direct tag @Zac Sweers, but i'm not sure if this is anvil-ksp related or general ksp issues
    z
    • 2
    • 4
  • m

    mattinger

    07/30/2025, 4:46 PM
    Does anyone know what the proper configuration of the "anvil" extension is for a module which has both kotlin and java, and still has some raw dagger components? There's a lot of properties on the original anvil extension, plus there's the added "useKsp" function, and it's unclear to me what gets used when.