Best practice for overriding class fields in Dart

Viewed 995

I'm in the middle of creating a theming system for my app, where I wish to create a base theme class, with fields for colour and fonts:

abstract class BaseTheme {
  late Color backgroundColor;
  late Color appBarBackgroundColor;
  late Color progressIndicatorColor;

  late Color primaryColor;
  late Color iconColor;
}

(I have to define them as late to keep the Null Safety happy! These have to be overridden anyway, so no problems there).

I would then implement this base class for each theme, as below:

import 'base_theme.dart';

class LightTheme implements BaseTheme {
  Color backgroundColor = Colors.white;
  Color appBarBackgroundColor = Color(0xff1a1818);
  Color progressIndicatorColor = Color(0xffffc114);

  Color primaryColor = Colors.red;
  Color iconColor = Colors.white;
}

I would then use these values to populate the ThemeData:

BaseTheme _lightTheme = new LightTheme();

ThemeData _theme = ThemeData(
  scaffoldBackgroundColor: _lightTheme.backgroundColor,
  // etc...
}

Now, this appears to work fine. However, the page on overriding fields in the Linter for Dart states that overriding fields is bad practice. So, is there a better way to achieve something similar?

What's confusing is that, on that page, one example shows overriding fields when you extend a base class, which is considered bad. However, overriding fields when you implement a base class is good - But in this case, the field type has changed from Base to Derived (ie. they're not the same).

3 Answers

As the lint recommends, you should avoid overriding fields. Your abstract class should define an interface; as such, it should declare getters and setters and leave the implementation to the concrete, derived classes. Note that this avoids the need for late (which is an implementation detail that does not belong in the abstract base class).

abstract class BaseTheme {
  Color get backgroundColor;
  set backgroundColor(Color value);

  Color get appBarBackgroundColor;
  set appBarBackgroundColor(Color value);

  // etc.
}

Dart 2.13 allows members to be declared as abstract, so you can use that to avoid the repetitiveness:

abstract class BaseTheme {
  abstract Color backgroundColor;
  abstract Color appBarBackgroundColor;

  // etc.
}

Also see the GitHub issue: overridden_fields documentation is vague.

The reason it's not recommended as in the rule is because of memory allocation. When you override a field of an extended class you allocate more memory. For example:

class Base {
  Object field = 'lorem';

  Object something = 'change';
}

class Bad1 extends Base {
  @override
  final field = 'ipsum'; // LINT
}

In the example above when you create a new instance of Bad1 you will get field and super.field. So you allocated memory for two Object instances when you probably only needed one. You can read more about it here https://github.com/dart-lang/linter/issues/230 and here https://github.com/dart-lang/linter/issues/2428

You bring up a good point about the change in type in the docs in this example

abstract class BaseLoggingHandler {
  Base transformer;
}

class LogPrintHandler implements BaseLoggingHandler {
  @override
  Derived transformer; // OK
}

These docs are old so this is pre null safety. Now this is a compile type error because to narrow the type you need to be more explicit and use the covaraint keyword. Like so

abstract class BaseLoggingHandler {
  BaseLoggingHandler(this.transformer);
  Base transformer;
}

class LogPrintHandler implements BaseLoggingHandler {
  LogPrintHandler(this.transformer);
  @override
  covariant Derived transformer; // OK
}

Here is an example to illustrate the concept of a covaraint.

Given

abstract class Base {}

class Derived extends Base{}

the following

typedef Example = void Function(Derived);

Example e = (Base b) {};

This is valid because a Derived is a Base and if you were to give me a function (Base b) {} I can safely invoke your function passing an object of type Derived and I know operations you will take upon that object are safe.

But this is not valid for the opposite reason.

typedef Example = void Function(Base);

Example e = (Derived d) {};
Related