Cleanly overriding parts of a theme locally in Flutter

Viewed 7951

I have a widget which has two TextFields as descendants. I would like to apply the same styling to these TextFields. My understanding is that the right way to do this is to apply a localized theme to my widget tree. The following is my attempt. This is a code snippet from my root widget's build function. Is there not a cleaner way to do this?

final ThemeData _themeData = Theme.of(context);
return Theme( // HACK
  data: _themeData.copyWith(
    inputDecorationTheme: InputDecorationTheme(
      border: OutlineInputBorder(),
    ),
    textTheme: _themeData.textTheme.copyWith(
      subhead: _themeData.textTheme.subhead.copyWith(
        fontSize: 30.0,
      ),
    ),
  ),
  child: _buildTheRestOfMyWidgetTree(context),
);

The thing that I am annoyed by is that to override a single property (_themeData.textTheme.subhead.fontSize), I have to explicitly and manually make copies of three intermediate data structures (_themeData, then _themeData.textTheme, then _themeData.textTheme.subhead).

2 Answers

While I can understand the frustration of having to "copy" everything, this is how you should do it.

Data are immutable in Flutter. You cannot mutate them, you are forced to clone them with different properties.

Therefore your assumption is correct: If you want to modify a nested property, you have to clone all of its parents too. Which leads to:

final ThemeData theme = Theme.of(context);
theme.copyWith(
  textTheme: theme.textTheme.copyWith(
    subhead: theme.textTheme.subhead.copyWith(
      fontSize: 30.0,
    ),
  ),
);

Again: you cannot avoid it.

It would help if you packaged that part of the code up and made it a widget so that your tree is cleaner. That's how it's done in this example.

class TextFieldOverride extends StatelessWidget {
  const TextFieldOverride({this.child});
  final Widget child;
  @override
  Widget build(BuildContext context) {
    final themeData = Theme.of(context);
    return Theme(
      child: child,
      data: themeData.copyWith(
        inputDecorationTheme: InputDecorationTheme(
          border: OutlineInputBorder()),
      textTheme: themeData.textTheme.copyWith(
        subhead: themeData.textTheme.subhead.copyWith(
          fontSize: 30.0))));
  }
}

...

TextFieldOverride(
  child: TextField(...)
)

Or if there are few places the code will be duplicated, you can just make the changes directly:

...
child: TextField(
  style: Theme.of(context).textTheme.subhead.copyWith(fontSize: 30.0),
  decoration: InputDecoration(border: OutlineInputBorder(),
    ...
  )
)

Or perhaps the best choice is to create a function that does the above for you.

TextField buildTextField(BuildContext context) => TextField(
  style: Theme.of(context).textTheme.subhead.copyWith(fontSize: 30.0),
  decoration: InputDecoration(border: OutlineInputBorder(),
    ...
  )
)
Related