https://kotlinlang.org logo
#compose
Title
# compose
b

Bryan Herbst

10/21/2020, 2:44 PM
I have my own typography system (i.e. not
MaterialTheme.Typography
), but I’d like to lean on Material for a number of components such as
Button
. Specifically looking at
Button
, I’d like to use my own
FancyTheme.typography.button
instead of
MaterialTheme.typography.button
. My first thought was to do something like
Copy code
@Composable
fun FancyButton(content: @Composable RowScope.() -> Unit) {
  Button {
    ProvideTextStyle(
      value = FancyTheme.typography.button,
      children = content
    )
  }
}
But that doesn’t quite work since
ProvideTextStyle
’s content doesn’t accept a
RowScope
receiver. Is there a better way to do this?
I’d like to avoid re-implementing all of Button. I could also add my own
Row
in
FancyButton
, but that doesn’t seem ideal either.
z

Zach Klippenstein (he/him) [MOD]

10/21/2020, 2:52 PM
This doesn’t work?
Copy code
@Composable
fun FancyButton(content: @Composable RowScope.() -> Unit) {
  Button {
    ProvideTextStyle(FancyTheme.typography.button) {
      content()
    }
  }
}
b

Bryan Herbst

10/21/2020, 2:55 PM
Ahh, it does, thanks!
z

Zach Klippenstein (he/him) [MOD]

10/21/2020, 2:56 PM
I think you can save a lambda if you invert those composables too:
Copy code
@Composable
fun FancyButton(content: @Composable RowScope.() -> Unit) {
  ProvideTextStyle(FancyTheme.typography.button) {
    Button(children = content)
  }
}
b

Bryan Herbst

10/21/2020, 2:59 PM
I haven’t tested that one yet, but my assumption is that text style would be overwritten by `Button`’s internal
ProvideTextSyle
z

Zach Klippenstein (he/him) [MOD]

10/21/2020, 3:00 PM
oh, that’s probably right
a

Alexjlockwood

10/21/2020, 6:50 PM
this is a general problem i’ve faced as well while building out a design system (the fact that we want to use our own custom colors and text styles, but many of the material components have hard dependencies on their own color/type palette). i think you have a couple options if you want to reuse
Button
. (1) require that the caller to pass in a
Text
with a text style explicitly specified in its
style
argument (which will ensure that material will not overwrite the text style) (2) haven’t tested this, but i think you could also re-wrap the
Button
in a new
MaterialTheme
with a different
Typography
palette specified. so if
Button
internally uses
MaterialTheme.typography.button
, then you would wrap the
Button
in a new
MaterialTheme
that maps
Typography#button
to the text style you use in your design system. i personally don’t like either of these options tbh. (1) is not ideal because people are not going to remember to do this (and even if they did, it’s kind of tedious), and (2) feels like i’m hacking around an implementation detail in the material compose library that could suddenly change under my feet. what i’ve ended up doing in a many cases is just writing my own components from scratch, using the material compose library source code as a guide. i will say, it actually hasn’t been that bad… i’ve forked a pretty small amount of code so far. but i guess it really depends on how different your design system is from material (our design system is different enough that these hacks around material were not worth it)
oh also, with approach (1) you could potentially make things a bit better by requiring the caller to pass in a
String
instead of a generic
Text
composable. that would give you more control over the creation of the styled text, but also would make the component less flexible since you’d be removing the Slot-based API
b

Bryan Herbst

10/21/2020, 7:07 PM
I think you hit the nail on the head @Alexjlockwood - those are two options that I had explored and didn’t like either. The farther I go down this path with hacking around
MaterialTheme
the closer I’m getting to discarding
MaterialTheme
completely and basically forking the Material components. It’s unfortunate because I think the Material components get me 75% of the way there, but that last 25% is different enough that it will turn out to be worth the tradeoff.
a

Alexjlockwood

