Hello <@U6158UG1Z>, Regarding <this> tweet of your...
# compose
j
Hello @jim, Regarding this tweet of yours: whilst I generally share the sentiment of not tying up code to the platform, I’m sometimes struggling when it comes to practice. 1) When using Hilt,
hiltViewModel()
is the only “easy” way to get dagger-managed objects from within a
@Composable
function. Are there any alternatives? 2) In a non 100% compose app (where we can’t just disable all configuration changes in the manifest) any operation running inside a
rememberCoroutineScope()
will be interrupted during a configuration change. In these cases we need to resort to the `ViewModel`’s
viewModelScope
. Are there any alternatives? Any insight on the topic is very much appreciated!
👀 1
z
I think DI frameworks are hard to unwind - dependency injection as a design principle, writing functions and classes that take dependencies as parameters, actually makes this stuff simpler if done at the right levels of abstraction.
j
Yes, accurate clarification. I was using DI as shorthand for DI Framework, but agree'd, taking in dependencies as parameters is highly desirable.
c
Do you have some recommended patterns for those cases where a coroutine scope is required for "setting state" (example
drawerState.open()
)? I'm doing this inside my Composables. Moving the drawerState to something like a ViewModel is one option but it doesn't feel right for mixed Compose and android View apps
j
Using
rememberCoroutineScope()
for things which are UI related and not long lived like that. You grab the coroutine scope up near where the drawer lives, and use it in the callback that is passed down to your button (both because that's the correct data flow, and also to ensure the coroutine has the correct lifetime and isn't running for longer/shorter than the drawer lives).
👍 1
c
That's what I'm doing anyway. I think I misunderstood your comment that Composables shouldn't know about coroutines
m
everyone is talking abut `ViewModel`s not being needed in
Compose
and DI being bad. yet… everybody uses AAC
ViewModel
and
Hilt
. when I asked “are `ViewModel`s needed” everyone replied on that thread with “yes. they are not to be dropped yet” so am a bit confused. it’s not that hard to avoid
ViewModel
, when I think about it, but I haven’t tried it in anything but simple examples to see how it works out. can any of you link to some examples that uses custom `ViewModel`s and no DI framework (passing dependencies as parameters, always) so we (I) can see how that plays out and how that looks like in real project? how does it scale, what are the major problems with it etc. it’s easier with examples
⬆️ 1
c
Personally, I tried to not use AAC VMs on my brand new android app, but I think you pretty much need them if you're using AAC Navigation + hilt, BUT you should use the VM for anything except the top level "Screen" composable. All of your other composables should not have a VM injected. That's basically how I'm working right now.
👍 1
j
Most of the large apps using Compose are closed-source. Simplistic but good open source examples: https://github.com/JetBrains/compose-jb/tree/master/examples
z
Also i think most large apps end up building their own “architecture components” from scratch – Scoop, Riblets, Workflow, etc
👆 1
m
@jim yeah, they are hard to come by. thank you, I will take a look
@Zach Klippenstein (he/him) [MOD] that is also true
@Colton Idle am using `ViewModel`s but only on top most level and I pass parameters to screens and it’s composable after that too. when am constructing screens inside
NavHost
I create
ViewModel
and I pass just state and lambdas to screens
j
@jim in my specific case I’m using
ViewModel
via
hiltViewModel()
just as a shortcut to tap into Hilt to get dependencies. My viewmodels are empty classes with only dependencies provided by Hilt as properties. Each screen composable (i.e. the root composable of each navigation destination) gets its own viewmodel calling
hiltViewModel()
, dependencies are then passed down the composable hierarchy as usual. The idea was to use a “wrapper composable” around the actual “screen composable” as the “entry point” that taps into Hilt. To give you a rough idea:
Copy code
@HiltViewModel
class MyScreenViewModel @Inject constructor(
  val stuff: stuff
) : ViewModel()

/**
 * Just a wrapper around the actual screen composable to keep the platform dependency "isolated".
 */
@Composable
fun MyScreenWrapper() {
  val vm = hiltViewModel<MyScreenViewModel>()
  MyScreen(stuff = vm.stuff)
}

/**
 * This is the actual screen composable which is not tied to the platform.
 */
@Composable
fun MyScreen(stuff: Stuff) {
	// Here the actual UI is built using reusable composable components.
}
I could delete the viewmodels and the screen wrappers: this will untie my composables from the Android platform, but then the only place remaining where I can tap into Hilt is
MainActivity
. This means I’ll have to make
MainActivity
as the “single point of injection” with Hilt and grab all dependencies from there. My initial gut feeling was that I’ll end up with too many
@Inject
lines inside `MainActivity`: Would this scale with lots of screens? Now the more I think of it the more I think this is not a bad idea at all and that there are easy solutions to the “too many
@Inject
lines”
problem. What do you think?
j
👍 Making MainActivity the only point of injection sounds right/reasonable to me.
👍 2
🙏 1
j
@jim I’ve tried tinkering with this but it was a dead end. Using MainActivity as the only point of injection is easily doable but then we loose the ability to hold (as in “cache”) objects for the lifetime of a screen. Let’s assume that: 1. We can’t avoid all configuration changes (which has never been as true as of Android 12 https://commonsware.com/blog/2021/10/31/android-12-wallpaper-changes-recreate-activities.html ). 2. Our app has a single Activity with navigation implemented using the Jetpack Navigation library. THEN: If we don’t use ViewModels in each screen composable I can’t see no other practical means to create an in-memory cache of data that: • Survives configuration changes. • Spans the lifetime of a screen (keep in mind we’d like to hold on this cache when the screen is not visible but still in the navigation back stack). • Is automatically released (i.e. freed) when the screen is removed from the navigation back stack. Unless one is implementing its own navigation library/framework (i.e. Scoop, Riblets, Workflow...), which we assume we aren’t in this case. Given all of this context, then how effectively can we “avoid” Architecture Component ViewModels ?
1