Skip to content

Add a Container equivalent that inherits from a theme #154740

Description

@caseycrogers

Use case

I've found Flutter's built-ins (including Material) to be VERY good if you're just trying to get your app built and are not very sensitive to it's specific appearance (ie you don't mind if everything looks like Material). I've found it to be much harder if you're trying to build an app that matches mocks/design guidelines set by a design team.

Consider the following scenario (a real life one I am experiencing at this moment).
I have been tasked with building a Flutter app to match a Figma mock designed according to a proprietary (but very simple) design system:

  1. Foreground content are contained in "boxes" (similar to Material's Card)
  2. Boxes have a brand_color_a -> brand_color_b vertical gradient fill.
  3. Boxes have a brand_color_c stroke with a width of some_int
  4. Boxes have a some_int border radius
  5. Boxes have a some_drop_shadow shadow
  6. Boxes pad their child content by some_edge_insets

There is no easy way for me to do this with Flutter's built ins today. Lets look at the options:
Container
+ flexible enough to match the design guidelines
- doesn't inherit from a theme so I'd have to pass the style arguments in everywhere I make a box
DecoratedBox
- same as above, but not as flexible
Card
+ themed
- isn't flexible enough to add custom drop shadows, gradients, etc
Mix (an external package)
+ flexible
+ themed
- not built in
- weird syntax designed to mimic CSS and thus very non-idiomatic in Flutter
Create my own custom Box widget and BoxTheme inherited widget
+ flexible
+ themed
- tedious

Proposal

Create:

  1. A ThemedContainer widget (name not that important, could be StyledContainer, Box, etc) with all the same arguments as Container
  2. A ContainerTheme inherited widget and a ContainerThemeData data class where the theme data contains all the same styling arguments as Container (padding, margin, decoration, color, etc)
  3. Add ContainerThemeData containerTheme to the core ThemeData class

Usage would look something like this:

// main.dart
return MaterialApp(
  theme: ThemeData(
    containerTheme: ContainerThemeData(
      padding: EdgeInsets.all(someInt),
      decoration: BoxDecoration(
        gradient: LinearGradient(colors: [brandColorA, brandColorB]),
        border: Border.all(width: someWidth, color: brandColorC),
        borderRadius: BorderRadius.circular(someInt),
        boxShadow: [someBoxShadow],
      ),
    ),
  ),
  home: MyApp(),
);

// my_feature.dart
return Column(
  children: [
    // Acts like a container but the padding, decoration, etc are all set according to the container
    // theme defined in main.
    ThemedContainer(child: SomeWidgetA()),
    // All theme values are inherited except padding which is overridden.
    ThemedContainer(child: SomeWidgetB(), padding: EdgeInsets.only(top: 25)),
  ],
);

Open questions:
What happens if the overridden values conflict with the theme values (eg color is overridden to pink, but the theme sets a box decoration and it is not valid to set both decoration and color)?
I see three okay-but-not-great options here:
A. Throw an assertion error
This is in-line with Container's behavior and fails loudly which is good, but can create a huge mess of confusing errors if you modify theme in a way that introduces conflicts
B. Ignore the entire conflicting set of attributes in the theme and apply the overridden attributes
This silently fails rather than loudly-for example when overriding color one would not expect shadows, etc to also be overridden
C. Remove the potentially conflicting values from ThemedContainer (eg the top level color)
The conflicting scenario now just isn't possible, but the class is now a little less convenient to use
I prefer A or C.

What happens when decoration is set at the widget and theme level and/or at different ContainerTheme widgets that have inherit set to true?
We have a pretty similar conflict potential as above. I'd suggest we attempt to merge each nested decoration and throw if there are conflicts. That feels like the most user-expected behavior, but has potential for confusing assertion errors, especially when editing the top level theme.

Should this even got in MaterialApp/Theme? It's explicitly NOT Material (Card is the material solution for this).
MaterialApp is in an awkward spot. It's ostensibly tied to Material, but over time it has become far more flexible than Material as developers demand more flexibility out of it. Ideally, Material and Flutter wouldn't have been so deeply intertwined in the first place and Flutter would offer it's own fully-fledged flexible theming system outside of Material. Maybe that ship hasn't sailed yet? You could put this in WidgetsApp in it's own theme and then have MaterialApp include a top level containerTheme argument that just plumbs to WidgetsApp, but I'm not sure the added misdirection actually gives us much here-do we want theoretical elegance or practical elegance?
All this said, it wouldn't be a big deal also to just not include the container theme in either place and make devs explicitly include a container theme widget in the widget tree.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3Issues that are less important to the Flutter projectc: new featureNothing broken; request for a new capabilityf: themingStyling widgets with a themeteam-designOwned by Design Languages teamtriaged-designTriaged by Design Languages team

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions