Last active
November 19, 2025 03:11
-
-
Save loic-sharma/522f7f1c90321aeee94b43f2391c3f31 to your computer and use it in GitHub Desktop.
Style API
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import 'package:flutter/material.dart'; | |
| void main() => runApp(const MyWidget()); | |
| class MyWidget extends StatelessWidget { | |
| const MyWidget({super.key}); | |
| @override | |
| Widget build(BuildContext context) { | |
| return const Column( | |
| children: <Widget>[ | |
| // Example usage of the Styled widget with hover styles. | |
| // | |
| // Notice that unlike decorators: | |
| // | |
| // 1. Styles are applied first to last | |
| // 2. Styles are _before_ the widget they style | |
| // 3. Styles can passed down to a child widget to be applied on a subtree | |
| // 4. Styles can be applied conditionally if a condition is met, | |
| // like if a widget is hovered, or if the screen is a certain size. | |
| // | |
| // However: | |
| // 1. This requires static extensions that support const factory constructors! | |
| // 2. While styles can be const, the widgets they create cannot. Unlike decorators, | |
| // I don't see a path to const-ability. | |
| Styled( | |
| style: Styles([ | |
| .backgroundColor(Colors.blue), | |
| .padding(.all(16.0)), | |
| .textStyle(color: Colors.green, fontSize: 24), | |
| .onHover(.backgroundColor(Colors.blue)), | |
| .onSmall(.textStyle(fontSize: 12)), | |
| ]), | |
| child: Text('Hover over me!'), | |
| ), | |
| // A design system can use the style API to let | |
| // callers customize widgets. | |
| HypotheticalButton( | |
| label: 'Press Me', | |
| labelStyle: Styles([ | |
| .textStyle(fontSize: 24, color: Colors.white), | |
| .onHover(Styles([.textStyle(color: Colors.yellow)])), | |
| ]), | |
| ), | |
| ], | |
| ); | |
| } | |
| } | |
| class HypotheticalButton extends StatelessWidget { | |
| const HypotheticalButton({super.key, required this.label, required this.labelStyle}); | |
| final String label; | |
| final Style labelStyle; | |
| @override | |
| Widget build(BuildContext context) { | |
| return Styled( | |
| style: labelStyle, | |
| child: Text(label), | |
| ); | |
| } | |
| } | |
| class Styled extends StatelessWidget { | |
| const Styled({super.key, required this.style, this.child}); | |
| final Style style; | |
| final Widget? child; | |
| @override | |
| Widget build(BuildContext context) { | |
| return style.build(context, child ?? const SizedBox.shrink()); | |
| } | |
| } | |
| abstract class Style { | |
| const Style(); | |
| Widget build(BuildContext context, Widget child); | |
| const factory Style.none() = NoneStyle; | |
| const factory Style.list(List<Style> styles) = Styles; | |
| const factory Style.backgroundColor(Color color) = BackgroundColorStyle; | |
| const factory Style.padding(EdgeInsetsGeometry insets) = PaddingStyle; | |
| const factory Style.textStyle({Color? color, double? fontSize}) = TextStyleStyle; // Beautiful :') | |
| const factory Style.onHover(Style style) = HoverStyle; | |
| const factory Style.onSmall(Style style) = SmallScreenStyle; | |
| } | |
| class NoneStyle extends Style { | |
| const NoneStyle(); | |
| @override | |
| Widget build(BuildContext context, Widget child) { | |
| return child; | |
| } | |
| } | |
| class Styles extends Style { | |
| const Styles(this.styles); | |
| final List<Style> styles; | |
| @override | |
| Widget build(BuildContext context, Widget child) { | |
| var result = child; | |
| for (final style in styles.reversed) { | |
| result = style.build(context, result); | |
| } | |
| return result; | |
| } | |
| } | |
| class BackgroundColorStyle extends Style { | |
| const BackgroundColorStyle(this.color); | |
| final Color color; | |
| @override | |
| Widget build(BuildContext context, Widget child) { | |
| return ColoredBox( | |
| color: color, | |
| child: child, | |
| ); | |
| } | |
| } | |
| class PaddingStyle extends Style { | |
| const PaddingStyle(this.insets); | |
| final EdgeInsetsGeometry insets; | |
| @override | |
| Widget build(BuildContext context, Widget child) { | |
| return Padding( | |
| padding: insets, | |
| child: child, | |
| ); | |
| } | |
| } | |
| class TextStyleStyle extends Style { | |
| const TextStyleStyle({this.color, this.fontSize}); | |
| final Color? color; | |
| final double? fontSize; | |
| @override | |
| Widget build(BuildContext context, Widget child) { | |
| return DefaultTextStyle( | |
| style: TextStyle(color: color, fontSize: fontSize), | |
| child: child, | |
| ); | |
| } | |
| } | |
| class HoverStyle extends Style { | |
| const HoverStyle(this.style); | |
| final Style style; | |
| @override | |
| Widget build(BuildContext context, Widget child) { | |
| // TODO: This API doesn't exist. The idea is this looks up the hover state | |
| // of the current "control" from the context. | |
| bool isHovered = WidgetStateContext.isHoveredOf(context); | |
| return Styled( | |
| style: isHovered ? style : Style.none(), | |
| child: child, | |
| ); | |
| } | |
| } | |
| class SmallScreenStyle extends Style { | |
| const SmallScreenStyle(this.style); | |
| final Style style; | |
| @override | |
| Widget build(BuildContext context, Widget child) { | |
| final isSmall = MediaQuery.of(context).size.width < 600; | |
| return Styled( | |
| style: isSmall ? style : Style.none(), | |
| child: child, | |
| ); | |
| } | |
| } | |
| class WidgetStateContext { | |
| static bool isHoveredOf(BuildContext context) => false; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment