I'm trying to determine the best way to manage cha...
# compose
r
I'm trying to determine the best way to manage changes in UI state when the screen configuration changes. I use the width provided within
BoxWithConstraints
to compute the number of a particular UI element that can fit on the screen. Let’s say I want two elements when the width is “portrait” and three when it is “landscape”. These elements display editable state that I want retained across rotations, so that when the third element is rotated off screen and then back on, it displays its old data. (My state is “global”, so I’m not talking about
rememberSaveable
.) The composable I've written that detects the rotation puts out the appropriate number of elements and then uses a
SideEffect
to tell the state what the number of elements is. This seems backwards, since it’s like the state change is reacting to the UI change. (Another detail that may be relevant: I have a button that “tabs through” the visible elements, so that button’s callback needs to check the state to know what’s on screen.) Now I’m thinking that instead I should use a
LaunchedEffect
(with key the number of elements) to update the state and then use the number of elements as recorded in the state (rather than in the local variable as above) to generate the elements. This seems more natural, almost like making configuration change a callback. In any case, I wanted to ask about best practices before I got too far off track. Thanks.
a
It’s a great question, and more guidance is coming soon around this area!
This seems backwards, since it’s like the state change is reacting to the UI change.
Your intuition is correct here, and I’d recommend seeing if it is possible to avoid any
SideEffect
or
LaunchedEffect
at all. At the level of
BoxWithConstraints
, you can transform your width into a specific state (such as number of columns). Then, see if you can combine that state with the rest of your app’s state in such a way so that you don’t need any
SideEffect
s.
(Another detail that may be relevant: I have a button that “tabs through” the visible elements, so that button’s callback needs to check the state to know what’s on screen.)
This is a very interesting detail, one that I have a question about: What happens if the user is tabbed to an element that is only visible at some screen sizes, and then they rotate their device so that the element is no longer visible? Then what happens if they rotate their device back, so the element is visible again?
r
That's a good follow-up question, one that I've been pondering. Based on how my app works I have to keep the currently selected element on screen. I've been trying to settle on a policy as to which of the other elements to remove, and I haven't necessarily found one that is entirely intuitive to the user. It's interesting that you asked that because I was wondering if that feature was worth supporting at all, given that policy issue, and given this rabbit hole I went down thinking about configuration change. But if nothing else it turned into an interesting exercise.
a
That policy is probably going to look a bit different for each app and situation, but the main thing I’d focus on is preserving user’s state. There’s the continuity aspect to that, in the sense that a user’s current state is preserved when the size changes (rotation, folding, window size change). So in your case, ensuring that the currently selected element remains selected after rotating/folding/window size change keeps continuity. There’s also a reversibility angle to that, where if the user rotates/folds/resizes the window, and then immediately reverses that action, they should be brought back to where they started. It sounds like from this:
so that when the third element is rotated off screen and then back on, it displays its old data
That you’re already taking reversibility into account
For the case where some element is hidden, is there some other way to view those elements at the smaller screen size?
r
Correct, I have been at least preserving "reversibility". I was not planning on allowing the off-screen elements to be seen (say through something scrollable or swipeable) because a property I wanted to preserve is that "every change is instantly seen" (it's hard to explain my rationale further without getting into app details). That said, I don't expect rotation to be common -- either it'll be by accident, and you'd quickly rotate back to where you were, or you'll just prefer one orientation over another and stay in it.
👍 1
Regarding your suggestion about avoiding side effects altogether, I don't see how to update state then. Unless you mean pass the information down to all the relevant composables as a parameter? Even then I get a little hung up thinking about how to keep track of which elements are visible without high-level state tracking that (the number of elements in the reduced element config does not necessarily determine which ones are visible). In any case, I appreciate your responses so far, and I certainly look forward to the forthcoming guidance.
I could give an analogy to my app: Suppose the elements are `TextField`s, each specified (selectable) to be in a different language. The currently selected element is the input language, and the others are the output languages -- translations. You could swap between them and thus translate in different directions (and have fun watching how the translations repeatedly degrade 🙂 .) If in portrait mode, you might have English and French elements; landscape might add Spanish. You might want to just stick with two languages, but you might rotate if you want three.
a
Got it, here’s a sketch of a solution to that: You could keep track of the order in which languages were interacted with last, so even if the order left-to-right doesn’t change visually, you are keeping track of an additional piece of state. So the source of truth for your “selected” state would come from the first element in that list (the most recently interacted with language), and then you’d display the
N
most recent languages, where
N
is based on the size available. One consequence of that is your underlying app state will always be able to display all of the languages that could be shown, even if there isn’t space to do so.
In your actual situation, that might require re-organizing your state handling some, but always having the data available to display any screen size should end up simplifying your data flow overall.
r
I already have the extra state pre-allocated for the max elements, so I have that part. Then I was trying to have everything determined by the count like you just did (though not based on "recents", which sounds like the right policy -- thanks for the idea!). But then for some reason I didn't know where to do the ordering (probably because I was stuck thinking inside the composable that was computing the number of elements). But now as I read your answer, I know where to do it: in the callback for the button that selects. Thank you!
a
That sounds great, and you’re welcome! And of course, the callback for the button that selects can know what
N
was, and what was being displayed.
Even then I get a little hung up thinking about how to keep track of which elements are visible without high-level state tracking
There’s a client/server analogy here that I really like (credit to Adam Powell): Imagine that your UI is a client, and anything in a
ViewModel
layer (or in this case, everything outside of
BoxWithConstraints
) is a server. The server should be able to function independently of the client, regardless if there are 0, 1, 2, or more UI “clients” rendering the data. For a few reasons, it’d be very difficult to have the server keep track of how many columns a specific client is trying to render. So in this fictional case, you probably wouldn’t be sending out requests to a server for different sets of data for a small screen or large screens. Instead, you’d encode all of the data necessary in a format where any UI client or clients could display at whatever screen size they have. Analogy aside, there isn’t actually a network request in-between your “client” and “server” to keep everything in sync, but shifting to that mode of thinking where possible can help reframe the problem into a simpler solution in these cases. Because that solution is based purely on state, it will be easier to test, more flexible, and easier to build on top of (hoisting state, animations, alternate ways to present data)
r
There is a requirement I didn't state explicitly that I don’t think we accounted for (and I think it breaks your good client/server analogy). Continuing with my analogy, if the Spanish language box is off screen, I don't want to translate to Spanish (wasted computation). This means that the callback within the element currently taking input text (the selected element) would need to know the number of elements (and thus infer the off-screen elements). Do you do this by passing down the number of elements (or equivalently the
BoxWithConstraints
sizes and deriving it from that) to the elements, which will be captured in the
onValueChange()
as a parameter to the "translation" callback made from the input element? But then individual composable elements are aware of this higher-level state (rather than only a hoisted callback being aware of it). I don’t know if that’s better than a side effect done at the higher level. (Now you did say “the callback for the button that selects can know what N was, and what was being displayed” so maybe you had another mechanism in mind that I missed.)
a
I will say that in most cases, I would expect the “wasted computation” to not really be that expensive. In the cases where there is a measurable impact, I would aim towards a somewhat similar solution to displaying images, where you pass down a
url
until the UI element that is displaying the image. If calculating and passing down the translation is too expensive, pass down some sort of key, identifier, or other lazy mechanism abstracted in a way of your choosing that will only do the expensive computation if actually needed. If that computation is expensive, I’m guessing there’s some sort of loading state and caching mechanism too.