skip to content

In Dart, what does the covariant keyword on a parameter do, and why do Flutter overrides like CustomPainter.shouldRepaint rely on it?

level: middleimportance: nice to knowfreq 22%

answer

  1. tightening an override is normally illegal
  2. static error becomes a runtime check
  3. put it on the base declaration
  4. shouldRepaint and updateShouldNotify
  5. instance methods, setters and fields only

basics

~20 s

covariant 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 s

An 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 lines
dart
import '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

for a junior

Recognise shouldRepaint(MyPainter oldDelegate) as legal because Flutter's CustomPainter marks that parameter covariant.

for a middle

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.

for a senior

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.

for a principal

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