Hi, I'm currently learning compose and am finishin...
# compose
g
Hi, I'm currently learning compose and am finishing up the Layouts codelab in the official docs. I am not a fan of how modifiers work, but it may be because I don't fully understand them yet. The main issue I have is this... order matters when placing modifiers, constraints flow right and size flows left. But how are you supposed to know if a constraint was changed or not? A good example is the padding and size modifiers example in the codelab. Padding does not change constraints, but size does. So placing padding before size and visa versa will have different results. It's not immediately apparent that padding does not change the constraints though. Unless there is something I'm missing, there isn't any way of knowing. With all the different types of modifiers that will be available, I can see loads of bugs arising from this. Is there any way to tell what constraints a modifier changes without looking at the source code?
👍 1
a
Hi, thanks for taking a run through the codelab! From a runtime determination perspective, only the modifier itself needs to know how it is going to behave. It's similar to
Flow
in this way; each link in an assembled chain applies its own behavior and the links to either side of it communicate with it only through a very narrow contract.
Just like with
Flow
operators, this brings a lot of composability and extensibility, but it means that the burden falls on the docs and appeals to familiar API conventions for each modifier to set expectations for the developer assembling them
g
That makes sense. Its a powerful design but will take some time to get used to.
What's not intuitive about the padding and size example, which is probably what makes it a good one for teaching modifiers, is that the padding actually becomes a margin when placed before size. ....padding(15.dp).size(200) results in an actual size of 230x230. Which is usually how margin works I believe
c
@gbaldeck I stumbled upon this too a while back and it brought up this article that I found floating around on twitter. although it may not help you with compose, you might find it helpful to try to break out of the whole padding vs margin thinking. https://mxstbr.com/thoughts/margin/
g
That article does not apply to compose, but I also disagree with what he is trying to say. Margin is part of your component. It is part of the total space. It is encapsulated with your component if you put it in the CSS scoped to your component.
It is not harder for designers to reason about because they can see the total X and total Y of the component
I think what he means is to not use margin: auto, and instead use a css library with a grid, which i do agree with. But he is instead making a blanket statement about margin
a
the thinking in that post is applicable to how we thought about it for compose. There's also a bunch of history and experience from android views we applied:
👍 1
Views became a classical fragile base class. Any time we wanted to add new properties, View itself became a bottleneck and sort of a dumping ground for all of them, along with the deployment mechanism of OS updates
it was the only place where we could add a new property that would apply in the same way for all possible View types
A practical and padding-related example of this is when we added right to left layout support, which brought start and end padding
in order to keep compatibility, and to make sure that layouts written for an OS version that supported bidi layout could also be written to work on older versions using only the absolute padding properties that existed back then, we came up with a complex set of layering and override rules. How and when start or end overrides either left or right, etc.
and this cascaded down to other uses of padding, like how background drawables could include padding as part of their 9-patch asset definition. How and when these set or apply, how they shadow one another if the layout direction changes dynamically, how we need to keep original input values to potentially restore later in those cases
then there's other padding modes of scroll bars, another "universal" feature that was implemented as a part of View itself, which can contribute a form of additional padding
it got to the point where we couldn't keep the full set of behaviors in our heads anymore. No matter how many tests we wrote to confirm the correct, "proven" behavior, during the development of API 17 when bidi padding was first added, any time someone changed the code I would tell them, "see you on the chase list tomorrow" - because as we distributed the build to our internal test population, the probability of someone reporting a broken app from play store within 24 hours approached 1
the root of the pain was that all of these interactions with one another were custom. The code that processed any one padding property had to be aware of all of the others to work correctly.
With compose we set a goal to avoid the View fragile base class problem and add these kinds of properties externally with things like wrapper components, or eventually, modifiers.
We then looked at the problems of interactions between components; how to express in one of these externalized properties how they order against, add to and shadow one another. If modifier A and modifier B were written by different people as part of different libraries that have no knowledge of one another, how do you make them work together and establish what "correct" means?
All of the models we explored and tried weren't worth it. They all made it far too complicated to write one of these properties, and would result in a blame game of library A+B user having their bug redirected between both libraries, each pointing the finger at the other.
And so we took the approach of Rx or Flow operators or Flutter's single child widgets: minimize the contract and have ordering be explicit, and applied by the only person who knows what behavior they want when they combine libraries A and B: the person assembling the modifiers, by way of explicit ordering.
With that decided, it wasn't possible to define "margin" in the classical sense of, "margin is always outside the element" since modifier order is what determines the notion of inside or outside.
g
I see your reasoning behind it now and how powerful it is. Thanks for the explanation. It is going to be a learning hurdle for most, that is for sure. When the order of modifiers can make padding act like margin, and it's not completely apparent why, it's going to cause some confusion. Still it will just take time, especially for people coming from a flutter or web background
Like myself
a
Yep, we know. We've done more user studies on this precise case than any other so far. 🙂
and as you noted, it's why the case features so prominently in the codelab
I still think we're missing a core piece of documentation around how compose's layout system works more generally
It's still an option for layout containers to define margins as parent data/layoutparams, but we've generally avoided it so far in the standard layouts
since a parent works with the outermost, already-modified element for each child, and it can apply margins at that layer
g
I agree, some additional diagrams and other examples would be helpful. Particularly around things like making the composable clickable (and modifiers like clickable) and what parts of the modifier chain it will apply to
a
basically like how
ViewGroup.MarginLayoutParams
always worked
Definitely. We're in the process of reworking some of the lower-level input handling at the moment and that's cascading up to other APIs like clickable as we speak
we also spent a lot of time discussing whether and how we should support collapsing margins if we supported margins at all; members of the team with a web background were strong proponents of doing so, but android view margins never collapsed. This was part of why we decided to call the remaining modifier "padding" so that there wasn't any expectation of collapsing behavior. (We also gave, "spacing" a try to further distance the term, but that got confusing since it doesn't imply all sides of the element and it's not a term people look for in autocomplete)
c
Working with my designers on our compose component library... they really like not having a concept of margin. A component is everything within their view bounds and if they want spacing on top of the component for example, then they like that they can put in a spacer_2 component in there. It makes really dynamic lists really easy rather than building these components with margin in isolation and then you stick them together in a list and you're like "oh god whats with all of these huge gaps between components"
a
to be fair, part of why that was always so painful in android view layouts is because we never supported collapsing margins. Usually that goes a long way to avoiding that kind of problem.
👍 1
or rather, we tried some years back, you can still find a bunch of code related to, "optical bounds" in View - it never really worked right
👍 1
since for it to be useful, it sort of has to cascade up the whole view hierarchy so that adding one more layer of container doesn't change the meaning of everything
k
@Adam Powell In the context of https://kotlinlang.slack.com/archives/CJLTWPH7S/p1608047588295500?thread_ts=1607996900.279600&cid=CJLTWPH7SS I am not sure what exactly you mean by
The code that processed any one padding property had to be aware of all of the others to work correctly.
As i can only assume that you mean: the code that processes any one of the padding needs to be aware of any changes in the behaviour of any portion of the rest of the code that may handle padding (such as adding code for supporting bidi padding) And then ensure that everything still works the way it should with both the new behaviour, and the old behaviour
a
Android had a bunch of unique challenges due to compatibility but even in general, when you define a fixed box model, you define a fixed order and behavior of properties relative to one another. So yes, whenever one shadows another, those interactions become fragile and difficult to reason about
k
So for example, a modifier that implements padding would need to also deal with bidi padding (probably) And if a user creates their own padding modifier, it would also need to support bidi padding for RTL and LTR layouts, and so on for other modifiers, right? Or would this be left souly up to the user as to whether they want to support bidi padding or not, if they don't support bidi padding then their Modifier breaks in RTL and LTR layouts, but other padding modifiers that do support bidi padding correctly, will not be affected as they correctly handle bidi padding, such as the default padding modifier provided by Compose Modifier