Flutter theming and custom widgets

630 views Asked by At

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 theme property of MaterialApp
  • For those widgets that the colorScheme specified on the MaterialApp is not valid write their specific theme, also in MaterialApp, 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 ElevatedButton and its children: CircularProgressIndicator and a Text should 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(),
    );
  }
}
1

There are 1 answers

0
Nikita Shadkov On

I would suggest you to make a ThemeManager

class ThemeManager with ChangeNotifier{

  ThemeMode _themeMode = ThemeMode.light;

  get themeMode => _themeMode;

  toggleTheme(bool isDark){
    _themeMode = isDark?ThemeMode.dark:ThemeMode.light;
    notifyListeners();
  }

}

Provide theme from themes file, where you specified your own ThemeMode

const COLOR_PRIMARY = Colors.deepOrangeAccent;
const COLOR_ACCENT = Colors.orange;

ThemeData lightTheme = ThemeData(
  brightness: Brightness.light,
  primaryColor: COLOR_PRIMARY,
  elevatedButtonTheme: ElevatedButtonThemeData(
    style: ButtonStyle(
      padding: MaterialStateProperty.all<EdgeInsetsGeometry>(
        EdgeInsets.symmetric(horizontal: 40.0,vertical: 20.0)
      ),
      shape: MaterialStateProperty.all<OutlinedBorder>(
        RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(20.0))
      ),
      backgroundColor: MaterialStateProperty.all<Color>(COLOR_ACCENT)
    ),
  ),
);

ThemeData darkTheme = ThemeData(

    brightness: Brightness.dark,
    accentColor: Colors.white,
    elevatedButtonTheme: ElevatedButtonThemeData(
      style: ButtonStyle(
          padding: MaterialStateProperty.all<EdgeInsetsGeometry>(
              EdgeInsets.symmetric(horizontal: 40.0,vertical: 20.0)
          ),
          shape: MaterialStateProperty.all<OutlinedBorder>(
              RoundedRectangleBorder(
                  borderRadius: BorderRadius.circular(20.0)
              )
          ),
          backgroundColor: MaterialStateProperty.all<Color>(Colors.white),
          foregroundColor: MaterialStateProperty.all<Color>(Colors.black),
          overlayColor: MaterialStateProperty.all<Color>(Colors.black26)
      )
  ),
);

Initialize ThemeManager in main file

void main() {
  runApp(MyApp());
}

ThemeManager _themeManager = ThemeManager();

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {

  @override
  void dispose() {
    _themeManager.removeListener(themeListener);
    super.dispose();
  }

  @override
  void initState() {
    _themeManager.addListener(themeListener);
    super.initState();
  }

  themeListener(){
    if(mounted){
      setState(() {

      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter Demo',
      theme: lightTheme,
      darkTheme: darkTheme,
      themeMode: _themeManager.themeMode,
      home: MyHomeScreen(),
    );
  }
}

Here is a great YouTube video about it: link