In Dart, what does the covariant keyword on a parameter do, and why do Flutter overrides like CustomPainter.shouldRepaint rely on it?
answer
- tightening an override is normally illegal
- static error becomes a runtime check
- put it on the base declaration
- shouldRepaint and updateShouldNotify
- instance methods, setters and fields only
basics
~20 scovariant lets an override narrow a parameter to a subtype, which is otherwise an invalid override. The analyzer accepts it and Dart checks the argument at run time instead, so Flutter subclasses can write shouldRepaint(MyPainter oldDelegate).
solid answer
~40 sAn override may normally widen a parameter type but not narrow it, because a caller holding the supertype could pass any argument the base type allows. Marking the parameter `covariant` tells the analyzer the narrowing is intentional: the static error goes away and Dart inserts a runtime check that throws a `TypeError` if a wrong argument arrives. The keyword can sit on the base declaration or the override; on the base it covers every subclass without repeating it. Flutter does exactly that: `CustomPainter` declares `bool shouldRepaint(covariant CustomPainter oldDelegate)`, and `InheritedWidget` declares `updateShouldNotify(covariant InheritedWidget oldWidget)`, so your subclass writes `shouldRepaint(RingPainter oldDelegate)` and reads its own fields without a cast. It is allowed on instance method and operator parameters, setters and mutable instance fields.
code
dart · 18 linesimport 'package:flutter/material.dart';
class RingPainter extends CustomPainter {
RingPainter(this.progress);
final double progress;
@override
void paint(Canvas canvas, Size size) {
// draw an arc proportional to progress
}
// CustomPainter declares this parameter covariant, so the override may
// narrow it to RingPainter without repeating the keyword or casting.
@override
bool shouldRepaint(RingPainter oldDelegate) =>
oldDelegate.progress != progress;
}go deeper
Recognise shouldRepaint(MyPainter oldDelegate) as legal because Flutter's CustomPainter marks that parameter covariant.
Explain that narrowing an override's parameter is normally an error, and that covariant trades that static error for a runtime TypeError check on the argument.
Know where the keyword is allowed, why the base declaration is the right place, and when a narrowed parameter signals a hierarchy that should be generic instead.
Treat covariant as a documented contract in shared base classes and decide when its runtime risk is acceptable versus a self-bounded generic design.
## The rule that `covariant` bends When a subclass **overrides** a method, Dart requires the override to be usable everywhere the original is. For a parameter, that means the override may accept the **same type or a supertype**, never a narrower subtype: ```dart class Animal { void chase(Animal a) {} } class Mouse extends Animal {} class Cat extends Animal { @override void chase(Mouse a) {} // invalid override: narrows Animal to Mouse } ``` The reason is substitution: code holding `Animal pet = Cat();` may call `pet.chase(Alligator())`, and a `Cat` that only handles mice would receive something it cannot handle. ## What the keyword changes Some designs genuinely want the narrow parameter, most often when an object compares itself with **another instance of the same subclass**. Marking the parameter **`covariant`** says "I know, and I accept the risk": 1. The **static error disappears** — the override compiles. 2. Dart adds a **runtime check** on that parameter. A call through the supertype with an argument that is not the narrowed type throws a **`TypeError`** before the body runs. 3. Calls through the subclass's own static type are checked at compile time as usual. The keyword can be written on the **base** declaration or on the **override**. Written on the base, it makes that parameter covariant in every override, so subclasses narrow it without repeating the word — the Dart documentation recommends the superclass as the usual place. ## Where Flutter uses it Flutter puts `covariant` on base declarations whose subclasses always compare themselves with an older copy of the same class: | Base declaration in Flutter | What your subclass writes | |---|---| | `bool shouldRepaint(covariant CustomPainter oldDelegate)` | `bool shouldRepaint(RingPainter oldDelegate)` | | `bool updateShouldNotify(covariant InheritedWidget oldWidget)` | `bool updateShouldNotify(ThemeScope oldWidget)` | | `void didUpdateWidget(covariant T oldWidget)` on `State<T>` | `void didUpdateWidget(Counter oldWidget)` | The framework only ever passes the previous instance of the same class, so the runtime check never fires in normal use, and your code reads `oldDelegate.progress` directly instead of casting with `as`. ## Where it is allowed The modifier applies to **one parameter at a time** and only to members that can be overridden: - parameters of **instance methods** and **operators**; - the parameter of an **instance setter**; - a **mutable instance field**, which makes its implicit setter's parameter covariant; - since Dart 3.13, a **primary constructor** parameter only when it is a mutable declaring parameter written with `var`. Top-level functions and static methods cannot use it, because nothing overrides them. It is also not a variance annotation on a type parameter: `class Box<covariant T>` is not Dart. ## Relation to covariant generics The same idea already runs inside every generic class. `List<E>.add(E value)` behaves as if its parameter were covariant: seen as a `List<Animal>`, a `List<Cat>` checks each added value at run time. The keyword lets you opt into that behaviour for an ordinary, non-generic parameter. ## When to use it - **Use it** for "compare with another of my own kind" members — equality-like checks, `shouldRepaint`-style hooks, `copyFrom(covariant Shape other)` patterns. - **Avoid it** as a shortcut for a hierarchy that is simply wrong. If callers routinely pass other subtypes, the check will fire in production; a generic base class, such as `abstract class Shape<S extends Shape<S>>`, or a separate method is the better design. - **Document it**: a covariant parameter is a promise that callers going through the base type will pass the right subtype. ## Reading one in code review When a narrowed parameter shows up in a diff, three questions settle it: 1. **Who calls it through the base type?** For `shouldRepaint`, only the framework, and only with the previous painter of the same class, so the check cannot fire. For a method your own code calls on a `List<Shape>`, it can. 2. **Is the narrowing on the base or on one override?** A `covariant` written only on one subclass is easy to miss; on the base declaration it is part of the contract every implementer reads. 3. **Would a failing call be a bug or an expected case?** If mixed arguments are legitimate, the method should accept the base type and branch with a type test or a `switch` on a sealed hierarchy instead of throwing a `TypeError`.
- What does `covariant` on a mutable instance field do?A mutable field has an implicit setter, and `covariant` makes that setter's parameter covariant. A subclass can then override the field or its setter with a narrower type, and an assignment through the base type with a wrong value throws a `TypeError` at run time instead of being rejected statically.
- Why not just keep `shouldRepaint(CustomPainter oldDelegate)` and cast with `as RingPainter` inside?It works and fails the same way on a wrong argument, but it hides the contract in the body and repeats a cast in every subclass. The covariant parameter states the expectation in the signature, lets the analyzer type `oldDelegate` as `RingPainter` everywhere in the method, and puts the check at the method boundary.
saying these in an interview costs you the question
- Thinks covariant turns off type checking for the parameter
- Believes every override must repeat covariant to narrow the type
- Uses covariant on a top-level function or a static method
- Confuses covariant with a variance modifier on a class type parameter
- Says overrides may always narrow parameter types in Dart