@Composable | |
fun FirstScreen( | |
navigate: () -> Unit, | |
firstScreenTracker: FirstScreenTracker, | |
viewModelFactory: ViewModelFactory, | |
firstViewModel: FirstViewModel = viewModel(factory = viewModelFactory) | |
) { | |
// content... | |
} |
Sometimes, composables are just asking too much from callers.
By that point, we are probably in-too-deep to change that without breaking 10 other things in the process.
Housekeeping
The goal of this post if to figure out how to create independent composables that can:
- Create their own Dagger component
- Inject themselves
- Build a
ViewModel
with a custom factory.
– For more information onViewModels
and compose, check out this meme/blog
TL;DR
@Composable | |
fun FirstScreen( | |
navigate: () -> Unit, | |
firstContainer: FirstContainer = rememberFirstContainer(), | |
firstViewModel: FirstViewModel = viewModel(factory = firstContainer.viewModelFactory) | |
) { | |
// content... | |
} |
Starting point
With Dagger2
, the activity/fragment normally:
- Builds its own Dagger component
- Or grabs some dependencies from the application component
It would then pass dependencies downstream to composables as parameters (as they are or via functions):
class MainActivity : AppCompatActivity() { | |
@Inject lateinit var firstScreenTracker: FirstScreenTracker | |
@Inject lateinit var viewModelFactory: ViewModelFactory | |
override fun onCreate(savedInstanceState: Bundle?) { | |
DaggerFirstComponent.builder().build().inject(this) // build dagger component and inject | |
super.onCreate(savedInstanceState) | |
setContent { | |
AppTheme { | |
Surface { | |
FirstScreen( | |
navigate = { TODO() }, | |
firstScreenTracker = firstScreenTracker, // pass injected parameters downstream to composable | |
viewModelFactory = viewModelFactory | |
) | |
} | |
} | |
} | |
} | |
} |
Information can also be passed down as CompositionLocals.
That approach is debatable, to say the least, and will not be explored in this post.
Migration
In order to make FirstScreen
independent, we will need to isolate the injected dependencies into a separate class.
The @Stable
annotation will help the compose compiler know that this class will not really change after it has been created.
The Dagger component
@Component( | |
modules = [FirstModule::class, VmModule::class] // dagger modules here if needed | |
) | |
interface FirstComponent { | |
fun inject(firstContainer: FirstContainer) // inject the container instead of the activity/fragment | |
@Component.Builder | |
interface Builder { | |
fun build(): FirstComponent | |
} | |
} |
The compose layer
@Composable | |
fun FirstScreen( | |
navigate: () -> Unit, | |
firstContainer: FirstContainer = FirstContainer().also { | |
DaggerFirstComponent.builder().build().inject(it) | |
}, | |
firstViewModel: FirstViewModel = viewModel(factory = firstContainer.viewModelFactory) | |
) { } |
Job Offers
While this initially works, it will also create the Dagger component again on every single recomposition.
For an innocent example like this one, it would barely be a hit on performance. Not so for more complex screens.
Let’s use the classic remember
keyword, then:
I am text block. Click edit button to change this text. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut elit tellus, luctus nec ullamcorper mattis, pulvinar dapibus leo.
@Composable | |
fun FirstScreen( | |
navigate: () -> Unit, | |
firstContainer: FirstContainer = rememberFirstContainer(), | |
firstViewModel: FirstViewModel = viewModel(factory = firstContainer.viewModelFactory) | |
) { // content here.. } | |
@Composable | |
fun rememberFirstContainer(): FirstContainer { | |
return remember { | |
FirstContainer().also { | |
DaggerFirstComponent.builder().build().inject(it) | |
} | |
} | |
} |
Should someone actually do this?
This approach goes against most compose guidelines. Composables should really be pure functions, fast, idempotent, and free of side effects.
But, when major refactoring is not really feasible, this will get things working without too much effort.
Don’t forget (sorry😑)
If efficiency is paramount, remember
will not really cut it. Ian Lake explains why here:
remember is *not* enough of a signal to survive being removed from the Compose hierarchy i.e., when you are on the back stack
In cases such as the above, the DI component will be recreated and injected into the composable again.
It’s not the end of the world, but it is a drawback — especially for heavy Dagger components.
There are 2 options for more advanced scoping if you are worried of losing remember
values too easily:
- resaca
– By sebaslogen. Works great 👍 - Circuit
– By Slack. It does way more things than just a more powerfulremember
variation. I have no personal experience with this one
Anyways
Hope you found this somewhat useful.
Later.
This article is previously published on proandroiddev.com