skip to content

In Dart, when should a function be top-level, static or an instance method, and why does Effective Dart discourage static-only classes?

level: middleimportance: should knowfreq 32%

answer

  1. Dart does not require a class
  2. main is where it all starts
  3. a library is the namespace
  4. import prefixes, show and hide
  5. needs state, or makes an instance

basics

~20 s

Dart functions can be top-level, static or instance methods: instance for behaviour that needs an object's state, static for type-tied factories and parsers, and top-level for standalone helpers and main. Effective Dart avoids static-only classes because libraries already provide namespaces.

solid answer

~40 s

Dart lets functions live at the top level of a library, which is where `main(List<String> args)` must be; `args` holds only the command-line arguments after the script. I make something an **instance method** when it needs the object's state or should be overridable, a **static method** when it is the type's own way to create or look something up, like `int.parse`, and a **top-level function** for standalone helpers. Effective Dart says to avoid classes with only static members: the class is never instantiated, and a library is a better namespace because importers can use `as` prefixes or `show`/`hide` to resolve clashes themselves. Groups of related constants are the accepted exception.

code

dart · 24 lines
dart
// bin/format.dart
String formatPrice(num amount, {int decimals = 2}) =>
    '${_round(amount, decimals)} EUR'; // top-level, public

String _round(num value, int decimals) =>
    value.toStringAsFixed(decimals); // top-level, library-private

class Money {
  Money(this.cents);
  final int cents;

  static Money? tryParse(String input) { // static: makes a Money
    final value = double.tryParse(input);
    return value == null ? null : Money((value * 100).round());
  }

  String format() => formatPrice(cents / 100); // instance: uses cents
}

void main(List<String> args) {
  print(args); // e.g. [9.99] for: dart run bin/format.dart 9.99
  final money = Money.tryParse(args.isEmpty ? '0' : args.first);
  print(money?.format());
}

go deeper

for a junior

Recall that Dart supports top-level functions, that main is one, and that main can take a List<String> of command-line arguments.

for a middle

Explain the difference between top-level, static and instance functions and the library-as-namespace argument against static-only classes.

for a senior

Choose placement for testability and evolution: overridable instance methods where behaviour varies, top-level helpers where it does not.

for a principal

Shape package structure so libraries, not utility classes, group related functions, with import prefixes handling collisions across teams.

## Three places a function can live Dart does not force every function into a class. A function can be: | Kind | Declared | Called as | Can use `this` | |---|---|---|---| | **Top-level** | directly in a library file | `formatPrice(9.99)` | no | | **Static method** | `static` inside a class | `Money.parse('9.99')` | no | | **Instance method** | inside a class, no `static` | `price.format()` | yes | - A **top-level function** belongs to its library. A leading underscore, `_round()`, makes it private to that library. - A **static method** belongs to the class's namespace but has no instance; it cannot touch instance fields. - An **instance method** operates on one object's state and can be overridden by subclasses. ## `main` is the required top-level function Every Dart program starts at a **top-level `main()`** function. The dart.dev guide describes it as returning `void` with an optional `List<String>` parameter for arguments: ```dart void main(List<String> arguments) { print(arguments); } ``` Running `dart run args.dart 1 test` gives `arguments == ['1', 'test']`: only the arguments after the script, as strings. A Flutter app's `main` typically takes no arguments and calls `runApp`: `void main() => runApp(const App());`. For real command-line parsing, the `args` package builds on this list. ## Why Effective Dart discourages classes of only static members Developers coming from languages where everything must live in a class often write `class StringUtils { static String pad(...) ... }`. Effective Dart says to **avoid defining a class that contains only static members**: - Dart has top-level functions, variables and constants, so the class adds nothing. - If you want a namespace, a **library** is the better tool: importers can add a prefix (`import 'format.dart' as fmt;`) or use `show` and `hide` to resolve collisions their own way. - A type that is never instantiated is a code smell; in idiomatic Dart, classes describe kinds of objects. The guide allows exceptions for groups of constants and enum-like types, and the `avoid_classes_with_only_static_members` lint (not in the default sets) can enforce the rule. ## When static is still right 1. **Factories and parsers tied to a type**: `int.parse`, `DateTime.now`, `Money.tryParse(...)` read naturally as "the class's way of making one". 2. **Constants and helpers that belong to a type's concept** alongside instance members: `Duration.zero`, `Colors`-style palettes. 3. **Private helpers used only by one class**, where a static method keeps them next to their only caller. ## When instance methods are right When the behaviour needs the object's state or should be overridable. A `PriceFormatter` configured with a currency and precision exposes `format(amount)` as an instance method, because each formatter carries its own settings. ## A decision checklist - Does the function need an object's state, or should subclasses override it? **Instance method.** - Is it a way to create or look up an instance of one type? **Static method** (or a factory constructor). - Is it a standalone helper or a program entry point? **Top-level function**, in a library whose name gives it context. ## What an interviewer listens for - That Dart supports top-level functions, and `main` is one. - What `main(List<String> args)` receives. - Effective Dart's stance on static-only classes and the library-as-namespace alternative. - A reason, not a habit, for each choice.

  • What exactly does main(List<String> args) receive when you run dart run args.dart 1 test?
    A `List<String>` with the arguments after the script: `['1', 'test']`. Values arrive as strings, so numbers must be parsed with `int.parse` or similar. The parameter is optional; `void main()` is valid when the program takes no arguments, which is the usual shape of a Flutter app's `main`.
  • How do you resolve a name clash between two Dart libraries that both export a top-level format function?
    Import one or both with a prefix, `import 'money.dart' as money;`, and call `money.format(...)`, or narrow imports with `show` and `hide`. That is why Effective Dart prefers libraries over static-only classes as namespaces: the importer decides how to handle collisions.

saying these in an interview costs you the question

  • Thinks every Dart function must live in a class
  • Wraps helpers in a Utils class of static methods by habit
  • Believes main receives the script name as the first argument
  • Says static methods can read instance fields
  • Makes state-dependent behaviour static and passes the object in