Is there a "construct this type with const" meta annotation?

Sometimes, constructing a class as const has a different meaning than constructing it with new. For example, String.fromEnvironment() says:

This constructor is only guaranteed to work when invoked as const. It may work as a non-constant invocation on some platforms which have access to compiler options at run-time, but most ahead-of-time compiled platforms will not have this information.

That means that if you forget to write the const in const String.fromEnvironment(), you won’t get the dart-define=... compiler flag values at runtime. And there is no warning. You just get an empty string.

But this goes beyond fromEnvironment(). You can have a class that’s supposed to have only one const instance, such as:

/// Represents an action that does nothing.
class NoOpAction extends Action {
  const NoOpAction();

  void execute() {
    // Pass.
  }
}

And you might want to check if an action is no-op, so you want to write code like:

if (action == const NoOpAction()) ...

But that will only work if both sides of the equation are const. So you want to force the const.

I though there was a pkg:meta annotation for this but I can’t find this now.

  @mustInstantiateAsConst   // or something like that ?
  const NoOpAction();

I think that the traditional way of dealing with this would be something like:

class NoOpAction extends Action {
  static const instance = NoOpAction._();

  const NoOpAction._();

  void execute() {
    // Pass.
  }
}

But I’d like to avoid this because it forces the user to think (“wait, does this subclass have an .instance static field or should I use a constructor?”) and also code completion becomes unhelpful when you have something like this (you start writing the class name but instead of the option to import the appropriate file, you get nothing — until you start typing the .instance part).

(Also, this is different and separate from the prefer_const_constructors lint. This is about selectively marking a constructor. The prefer_const_constructors lint marks every such constructor use, which is not what I want — that lint is annoying and has dubious value, but that’s a separate discussion.)

1 Like

Not sure what would the practical application of your last example

Which do you mean?

Your NoOp example. I try to understand when you would need something like that

Oh, this is actually pretty close to what you’d have in game AI logic. You want some kind of default “idle” behavior. (Of course the behavior has more than just execute() in practice…) You could use null, but sometimes you have more than one default, and often it’s more practical to have the default behavior defined in one place instead of checking for null everywhere and substituting some default all over the codebase.

1 Like

That’s the purpose of prefer_const_constructors (and only once I needed to add an explicit avoid to that rule). It’s a pretty solid lint, IMO.

The String.fromEnvironment(), for me, it’s a bad behaviour. One should not have to read the official docs in order to use something (often people won’t). This leads to bugs impossible to find. IMO, you must do what you said you would do, without conditionals (or those conditionals have to be very explicit).

Your nop operation should be an static const inside the base class (that seems right to me, because I learn that way from other languages I used before - meaning: it’s not “right” per-se, but it is a common pattern used in many other languages, “it just makes sense” ™).

So:

class _NoOpAction extends Action {
  const _NoOpAction();
 
  void execute() {
    // Pass.
  }
}

class Action {
  static const noOp = _NoOpAction();
}

...

final emptyOp = Action.noOp;

First time I saw Dart, I found the “official” singleton pattern disturbing (it uses a factory that is the same as the constructor - so no difference at all for the user - you could not know if a class was an instantiation or a singleton). Luckily, most devs out there use the default .instance property to make it explicit.

For me, const classes are a Dart feature made for optimization (one that is not always the right choice, for instance, const [] do not allow .add or when you are expliciting creating two different instances for the same immutable class (I have a linter ignore for that in some tests to make possible value-equality check without the interference of const)).

EDIT: reading the String.fromEnvironment docs: it’s not even about environment variables at all! They are compiler options! (another bad behavior in naming, since the word “environment” universally means “system environment variables” in this context). IMO, those compiler options should generate a .dart class that is compiled along our code, so we could access those are const variables (ex.: if there is a flavourColor=aabbcc option, something like dartCompilerOptions.flavourColor should return "aabbcc")

@filip Hey!

tl;dr: No, but you can probably make do with what exists today. For example:

// Does not have to be sealed, but could be.
sealed class Action {
  static const Action noop = NoopAction();
}

final class NoopAction implements Action {
  // Can't be constructed directly.
  const NoopAction._();
}

The nice thing is this will still work with pattern matching, i.e.:

switch (action) {
  case NoopAction():
    print('Filip said do nothing, so we are doing nothing');
}

If you had a constructor you want to be invoked manually (i.e. it has parameters), but only const, you could either or both:

  • tag the constructor @literal, which requires the use of const unless it has non-const parameters.
  • tag the parameters @mustBeConst.

So, for example:

import 'package:meta/meta.dart';

// User can create custom instances, but not during runtime.
final class Currency {
  @literal
  const Currency(@mustBeConst this.name);
  final String name;
}
2 Likes

Dart currently does not provide a way to enforce that a constructor must always be used with const, as there is no @mustInstantiateAsConst annotation. This can lead to unexpected behavior, such as when const is omitted from String.fromEnvironment(), causing compiler flags to be ignored. A common workaround is to use a static const instance, requiring users to write NoOpAction.instance instead of const NoOpAction(). While this ensures a single instance, it can be unintuitive. Another approach is a factory constructor that always returns the same instance, but this prevents the use of const. A long-term solution could be implementing a custom linter rule to enforce const instantiation where needed. Since Dart treats const primarily as an optimization rather than a strict requirement, there is no built-in solution yet, but a feature request for package:meta or a custom linter rule could help address this issue.

Thanks, Matan!

This turns out to be the exact notation I was looking for! I thought I saw something like that in the past but I couldn’t remember the name and couldn’t find it.

As for the sealed class — and that’s probably another discussion — I feel torn about using it because you have to be exhaustive on a library (i.e. file + part files) level. For something small that’s no problem, but in a game AI system, for example, you have dozens of behaviors. It becomes unwieldy pretty quick.

2 Likes

A random question for you @filip how did you achieve that text glitch effect in Knights of San Francisco?

Where do I start.

Hey @tfozo, this really is off topic but here are a few pointers:

  • The RichText widget allows you to have widgets inline. For example, you can have a paragraph of text with a button in the middle of it, and the button will be placed as if it was another word in the paragraph (if it’s small enough).
  • You can have widgets for every single word in the paragraph.
  • Those widgets can be animated any way you like.

Hope this helps!

1 Like

Thank you so much, it’s a good start. Thank you again. WOW I couldn’t have thought about that lol

1 Like

As usual, the devil is in the details. But if you’re stubborn enough, you’ll build something amazing. Have fun!

1 Like