10/21/2020, 7:16 PM
yeah, i’m even getting a bit wary of even using things like `RippleIndication`… while it is true that it currently has 0 hard dependencies on the material color palette, how can i know this will continue to be the case? and i would really rather not fork
RippleIndication
it would be nice if there was some guarantees that the material compose library could provide. for example, maybe a convention that
MaterialTheme
properties could always be customized (by using them as default arguments in public-facing component APIs). otherwise you end up having to hack around provider-statements that are used internally within their components with no great way of safely overriding them. (cc @nickbutcher who i’ve been talking to a lot about this as well 😄)
b

Bryan Herbst

10/21/2020, 7:21 PM
I had just been thinking about how great it would be if the Material components always exposed their
MaterialTheme
-provided properties! Would love to see replacing
MaterialTheme
become easier. JetSnack’s hack of changing all the colors to an obnoxious color to discourage use is certainly a clever way of going about it, but I think it highlights the need for a more cohesive approach
😂 1
a

Alexjlockwood

10/21/2020, 7:27 PM
yeah, the color debug thing is kind of the best you can do. unfortunately i wasn’t able to find a quite-as-obnoxious way of doing the same with text styles. lol eventually i just decided to not use
MaterialTheme
at all and it’s been not so bad so far. luckily a lot of the more obscure/non-finalized stuff like accessibility (well maybe not for you 😉) is encapsulated in things like
Modifier.clickable
and
Modifier.toggleable
in foundation… so `Button`s end up being basically a
Box
with a clipped shape and a colored background color. it’s been a nice learning experience too. 😄
❤️ 1
l

Louis Pullen-Freilich [G]

10/21/2020, 8:22 PM
Thanks for all the interesting thoughts! In this specific case, is there something you find off-putting about creating a wrapper that provides your own text style internally? On a related note, some of this confusion is a result of
AmbientContentColor
and
AmbientTextStyle
being foundation primitives, when they are realistically Material theming ambients that live in the wrong layer. These will be moved to material soon along with the existing
Text
component, which makes this separation more clear and leaves design system related ambients out of foundation, leaving un-opinionated components (similar to the split with
TextField
and
BaseTextField
). For this use case when you have a different type scale, you can easily create a
FancyText
that uses a
FancyTextStyle
, and just use that in your
Button
wrapper instead. Part of the complexity here is because you are trying to introduce a separate type scale alongside the existing Material one - if you forget to provide the text style then you will actually be using the Material type scale, which can be easy to miss. Some other miscellaneous thoughts:
oh also, with approach (1) you could potentially make things a bit better by requiring the caller to pass in a 
String
 instead of a generic 
Text
 composable. that would give you more control over the creation of the styled text, but also would make the component less flexible since you’d be removing the Slot-based API
It's true that this becomes less flexible, but if you are creating components for use in your app (as opposed to a publish component library) then this is probably a positive - it means that you can be opinionated and guarantee the same UI across all usages of your components, and make the call site more simple and easy to use. For example, you could consider accepting an
R.string
resource or similar representation so that the
Button
itself can do the localization.
what i’ve ended up doing in a many cases is just writing my own components from scratch, using the material compose library source code as a guide.
This is actually an explicit design goal of the material / foundation libraries. Material is not the design system, it's a particular implementation of a design system - if your design system differs then creating your own type safe, semantically meaningful theme with corresponding components is absolutely the correct choice here.
it would be nice if there was some guarantees that the material compose library could provide. for example, maybe a convention that 
MaterialTheme
 properties could always be customized (by using them as default arguments in public-facing component APIs).
In general we try to follow this for colors for example, every color pulled from the theme is just an override-able default. I think a text style is harder / more confusing to expose in the API, since there may not be any
Text
inside to actually consume the text style. For these use cases creating a wrapper
Button
that provides your custom text style seems reasonable too.
so `Button`s end up being basically a 
Box
 with a clipped shape and a colored background color
Yup! We try to layer things so it is easy to copy and paste and drop down to the layer relevant to you, these abstractions exist to encourage everyone to build meaningful components. The goal is to make the bar so low that in most cases it is easier to build your own
FancyButton
than trying to hack around with ambients to override Material opinionated parameters that aren't really intended to be changed.
b

Bryan Herbst

