One thing we saw in developer preview Compose code...
# compose
s
One thing we saw in developer preview Compose code was people would treat screen composables as equivalent to Fragments and do DI there. However, without a compose-friendly API it caused unexpected side-effects on recomposition. It seems a reasonable pattern, and it's definitely one I expect to see lots of apps do. We reached out to the Koin maintainers to share what we learned and they put together a nice solution ๐ŸŽ‰
โค๏ธ 5
๐Ÿš€ 2
๐Ÿ‘๐Ÿผ 12
v
thatโ€™s really nice to hear!
โž• 1
r
We're probably going to get some pattern with hilt? What I wonder the most if we should be getting dependencies via fields instead of parameters. It seems to simplify everything. But we learn that constructor injection is better than field injection, so it kinda feels wrong.
a
The hilt folks are certainly thinking about compose right now ๐Ÿ™‚
๐Ÿ‘๐Ÿผ 5
a big question is around how much compile-time validation should happen or is needed, since that's kind of dagger's style
s
I think the patterns for usage are still being discovered tbh. We're taking a look at Hilt as well! My personal style is to limit interaction with DI to a single composable and provide injection via parameters (parameters to composable are very similar to ctor arguments to a class). This makes it easy for tests to specialize any parameter, and avoids mixing DI with the rest of the code.
โž• 7
a
if you're thinking that compile-time DI by parameters starts sounding a whole lot like KEEP-87, you'd be right; the ideas there are definitely a part of the discussions as well
๐Ÿ™Œ 2
all balanced by a desire to keep things simple and understandable
s
Oh, hm. Deleting my draft doc "Dependency injection is a Monad" ๐Ÿ˜„ </jokes>
z
So what Iโ€™m hearing is once the IR backend is done, the google compiler team is gonna drive KEEP-87 to actually land? ๐ŸงŒ
s
I think the only thing that I can say to that is "Google does not comment on future roadmaps" ๐ŸงŒ
๐ŸงŒ 4
a
at the moment I think all of the keep-87-alikes are too conceptually complicated
โž• 4
and are a severe impediment to usability outside of enthusiasts and professionals
but who knows, a lot can change and there might be good ways to simplify approaches ๐Ÿ™‚ mostly I'd like to observe the kind of code people write for compose for a while before adding more major things like this outside of answering questions of how to interop with people's existing code
v
For anyone curious about what KEEP-87 is - https://github.com/Kotlin/KEEP/pull/87
๐Ÿ‘๐Ÿผ 2
c
I'm not intimately familiar with dagger, but understand the concept of DI. What @Sean McQuillan [G] said makes sooo much sense to me
My personal style is to limit interaction with DI to a single composable and provide injection via parameters (parameters to composable are very similar to ctor arguments to a class). This makes it easy for tests to specialize any parameter, and avoids mixing DI with the rest of the code.
Why would someone not want to do that?
g
Because it scales purely, if you pass all parameters manually from top most compostable. Imagine app with 100 screens written 100% compostable, and now imagine providing all your dependencies manually to all the screens, it even will not work if you want a dynamic navigation tree So in real world it will never be a single compostable, injection will work on level of some kind screens/high-level components Of course its not right to access DI framework in every of your compostables, same as you do not access dagger component in every class, but 1 single entry point for DI per app is another extreme position
โž• 1
j
@gildor I think @Sean McQuillan [G] want to say 1 single entry point per screen.
g
But what is a screen? You may have 10 entry points (screens) with 10 child screens each
So even if you consider screen as an entry which works with DI, you have not a single entry point. Then another thing, screen may be created from multiple not-relate component and reused between different parts of the app Or even different case, you have an app which essentially a one screen with a lot of components on the same screen
My point that there is no simple way to find point where you work with injection, same as Android views you usually do not object views (at least this what we consider a bad practice in our project) but you have some higher level entities (such as fragments) which you do inject. In compose you probably don't need fragment so you will replace it with composable.
m
If you (like me) find keep87 discussion a bit overwhelming and hard to understand, here is a nice blog post that explains it very well: https://quickbirdstudios.com/blog/keep-87-typeclasses-kotlin/
r
Keep 87 is dead. There is internal efforts in the compiler team where prototypes of multiple receivers and a look into enabling adhoc polymorphism is being explored and I'm helping with use cases from arrow meta. Arrow meta brings ad-hoc polymorphism and an implementation of the curry howard correspondence with union , refinement, coherent implicits and type cohercions for Kotlin. It's already being used to refactor arrow for 1.x to be released estable when the IR backend is the default.
We will remove from meta through deprecation cycles once some of the features make it or not to the lang but adhoc polymorphism and compile time DI will be possible in the worst case by just dropping a plugin in the next few months in beta. It is already available in snapshots
Keep 87 was a mistake, I did not anticipate it would draw so much discussion but it served the purpose to show the Kotlin community care about adhoc polymorphism and constrain based programming more than I originally thought.
Im confused about the discussion too but helped me learn there is more angles to it that we anticipated. This is why we ended up designing it all as theorem prover style proofs based exclusively in extension functions. Extension functions + Injection it's all we really needed for FP and arrow.
a
@raulraja are the things the arrow team learned over the course of the proposal distilled down in some docs somewhere? The rest of the compose team and I would be very interested to read such a doc if there is one.
โž• 1
and of course to continue related design discussions with you here in this slack and elsewhere - there's a lot of conceptual overlap in use cases here
r
There is an ongoing effort to document the features before they are released but they all come from the notion that any subtype relationship in Kotlin can be replaced in the IR phase by an extension function. This unlocks arbitrary relations between types in which injection, extensions, unions etc are just a product of the curry howard. We make the compiler aware this functions are global and provide values and bridges between types. For example:
Copy code
interface fun Persist<A> { fun A.save() }
data class User

