I kept running into the issue with LocalDensity.cu...
# compose
d
I kept running into the issue with LocalDensity.current needing to be accessed in a Composable, which messes with using StateFlows in ViewModels to create adaptive layouts. This is a pretty simple hack to get around it so I thought others might find it useful. Just add the DensityObserver to your root Composable
Copy code
val screenDensity = MutableStateFlow<Density>(Density(1f))

@Composable fun DensityObserver() {
  with(LocalDensity.current) {
    SideEffect {
      screenDensity.value = this
    }
  }
}
s
Why do you need access to the density outside of compose?
d
Adaptive layout business logic in the ViewModel to reposition things as the dimensions change
Copy code
override val showGameInfo = combine(hostSize, viewType, screenDensity) {
  hostSize, viewType, density ->
  if (viewType != StaffViewType.sightReadGame) false
  else with(density) { hostSize.height.toDp() >= 360.dp && hostSize.width.toDp() < 500.dp }
}.stateIn(scope, SharingStarted.WhileSubscribed(), false)
z
Don’t write non-snapshot state from composition. Wrap the value write in a SideEffect. But it also feels like a code smell for a view model to need to know about density.
☝🏼 1
☝️ 2
☝🏻 1
s
I don't feel like your ViewModel needs to be containing this logic at all. You also might wanna look into WindowSizeClass in order to make such adaptive changes in your layout, it's quite convenient.
☝🏼 1
☝️ 3
d
I’ll look at SideEffect. Well it doesn’t really need density directly, but it needs to know Dp size which requires density. I think it is proper to have it in the view model since that is where the logic for how to display the view goes, and it’s more flexible for combining with other StateFlows
👎🏼 1
a
By their nature a ViewModel outlives the Activity instance(s) using them, and information like the density and the available window size is information that you only have when a specific Activity instance is available. You can try to provide that information back to the ViewModel as it updates, but you're going against the grain of trying to have a ViewModel be aware of Activity-scoped things
d
That is really just an android implementation specific thing and not related to the MVVM pattern. It is fine for the view to communicate information to the VM, same as a click event. In this case, it’s the only way provided to get the necessary info in order to apply the logic that is appropriately contained in the VM. https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel
s
You can do whatever you want obviously, but I think you've been given reasons as to why you probably don't want to do this. If you don't contain this at all in your VM but you let this happen in your compose code, what do you think you'd be missing out on?
d
I’m not aware of any tangible reasons mentioned other than the android lifecycle, but all my compose development is multiplatform. I had this happening in the composable before, but to me it was clearly violating the separation of concerns for MVVM. All the visibility logic for my UI components were in the VM, except for these sore spots that required density. The View required state info from the VM that it shouldn’t need because it had to combine it with the density and size info to determine visibility. WindowSizeClass doesn’t get around this.
Anyway it feels like I’m just explaining the same thing over and over without anyone listening. I just wanted to share something I found useful 🤷‍♂️
s
Hey, nothing wrong with sharing something you found interesting. Also nothing wrong with people offering their opinions/advice on it. I appreciate you sharing it even if I wouldn't use this myself.
And by the way, do still look into WindowSizeClass, you can keep syncing it back to your VM if you wish, but you might anyway find it convenient for doing such adaptiveness-related work.
d
Yeah I’ll keep it in mind. I have something similar:
Copy code
class SizeCascade(val specs: List<Spec>) {
  class Spec(val requiredMin: Dp, val output: Dp)

  fun output(input: Dp): Dp = specs.firstOrNull { it.requiredMin <= input }?.output ?: specs.last().output
}