10/21/2020, 8:44 PM
I don’t think there’s anything off-putting in the Button example with wrapping the button in
ProvideTextStyle
. I do think that more generally it is a common theme with colors as well as typography as Alex noted. My concerns with going the re-implementation approach are: • Education & awareness - how do I get engineers on my team to use
FancyText
instead of
Text
? Most examples are going to use
Text
, so I think it’s natural for engineers to gravitate towards that. This is an issue with both wrapping and copying, but worth noting. • Mixing Material and custom components - There are some Material components that I don’t think I’ll need to (or would want to) modify. For example,
ModalBottomSheetLayout
. One easy solution to the education & awareness issue is to avoid exposing the
androidx.compose.material
library to our app code, but that makes using components we can use more difficult • Updates to components- It is easy to copy & paste
Button
today, but if a future version of Material makes changes to
Button
I need to know about those changes and then actually put in the effort to pull in those changes. I’m thinking more than just visual changes- things like accessibility improvements are likely to happen in the future. • New components - If we go the route of re-implementing the Material components, we inevitably will reach a point where Material introduces a new component that an engineer wants to use. I’d much prefer it is was usable (or easily wrappable) out-of-the-box over copying and pasting.
a

Alexjlockwood

10/21/2020, 8:47 PM
oh cool louis is here 👋
In this specific case, is there something you find off-putting about creating a wrapper that provides your own text style internally?
i think it’s mainly just the fact that i can’t be sure the material design spec will change some day w/o me realizing it. for example, i could wrap around
ListItem
and create my own
MaterialTheme
wrapper that maps our custom text style
CoreUiTheme.typography.bodyF1
to
MaterialTheme.typography.body2
for the purposes of customizing the list item’s secondary text, but it’s sort of ambiguous whether usage of this specific material text style is an implementation detail or part of a public facing API that will never change.
Yup! We try to layer things so it is easy to copy and paste and drop down to the layer relevant to you,
so far my experience creating my own components has been relatively painless. i think a big reason is the separation of foundation and material. it sounds like you are focusing a lot on moving as much core-logic as possible into foundation, which is definitely appreciated!
Updates to components- It is easy to copy & paste 
Button
 today, but if a future version of Material makes changes to 
Button
 I need to know about those changes and then actually put in the effort to pull in those changes.
and yeah, this is in truth the biggest annoyance ive faced so far. but i guess i can’t blame anyone but myself for getting into compose during the alpha stages 😂 in the meantime, i’ve been trying to figure out the best way to keep up-to-date with the changes that are happening internally each week (reading changelogs, browsing git history of the material source code every once in a while, etc.). i’ve also been putting off more complex components like text field which sound like they are being revamped w/ a new focus api or something (according to issuetracker at least 😄)
1
l

Louis Pullen-Freilich [G]

10/21/2020, 10:03 PM
One easy solution to the education & awareness issue is to avoid exposing the 
androidx.compose.material
 library to our app code, but that makes using components we can use more difficult
Yep, for larger apps this is the intended / recommended approach. It lets you expose only the components you want to expose, and ensures consistency - but as you mentioned it means you would need to wrap every component. Probably a good thing for large apps (so you can guarantee that the colors are correct, etc) but it is a non-trivial amount of boilerplate for smaller apps.
Updates to components- It is easy to copy & paste 
Button
 today, but if a future version of Material makes changes to 
Button
 I need to know about those changes and then actually put in the effort to pull in those changes. I’m thinking more than just visual changes- things like accessibility improvements are likely to happen in the future.
