I'm new to theming in Flutter and I'm trying to figure it out the proper way of theming a Flutter App and my custom widgets the best way possible.
My goals are:
- Write the most of the app theming in the
themeproperty ofMaterialApp - For those widgets that the
colorSchemespecified on theMaterialAppis not valid write their specific theme, also inMaterialApp, e.g:buttonTheme - Custom Widgets should behave the same as the Framework Widgets so they should have an specific theme, e.g: a custom widget composed by a
ElevatedButtonand its children:CircularProgressIndicatorand aTextshould expose all of the individual components theme as parameters of its own theme and the component should also have its own theme properties, for example an space between the components. - Custom Widgets should also be able to be modified inline, meaning in its declaration, not only by the themes, but this should be avoided as much as possible. I'd rather have multiple themes declared and choose inline which one to use, having a default of course.
I want to know if this is the right approach or not, if there are better ways, less complicated ones and or less boilerplate and if something of my current implementation makes no sense like the copying of themes and Theme wrapping inside the custom widget...
The widget theme
class ProgressElevatedButtonThemeData
extends ThemeExtension<ProgressElevatedButtonThemeData> {
final ProgressIndicatorThemeData? progressIndicatorTheme;
final ElevatedButtonThemeData? elevatedButtonTheme;
final TextTheme? textTheme;
final double? spacing;
const ProgressElevatedButtonThemeData({
this.progressIndicatorTheme,
this.elevatedButtonTheme,
this.textTheme,
this.spacing,
});
@override
ThemeExtension<ProgressElevatedButtonThemeData> copyWith({
ProgressIndicatorThemeData? progressIndicatorTheme,
ElevatedButtonThemeData? elevatedButtonTheme,
TextTheme? textTheme,
double? spacing,
}) {
return ProgressElevatedButtonThemeData(
progressIndicatorTheme: progressIndicatorTheme,
elevatedButtonTheme: elevatedButtonTheme,
textTheme: textTheme,
spacing: spacing,
);
}
@override
ThemeExtension<ProgressElevatedButtonThemeData> lerp(
covariant ThemeExtension<ProgressElevatedButtonThemeData>? other,
double t) {
if (other is! ProgressElevatedButtonThemeData) {
return this;
}
return ProgressElevatedButtonThemeData(
progressIndicatorTheme: ProgressIndicatorThemeData.lerp(
progressIndicatorTheme, other.progressIndicatorTheme, t),
elevatedButtonTheme: ElevatedButtonThemeData.lerp(
elevatedButtonTheme, other.elevatedButtonTheme, t),
textTheme: TextTheme.lerp(textTheme, other.textTheme, t),
spacing: lerpDouble(spacing, other.spacing, t),
);
}
}
A simplified version of the Widget
class ProgressElevatedButton extends StatefulWidget {
final String text;
final double? spacing;
final ButtonStyle? buttonStyle;
final TextStyle? textStyle;
final VoidCallback? onPressed;
const ProgressElevatedButton({
Key? key,
required this.text,
required this.spacing,
required this.buttonStyle,
required this.textStyle,
this.onPressed,
}) : super(key: key);
@override
State<ProgressElevatedButton> createState() => _ProgressElevatedButtonState();
}
class _ProgressElevatedButtonState extends State<ProgressElevatedButton> {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final widgetTheme = theme.extension<ProgressElevatedButtonThemeData>();
final progressTheme = theme.copyWith(
progressIndicatorTheme: widgetTheme?.progressIndicatorTheme);
final buttonTheme =
theme.copyWith(elevatedButtonTheme: widgetTheme?.elevatedButtonTheme);
final textTheme = theme.copyWith(textTheme: widgetTheme?.textTheme);
final spacing = widget.spacing ?? widgetTheme?.spacing ?? 16;
return Theme(
data: buttonTheme,
child: ElevatedButton(
style: widget.buttonStyle,
onPressed: widget.onPressed,
child: Row(
children: [
Theme(
data: progressTheme,
child: const CircularProgressIndicator(),
),
SizedBox(width: spacing),
Theme(
data: textTheme,
child: Text(widget.text),
)
],
),
),
);
}
}
App with the theme applied
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
// All the Framework's Widget will be using these colors
final colorScheme = const ColorScheme.light().copyWith(
primary: Colors.redAccent,
// ... the rest of the colors of [ColorScheme] I need
);
// I've decided that the [primary] color from [colorScheme] is not the color
// I want for [ProgressIndicator] so I changed it only for those Widgets.
final progressIndicatorTheme = theme.progressIndicatorTheme.copyWith(
color: Colors.green,
);
// Extending the theme to be able to use themes for my custom Widgets
final extensions = <ThemeExtension<dynamic>>[
// My custom Widget theme (ElevatedButton(children:[Progress, Text]))
// I've decided that the [color] from [progressIndicatorTheme] is not the color
// I want for my custom [ProgressElevatedButton] so I changed it only for those Widgets.
ProgressElevatedButtonThemeData(
progressIndicatorTheme: progressIndicatorTheme.copyWith(
color: Colors.blue,
),
),
];
return MaterialApp(
title: 'Flutter Theming',
// The theme for my whole app (no dark theme to simplify it)
theme: ThemeData(
colorScheme: colorScheme,
progressIndicatorTheme: progressIndicatorTheme,
extensions: extensions,
),
home: const SizedBox.shrink(),
);
}
}
I would suggest you to make a
ThemeManagerProvide theme from
themesfile, where you specified your ownThemeModeInitialize
ThemeManagerinmainfileHere is a great YouTube video about it: link