@Extension fun User.persist(): Persist<User> = Persist { ... }

User().save() //all members of Persist are projected over User and this desugars to User().persist().save() in IR
๐Ÿ‘ 2
Itโ€™s trivial, works with subtype polymorphism and allows expresssion level injection. That is, no need for setter or constructor based injection or codegen since this is resolved in the local module and providers that can be fun, val, object or class beside extension functions with @Given can be exported. If something likes this comes to Kotlin then we have a version of coherent implicits with internal overrides which serves all DI use cases, compatible with existing subtype polymorphism and itโ€™s also a lightweight form of theorem proving that serves for derivation of structures at compile time since proof resolution can be inductive. If you have A -> B and B -> C you can implicitly go from A -> C. For example data class to json encoder where the compiler projects encoders for the primitives and builds open the product type.
Itโ€™s more powerful than Keep-87 and has no resolution cost in the non derivation part since all proofs are indexed and available to resolution cached.
๐Ÿ˜ฎ 2
v
Very cool! Thanks for sharing this Raul!
๐Ÿ‘ 2
โž• 2
r
@Adam Powell Iโ€™ll be happy to show details of anything if you are interested over meet or want to do something similar in compose. IMO this is a lang issue not specific to Compose and relevant to most people using Kotlin. When we have the docs ready for the features Iโ€™ll send them to you in case the ideas can be of any help for Compose. In the meantime this can give you an idea of what is about Implicits: https://github.com/arrow-kt/arrow-meta/blob/master/compiler-plugin/src/test/kotlin/arrow/meta/plugins/typeclasses/GivenTest.kt Type classes and projections: https://github.com/arrow-kt/arrow-meta/blob/master/compiler-plugin/src/test/kotlin/arrow/meta/plugins/typeclasses/TypeClassesTest.kt Internal overrides of injected deps: https://github.com/arrow-kt/arrow-meta/blob/master/compiler-plugin/src/test/kotlin/arrow/meta/plugins/proofs/ResolutionTests.kt
a
agreed that it's a language-level issue, or at minimum an orthogonal framework/pattern, which is why I'm not in favor of Compose doing something special/bespoke for it ๐Ÿ™‚
๐Ÿ‘ 2
I'm more than happy to direct folks to arrow or dagger or koin or whatever folks find useful. (Arrow's optics/lenses in general seems to play really nicely with compose, for example)
c
I think that Raul could be a nice guest on @Leland Richardson [G] youtube channel ๐Ÿ˜€
โž• 4
r
@Adam Powell We are bringing synth optics in meta so nobody has to depend on Arrow you can just do:
Employee.company.address.street.name
with the compiler plugin over any recursive datastructure solving the nested copy and focus problem including sealed hierarchies. @carbaj0 Iโ€™m actually speaking with Leland in the Kotlin London round table coming up soon, not sure if already announced ๐Ÿ™‚
๐Ÿ” 2
Imagine that deeply nested data structure are lens view references and you can describe decoupled view updates in simple whens with that and ad hoc polymorphism with the proof system. This is what we aim to truly decouple concerns from components and UI state updates is packed with those use cases.
a
neat ๐Ÿ™‚
r
Also Dagger and Koin DI (I spoke to @arnaud.giuliani) about it can also use compile time DI verification of their graph, it should not be hard and much faster than current dagger or similar based on codegen
๐Ÿ‘ 1
a
@danysantiago relevant to your interests โ˜๏ธ
(from the dagger/hilt side of things)
d
( OT ) @raulraja is there any example of huge project using those crazy stuff from Arrow? I often find myself very fascinated looking into these, but I doubt I'll ever want to try them myself, at least not unless I can see them used in a real architecture
r
@Davide Giuseppe Farella what is a real architecture?
d
Everything that is not a mere example ๐Ÿ™‚ A real project
r
The current versions of arrow are used in real projects. The architectures, style an optimizations arrow proposes are cornerstone in backends and clients in the industry for over a decade now. Arrow has over 100k monthly downloads just on the snapshot repo despite being 0.x . Having said that the features I discussed have been developed over a year but are not production ready because they depend exclusively as compose does in the IR backend. Libraries canโ€™t publish yet stable until IR is stable. I donโ€™t share your view that people should adopt projects when they are used. You depend on someone to go first. Having said that arrow is heavily used and I taught arrow in companies using arrow both in backends and mobile. If you have something more specific or a use case youโ€™d like to know how arrow covers I can show you any time. ๐Ÿ™‚
๐Ÿ‘ 1
d
Ye, my point was not about โ€œI donโ€™t trust Arrowโ€ ๐Ÿ™‚ I know itโ€™s widely used, I was just curious to take a look at how this kind of โ€œextremeโ€ APIs ( meta, etc ) are adopted in a large project, how do they impact on architecture, testing, flexibility of the code, etc
r
I see, sorry I misunderstood, since that is off topic here if you want we can discuss it in #arrow or #arrow-meta. We can cover for example testing and using type classes for DI and how it affects it or any other use case you are interested.
๐Ÿ™ 1
๐Ÿ‘ 1
s
Ah, catching up, to clarify a bit above, when I said "single" I meant that more in the sense of "single responsibility" not "find one composable that's app-wide." Probably not the right word to communicate that. This is similar to the discussion of "where do I put state in a compose tree" โ€“ the answer is roughly the natural parent of all composables that use the state. Deciding the DI injection point is going to follow a similar guideline (e.g. one could imagine that your Toolbar composable might inject an analytics object, and providing the dependencies to a screen is a common use case as well). Re: Parameters โ€“ no matter where you use injection and which system (Koin, Dagger, Ambients) โ€“ the analog to ctor injection is parameter injection on a composable function. Constructor injection is also available (you can make composable member functions) but it's probably not my first choice. Taking the App example in the Koin documentation:
Copy code
@Composable
fun App() {
    val myService = get<MyService>()
    // or val myService by inject<MyService>()
}
This can be transformed to a parameter by using a default argument:
Copy code
@Composable
fun App(myService: MyService = get()) {
}
By lifting it from a local variable (equivalent to property injection) to a parameter (equivalent to constructor injection) it's easier to work with in other situations like tests. It also has the advantage of making the calling contract explicit (internal calls to
get
are hidden form the caller of
App
) To be concrete, that has a similar calling contract executes similarly to this constructor spelling:
Copy code
class App(val myService: MyService = get()) {
    @Composable
    fun compose() { }
}
This is definitely an area where I expect more iteration as we get more experience developing in Compose. Very educational reading all of the thoughts here!
That example comes from the docs here: https://doc.insert-koin.io/#/koin-android/compose
One thing I'm curious about is the equivalence between injecting parameters via default arguments like this and currying. It'll be interesting to see what patterns become common in this area that can be expressed in Kotlin. Given language level support for currying one could do something like this:
Copy code
@Composable
fun App(myService: MyService) {}
val App: @Composable () -> Unit = ::App(get())
(Note: This code doesn't compile today ๐Ÿ™‚ )
g
Thanks Sean for clarification, โ€œsingle responsibilityโ€ is a very good definition, this what I also meant, but not with such a nice choice of words ๐Ÿ™‚ Though, I really donโ€™t think that this service locator approach is good, itโ€™s very unsafe and will be abused a lot. It looks nice in an example โ€œjust call get() in default parameterโ€ but I cannot imagine a code base when a lot of composables would dynamically request dependencies from the graph and different set of them depending on compose state. And now adding runtime exception on top of it. If I would need a service locator in a composable, I would probably instead use standard Compose Ambients directly if we talking about approach as you described it, a single responsibility component which need access to some set of dependencies and under the hood manually manages composable hierarchy, then I donโ€™t see a reason to use service locator for this, it looks that provide an interface with a list of required dependencies to a top level composable for this case is right choice, and this perfectly works with dagger and doesnโ€™t leak any DI/Service Locator implementation details to this top level composable, just inject a provider of all dependencies, it also works pretty well with tests
s
Yep โ€“ to wrap up this conversation for future readers (since we all went pretty deep in the weeds here). The preferred mechanism to pass things to a composable is parameters to the function. This applies even if you are also using dependency tooling. The vast majority of composables written shouldn't be interacting directly with systems like this but just have parameters :).
๐Ÿ‘ 3
a
@Sean McQuillan [G] I can update the doc/sample to align more on the functional approach ๐Ÿ‘
Copy code
@Composable
fun App(myService: MyService = get()) {
}
๐Ÿ‘ 4