That's true - I think copy and pasting is for the exceptional case where you have a very not-Material
Button
- in most cases since you can override the background color and shape, all that's left is the default size (which you can also override with modifiers) - so just wrapping the underlying
Button
should shield you from these sort of changes.
New components - If we go the route of re-implementing the Material components, we inevitably will reach a point where Material introduces a new component that an engineer wants to use. I’d much prefer it is was usable (or easily wrappable) out-of-the-box over copying and pasting.
If your design system has custom colors / typography / shapes, then the default out-of-the-box component may not match your design system anyway, so it may require some pre-work from your side anyway. In any case for most Material components, some / a lot of the customization parameters might not be desired for an app, for example you might not want developers to be able to customize the shape of a
Button
- so having a wrapper makes it easier to expose only the things you actually want to expose, as well as setting the correct.
but it’s sort of ambiguous whether usage of this specific material text style is an implementation detail or part of a public facing API that will never change.
Ah I see, yep this is an interesting question. Default ambient values inside a component are not much different from default parameters though in that they may change. You can / should probably avoid this by using your own theming ambient that you know won't change to express this contract, instead of proxying your system -> Material.
b

Bryan Herbst

10/22/2020, 1:18 PM
Thanks Louis and Alex, this is all super valuable in helping guide our approach. For context, our design system isn’t particularly broad- it has a handful of components, typography definitions, and literal colors, but no semantic colors and nowhere near a full component library. While our app itself is definitely in the “large” bucket, our design system is in a bit of an intermediate size which is why we lean on a mix of standard Material components and components from our design system. I’m going to start experimenting with wrapping Material and not exposing it to our UI modules and see how that goes!
l

Louis Pullen-Freilich [G]

10/22/2020, 1:41 PM
Please let me know how it goes! A lot of these are still early thoughts, since no one has actually tried to build a large design system on top of Material / Compose. So far this has held up quite nicely in explorations / smaller samples like Jetsnack, but feedback here is really helpful so we can figure out how to make this even easier.
p

Prashast Rastogi

05/16/2021, 7:44 AM
@Bryan Herbst I'm struggling to create my own Typography which will contain custom text style names (not h1,h2,h3 as defined by material theme). Can you please show how did you create
FancyTheme.typography.button or FancyTheme.typography.text1
b

Bryan Herbst

05/17/2021, 12:40 PM
We don’t support overriding the text styles within our typography system, so it is just an object:
Copy code
object FancyTypography {
  val text1 = TextStyle( /*...*/ )
  val button = TextStyle( /*...*/ )
}
t

Tash

10/05/2021, 6:39 PM
Super insightful discussion! We are currently in the process of creating a design system implementation in Compose. Since we’re a large scale app, we’ve been trying to evaluate how far our “wrapping of Material” should go, and if there is any 1:1 relationship between our semantic tokens and that of Material’s that we can leverage. Given that this discussion was about a year ago, curious @Bryan Herbst @Alexjlockwood how have your design systems panned out so far? Were there new gotchas/new insights along the way? Thanks!
b

Bryan Herbst

10/05/2021, 8:26 PM
It has worked out quite well. We have a lint check that blocks use of Material components and offers quick fix replacements from our design system. Wrapping Material wasn’t as much work as I anticipated, though there are some specific use cases (particularly around bottom sheets) where we had to copy over (and keep in sync with upstream) much larger amounts of code than I would like.
t

Tash

10/06/2021, 8:23 PM
@Bryan Herbst That’s great to hear 🙌🏼 Yeah one of the concerns we also have is keeping up with Material implementations/additions of new widgets, etc. Just curious, did y’all go the “Jetsnack route” and override all MaterialTheme “tokens” with a “debug color” to encourage usage of the design system tokens? Kinda related, is your design system/theming system extensible in the same way MaterialTheme is? 🙏🏼
b

Bryan Herbst

10/06/2021, 8:27 PM
Just curious, did y’all go the “Jetsnack route” and override all MaterialTheme “tokens” with a “debug color”
Nope - Using anything from the Material Compose packages is a lint error in our codebase, so that isn’t a problem :)
Kinda related, is your design system/theming system extensible in the same way MaterialTheme is? 
Nope - we only have a single app with a single design system, so we don’t need to support configuring colors, shapes, typography, etc.
🙏🏼 1
t

Tash

10/07/2021, 2:58 AM
Using anything from the Material Compose packages
i see, so its a complete replacement 👍🏼
we only have a single app with a single design system
thats great, keeps the implementation from getting problematic if any type of “sub-theming” needs to be done
2 Views