Skip to content

Instantly share code, notes, and snippets.

@loic-sharma
Last active November 19, 2025 03:11
Show Gist options
  • Select an option

  • Save loic-sharma/522f7f1c90321aeee94b43f2391c3f31 to your computer and use it in GitHub Desktop.

Select an option

Save loic-sharma/522f7f1c90321aeee94b43f2391c3f31 to your computer and use it in GitHub Desktop.
Style API
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