Hi Everyone! I'm currently investigating Jetpack c...
# compose
j
Hi Everyone! I'm currently investigating Jetpack compose and how it might work for our project going forward, but we are heavy users of Dagger in the current world of Android. One project I've been looking around in is the JetNews app, and there is no use of Dagger or Hilt. Any idea how Dagger/Hilt will play with Compose? I'd imagine composables would end up being injectable, just like fragments and activities are right now. Would appreciate any articles or references if this integration already exists 😄
z
There’s some discussion here: https://kotlinlang.slack.com/archives/CJLTWPH7S/p1591604904406700 (both in the thread and just below this message in the channel)
👍 1
j
I think it would be difficult to have dynamic tree of functions play along with generated code from Dagger. I am unsure how to do that. But I was messing around with Kodein (but I think the idea can be done with Koin as well) where you create your own dependency injection tree/components as you move along. Basically what Dagger does is having a component for you app, creating from that a subcomponent for your activity and then creating another subcomponent for your fragment, creating a nice tree. You can create such a tree yourself if you use a dynamic DI framework in runtime. For example you can create an
ambient
that holds your DI object (the DI object in Kodein is basically a component in Dagger terms) An ambient is passed on down the composable function tree, and you can Provide different values for it. So you can with that extend the component by making a subDI on the current DI object (so basically create sub components) The code for making this happen is rather simple:
Copy code
val DI = ambientOf { DI {} }

@Composable
inline fun <reified T : Any> instance(): DIProperty<T> = DI.current.instance()

@Composable
fun composeSubDI(
    bind: DI.MainBuilder.() -> Unit,
    content: @Composable () -> Unit
) = Providers(
    DI provides subDI(DI.current, init = bind),
    children = content
)
Then your app can look something like this:
Copy code
```override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
        Providers(
            DI provides di // the activity/application di object (aka component)
        ) {
            someChild(1)
            someChild(2)
            someChild(3)
        }
    }
}

@Composable
fun someChild(argument: Int) {
    composeSubDI(
        {
            import(SomeViewModule())
        }
    ) {
        // DI.current is now whatever sub component of the component above
        val viewModel by DI.current.instance<MyViewModel>(argument)
        Text(text = viewModel.state.asState().someText)
    }
}
Now you have for example the main component, with 3 sub components next to each other (and their bindings will not conflict as they are separate sub components)
so yeah it is already quite doable to have dependency injection, having scoped tree objects etc. using existing dependency frameworks. But I do not know if it is possible with Dagger and especially with Hilt, as Hilt is build to be used together with android components like activities and fragments, where Compose is in essence a list of stateless functions
s
Here I am actually curious how it is going to play out in big apps, where one screen can have 10+ interactions Doesn't seems like providing 10 callbacks as function parameters is such a great idea. At the same time, I am even less sure if Dagger will help here either
z
Yea, this doesn't sound like something I would use dagger for. One way to do this is to wrap up some of the parameters to your screen composables in an Immutable view model type (not the jetpack thing). This can just be a data class with the data for the screen, and that data can include callbacks for the various interactions.
1
Typically the only thing I've seen UI layers that aren't forced to deal with all the Android overhead need to actually inject is stuff like formatters for date/time/money, and that seems like it would be reasonable to just provide directly via Ambients.
j
@Zach Klippenstein (he/him) [MOD] I saw the 1 or 2 screen examples (like the jet news app) and there are no solutions for this. For an app that consists of 1 or 2 screen it is easy to pass it, but if it becomes more then it will be very cumbersome. You should imagine that with compose you can create separate components much easier than with the old framework, leaving you perhaps with many more abstractions of logic than you have right now. Making 1 god viewmodel to handle your entire app which you pass down as an argument seems not the correct approach to me. Instead, making the components independent would be the thing. Only pass the information to the component what it needs (so perhaps some information and a callback interface). After that, the component should be able to do what it wants freely, that is how I see it. So if you want backend interaction from a button click, you somehow need to feed your dependencies to make that happen (viewModel -> usecase -> repository -> service etc..) To pass this all down with arguments would mean making your own DI injection, in my example I just reuse an already existing DI injection framework (Kodein) to do the work for me. So basically what I want to achieve, is a DI object that I pass down the composeable tree. I could use an argument, or use the Ambient (because this is the purpose of ambients). Using these ambients with the Provides() methods I can easily make scoped components
I do not think it is abusing the feature, but rather using it
z
I agree god objects are bad, and keeping concerns (data as well as logic) separated is good. I don't think that DI/SL is necessarily the right solution to that particular problem though.
It's great for providing service objects that vend data. Eg a presenter (or activity) might inject a repository that provides a stream of data. But it would probably be a smell for a custom View class to inject anything into itself other than some sort of presenter/controller.
1
j
Well I don't think everything has to be in the view. Using a viewmodel that exposes a viewstate makes the thing pretty easy to understand. (Not the android component viewmodel but a viewmodel that lives and dies with the component). Then you separate the logic from the view, making it more testable. Then the usecases and the rest are injected into the viewmodel