Skip to content

Top 30 Dart Coding Challenges with Solutions [2026]

October 8, 2026 7 min read
Dart interview guide by Second Talent
TL;DR: Dart coding rounds in 2026 test collections, null safety, records and patterns, sealed classes, async code and isolates. Every solution below was run on Dart 3.13.4, the stable release of September 15, 2026, and five of them use syntax added since Dart 3.6.

Dart 3.13, released on August 12, 2026, added primary constructors: class Point(var int x, var int y); now declares both fields and the constructor in one line.

It follows null-aware elements in Dart 3.8, dot shorthands in 3.10 and private named parameters in 3.12. Candidates who learned Dart through Flutter tutorials from 2023 write correct code that looks dated, and a good interviewer notices.

Key takeaways
  1. 1The Dart team stopped work on macros on January 29, 2025, so JSON and equality code is still generated with build_runner.
  2. 2Names on positional record fields are documentation only: (int min, int max) has no .min getter. Named fields do.
  3. 3A .then on an already completed future still runs later, as a microtask, never inline with the code that registered it.
  4. 4Dart 3.13 made final on an ordinary parameter a compile error, because that syntax now declares a field in a primary constructor.

Strings and Collections

The first eight are warm-ups that still separate candidates. Look for collection literals, for elements and the right built-in type instead of index loops copied from another language.

1. Reverse a string without breaking emoji.

Task: reverse('a😀b') returns 'b😀a'. Reverse the runes (Unicode code points), not the UTF-16 code units.

String reverse(String s) => String.fromCharCodes(s.runes.toList().reversed);

The common answer, s.split('').reversed.join(), splits a Dart string into UTF-16 code units, so it tears the emoji's surrogate pair in half and returns an invalid string.

We checked: the naive version fails this test and the runes version passes.

Runes still split a flag or an accented letter built from two code points; for user-visible characters the answer is the characters package, which works on grapheme clusters.

2. Check whether a sentence is a palindrome.

Task: ignore case, spaces and punctuation. 'A man, a plan, a canal: Panama' returns true.

bool isPalindrome(String s) {
  final cleaned = s.toLowerCase().replaceAll(RegExp(r'[^a-z0-9]'), '');
  for (var i = 0, j = cleaned.length - 1; i < j; i++, j--) {
    if (cleaned[i] != cleaned[j]) return false;
  }
  return true;
}

Two indexes walking inward compare the cleaned string without building a reversed copy. Comparing with cleaned.split('').reversed.join() is also correct here, because the regex leaves only ASCII letters and digits.

That same regex is the follow-up question: it deletes accented letters, so 'Été' is cleaned to 't'. Ask how the candidate would handle non-English input.

3. FizzBuzz, returned as a list, with a switch expression.

Task: for 1 to n, return "Fizz" for multiples of 3, "Buzz" for multiples of 5 and "FizzBuzz" for both.

List<String> fizzBuzz(int n) => [
  for (var i = 1; i <= n; i++)
    switch ((i % 3, i % 5)) {
      (0, 0) => 'FizzBuzz',
      (0, _) => 'Fizz',
      (_, 0) => 'Buzz',
      _ => '$i',
    },
];

Switching on the record (i % 3, i % 5) removes the classic bug of testing 3 before 15, because the (0, 0) case is matched first. The for element inside the list literal builds the result without a mutable list.

Both features date from Dart 3.0 (records and patterns) and Dart 2.3 (collection for), so a candidate on any current SDK should know them.

4. Return the k most frequent words in a text.

Task: 'the cat and the hat and the bat' with k = 3 returns the: 3, and: 2, bat: 1. Break ties alphabetically.

List<MapEntry<String, int>> topWords(String text, int k) {
  final counts = <String, int>{};
  for (final m in RegExp(r"[a-z']+").allMatches(text.toLowerCase())) {
    counts.update(m[0]!, (c) => c + 1, ifAbsent: () => 1);
  }
  final entries = counts.entries.toList()
    ..sort(
      (a, b) => b.value != a.value
          ? b.value.compareTo(a.value)
          : a.key.compareTo(b.key),
    );
  return entries.take(k).toList();
}

Map.update with ifAbsent is the idiomatic counter. Sorting all entries costs O(n log n) for n distinct words; for a huge vocabulary and small k, a follow-up is a bounded heap from package:collection's PriorityQueue.

A deterministic tie-break is worth asking for: without it, tests that pass on one run can fail on another input order.

5. Two-sum: return the indexes of two numbers that add up to a target.

Task: twoSum([2, 7, 11, 15], 9) returns (0, 1); return null when no pair exists.

(int, int)? twoSum(List<int> nums, int target) {
  final seen = <int, int>{}; // value -> index
  for (final (i, n) in nums.indexed) {
    final j = seen[target - n];
    if (j != null) return (j, i);
    seen[n] = i;
  }
  return null;
}

One pass with a map from value to index gives O(n) time.

The return type (int, int)? is a nullable record, which is cleaner than a two-element list or a sentinel like [-1, -1]. nums.indexed, added in Dart 3.0, yields (index, value) records that the for loop destructures in place.

6. Group words that are anagrams of each other.

Task: ['eat', 'tea', 'tan', 'ate', 'nat', 'bat'] returns three groups: eat/tea/ate, tan/nat and bat.

Map<String, List<String>> groupAnagrams(List<String> words) {
  final groups = <String, List<String>>{};
  for (final w in words) {
    final key = String.fromCharCodes(w.codeUnits.toList()..sort());
    groups.putIfAbsent(key, () => []).add(w);
  }
  return groups;
}

The sorted letters are the group key; putIfAbsent creates each list once. Dart's default Map is a LinkedHashMap, so groups come back in the order their first word appeared, and the test above relies on that.

Sorting each word costs O(k log k); a 26-slot letter count as the key is the O(k) follow-up for lowercase ASCII input.

7. Remove duplicates from a list and keep the original order.

Task: [3, 1, 3, 2, 1] returns [3, 1, 2].

List<T> dedupe<T>(Iterable<T> items) => {...items}.toList();

A set literal is a LinkedHashSet, which keeps insertion order, so spreading the input into one and back into a list is the whole solution.

A strong candidate adds the caveat: for custom classes this only works if the class overrides == and hashCode, which is question 15.

8. Check whether brackets are balanced.

Task: '{[()()]}' is balanced; '([)]' and '((' are not.

bool isBalanced(String s) {
  const pairs = {')': '(', ']': '[', '}': '{'};
  final stack = <String>[];
  for (final ch in s.split('')) {
    if (pairs.containsValue(ch)) {
      stack.add(ch);
    } else if (pairs.containsKey(ch)) {
      if (stack.isEmpty || stack.removeLast() != pairs[ch]) return false;
    }
  }
  return stack.isEmpty;
}

A List works as a stack through add and removeLast, both amortized O(1). The two failure paths are a closing bracket with nothing open and leftovers at the end; candidates often miss the second.

Types, Records, Patterns

These test what Dart 3 changed: sound null safety everywhere, records, pattern matching and sealed class hierarchies. They are also where Flutter-only developers tend to show gaps.

9. Sum the valid integers in a list of strings.

Task: ['4', 'x', '10', '', '-2'] returns 12. Ignore anything that does not parse.

int sumValid(List<String> raw) =>
    raw.map(int.tryParse).nonNulls.fold(0, (a, b) => a + b);

int.tryParse returns int?, and nonNulls (Dart 3.0) filters out the nulls and narrows the type to Iterable<int> in one step.

Watch for int.parse inside a try block, which works but uses exceptions for control flow, and for a trailing !, which throws on the first bad value.

10. Return the minimum and maximum of a list in one pass.

Task: minMax([3, -1, 9, 4]) returns (min: -1, max: 9); the caller can read .min and .max.

({int min, int max}) minMax(List<int> xs) {
  if (xs.isEmpty) throw ArgumentError('empty list');
  var (lo, hi) = (xs.first, xs.first);
  for (final x in xs.skip(1)) {
    if (x < lo) lo = x;
    if (x > hi) hi = x;
  }
  return (min: lo, max: hi);
}

This question has a trap we hit ourselves while testing.

Our first version returned (int min, int max), a positional record, and result.min failed to compile: per the records documentation, names on positional fields "are purely for documentation".

Named fields, ({int min, int max}), create real getters and can be destructured with final (:min, :max) = minMax(xs);. Records also get value equality for free, which is why the test can compare with ==.

Positional: (int min, int max)
  • Fields read as $1 and $2; the names do not compile as getters
  • Destructure by position: final (lo, hi) = r;
Named: ({int min, int max})
  • Fields read as .min and .max
  • Destructure by name: final (:min, :max) = r;

11. Compute the area of any shape with an exhaustive switch.

Task: model circles and rectangles so that adding a new shape without handling it is a compile error, not a runtime one.

sealed class Shape {}

class Circle extends Shape {
  Circle(this.r);
  final double r;
}

class Rect extends Shape {
  Rect(this.w, this.h);
  final double w, h;
}

double area(Shape s) => switch (s) {
  Circle(:final r) => 3.141592653589793 * r * r,
  Rect(:final w, :final h) => w * h,
};

A sealed class can only be extended inside its own library, so the compiler knows every subtype and checks that the switch covers them all. Add a Triangle and area stops compiling until it has a case.

The object patterns Circle(:final r) destructure the getters in place. This is the Dart 3 replacement for the visitor pattern, and the class modifiers page covers the other five modifiers candidates should recognize.

12. Validate a JSON object without casts.

Task: accept {'name': 'Ada', 'age': 36} and return 'Ada (36)'. Reject a string age or a negative age with a FormatException.

String describeUser(Object? json) {
  if (json case {'name': String name, 'age': int age} when age >= 0) {
    return '$name ($age)';
  }
  throw FormatException('bad user: $json');
}

An if-case with a map pattern checks the keys, the value types and binds the variables in one expression; the when guard adds the range check. Map patterns ignore extra keys, so 'extra': true passes.

The pre-Dart 3 version needed json['age'] as int, which throws a TypeError with a message nobody can act on.

13. Implement an LRU cache.

Task: a cache with a fixed capacity that evicts the least recently used key. get and put must be O(1).

class LruCache<K, V> {
  LruCache(this.capacity);
  final int capacity;
  final _map = LinkedHashMap<K, V>();

  V? get(K key) {
    final value = _map.remove(key);
    if (value != null) _map[key] = value; // move to most recent
    return value;
  }

  void put(K key, V value) {
    _map.remove(key);
    _map[key] = value;
    if (_map.length > capacity) _map.remove(_map.keys.first);
  }
}

LinkedHashMap keeps keys in insertion order, so removing and re-inserting a key on access moves it to the end, and the first key is the least recently used.

In Java or C++ this question expects a hash map plus a hand-written linked list; in Dart, knowing the standard library is the answer.

One edge worth raising: this version treats a stored null as a miss, so it only suits non-nullable V.

14. Add a chunked method to every list.

Task: [1, 2, 3, 4, 5].chunked(2) yields [1, 2], [3, 4], [5].

extension Chunked<T> on List<T> {
  Iterable<List<T>> chunked(int size) sync* {
    if (size <= 0) throw ArgumentError.value(size, 'size');
    for (var i = 0; i < length; i += size) {
      yield sublist(i, i + size > length ? length : i + size);
    }
  }
}

An extension adds the method without subclassing List, and the sync* generator yields chunks lazily, so .first on a million-item list builds one chunk, not all of them.

Extensions resolve statically: a list typed as dynamic will not find chunked, a common source of "method not found" errors at runtime. package:collection ships the same idea as slices.

15. Write an immutable value class with equality and copyWith.

Task: two Money(500, 'USD') objects are equal and collapse to one entry in a set.

class Money {
  const Money(this.cents, this.currency);
  final int cents;
  final String currency;

  Money copyWith({int? cents, String? currency}) =>
      Money(cents ?? this.cents, currency ?? this.currency);

  @override
  bool operator ==(Object other) =>
      other is Money && other.cents == cents && other.currency == currency;

  @override
  int get hashCode => Object.hash(cents, currency);
}

Overriding == without hashCode is the bug to look for: sets and map keys would then treat equal objects as different. Object.hash combines fields correctly.

The copyWith with nullable parameters has a known limit: it cannot set a nullable field back to null. Teams usually generate all of this with freezed, and a candidate should say so, then show they could write it by hand.

16. Write a generic binary search.

Task: works for any sorted list of comparable values; returns the index or -1.

int binarySearch<T extends Comparable<T>>(List<T> sorted, T target) {
  var lo = 0, hi = sorted.length - 1;
  while (lo <= hi) {
    final mid = lo + ((hi - lo) >> 1);
    final c = sorted[mid].compareTo(target);
    if (c == 0) return mid;
    if (c < 0) {
      lo = mid + 1;
    } else {
      hi = mid - 1;
    }
  }
  return -1;
}

The bound T extends Comparable<T> lets the function call compareTo on ints, strings or a candidate's own classes.

Computing the midpoint as lo + ((hi - lo) >> 1) is a habit from languages with fixed-width overflow; Dart's native int is 64-bit, but on the web integers are JavaScript numbers, so the habit costs nothing and travels well.

Async and Streams

Most Dart code in production is asynchronous, whether it runs in a Flutter app or on a server. These six questions find out whether a candidate understands the event loop or only the await keyword.

17. Load two independent values in parallel.

Task: fetch a user and an order count, each taking 200 ms, in about 200 ms total rather than 400.

Future<String> loadDashboard() async {
  final (user, orders) = await (fetchUser(), fetchOrderCount()).wait;
  return '$user has $orders orders';
}

Awaiting the two calls one after the other takes the sum of their times.

Calling .wait on a record of futures starts both first and keeps each result's own type, which Future.wait on a List cannot do: that returns List<Object> for mixed types.

On our run the parallel version finished in 226 ms. If one future fails, .wait throws a ParallelWaitError that still carries the successful values; see the API reference.

18. Retry a flaky call with a timeout and exponential backoff.

Task: try up to three times, give each attempt two seconds, and wait 100, then 200 ms between attempts. Rethrow the last error.

Future<T> retry<T>(
  Future<T> Function() task, {
  int attempts = 3,
  Duration base = const Duration(milliseconds: 100),
}) async {
  for (var i = 1; ; i++) {
    try {
      return await task().timeout(const Duration(seconds: 2));
    } on Exception {
      if (i == attempts) rethrow;
      await Future.delayed(base * (1 << (i - 1))); // 100, 200, 400 ms
    }
  }
}

Future.timeout throws a TimeoutException, which implements Exception, so the same on Exception clause retries timeouts and failures.

Catching only Exception leaves Error subclasses such as a TypeError alone: retrying a bug does not fix it. rethrow keeps the original stack trace, which throw e would lose.

A senior answer adds jitter so a fleet of clients does not retry in lockstep.

19. Debounce a stream of search keystrokes.

Task: emit a value only after the input has been quiet for 50 ms, and flush the last value when the source closes.

Stream<T> debounce<T>(Stream<T> source, Duration wait) {
  Timer? timer;
  T? last;
  var hasValue = false;
  late final StreamController<T> out;
  out = StreamController<T>(
    onListen: () {
      source.listen(
        (event) {
          last = event;
          hasValue = true;
          timer?.cancel();
          timer = Timer(wait, () {
            hasValue = false;
            out.add(last as T);
          });
        },
        onError: out.addError,
        onDone: () {
          timer?.cancel();
          if (hasValue) out.add(last as T);
          out.close();
        },
      );
    },
  );
  return out.stream;
}

Typing "d", "da", "dar" quickly and then "dart" after a pause emits dar and dart, as the test checks. The onListen callback subscribes to the source only when someone listens, which keeps the stream lazy like the built-in ones.

Follow-ups: the subscription is never cancelled when the listener cancels (add onCancel), and a production team would reach for debounceTime in package:rxdart or package:stream_transform.

20. Stream every item from a paginated API.

Task: fetchPage(n) returns a list, empty after the last page. Expose all items as one Stream<String>.

Stream<String> allItems() async* {
  for (var page = 0; ; page++) {
    final items = await fetchPage(page);
    if (items.isEmpty) return;
    yield* Stream.fromIterable(items);
  }
}

An async* generator pauses at each yield until the listener wants more, so pages are fetched on demand: a caller that takes the first three items never requests page two. yield* forwards a whole stream. return ends the stream, which closes it for the listener.

21. Predict the output order of this code.

Task: the labels already give the answer; in the interview, show the code without the numbers and ask for the order.

void eventLoopOrder() {
  print('1 sync');
  Future(() => print('6 event queue'));
  Future.microtask(() => print('3 microtask'));
  scheduleMicrotask(() => print('4 microtask'));
  Future.value(null).then((_) => print('5 then on completed future'));
  print('2 sync');
}

Synchronous code runs to completion first (1, 2). Then the microtask queue drains completely (3, 4 and 5) before the event loop takes the next event, the Future callback (6).

The one that catches people is 5: .then on a future that has already completed does not run inline; it is scheduled as a microtask, after the microtasks queued before it. We ran this on Dart 3.13.4 and the output matched the numbers.

Four steps of Dart's event loop: synchronous code runs to the end, then every queued microtask including .then on a completed future, then one event such as a Future callback or Timer, then microtasks again before the next event.

The practical consequence: a long chain of microtasks, or a loop that keeps scheduling them, starves timers, I/O and in Flutter the next frame. await on a completed future also yields to the microtask queue.

22. Run many async tasks with at most k in flight.

Task: process seven inputs with at most three running at once, and return results in input order.

Future<List<R>> mapPool<T, R>(
  List<T> inputs,
  int limit,
  Future<R> Function(T) task,
) async {
  final results = List<R?>.filled(inputs.length, null);
  var next = 0;
  Future<void> worker() async {
    while (next < inputs.length) {
      final i = next++; // safe: no await between read and increment
      results[i] = await task(inputs[i]);
    }
  }

  await Future.wait([for (var w = 0; w < limit; w++) worker()]);
  return results.cast<R>();
}

Three workers pull the next index from a shared counter.

That counter is safe without a lock because Dart runs one isolate's code on a single thread: nothing else can run between reading next and incrementing it, since there is no await in between.

Our test records the peak concurrency and it never exceeds three. Writing results by index keeps input order even though tasks finish out of order.

Future.wait on all seven would have no limit, which is how a script exhausts a database connection pool.

Isolates and Algorithms

23. Move a CPU-heavy job off the main isolate.

Task: count the primes below one million without blocking the event loop.

int countPrimes(int limit) {
  final sieve = List<bool>.filled(limit + 1, true);
  var count = 0;
  for (var i = 2; i <= limit; i++) {
    if (!sieve[i]) continue;
    count++;
    for (var j = i * i; j <= limit; j += i) {
      sieve[j] = false;
    }
  }
  return count;
}

Future<int> countPrimesOffThread(int limit) =>
    Isolate.run(() => countPrimes(limit));

Isolate.run, added in Dart 2.19, spawns an isolate, runs the closure there, returns its result and shuts the isolate down. Isolates share no memory: Dart copies the result back, so a function that returns a large object pays for the copy.

The closure must not capture anything that cannot be sent, such as an open socket. The web has no isolates, per the concurrency guide, so code that also targets the browser needs another path.

The run above returned 78,498, the known count of primes below one million.

Decision flow for Dart concurrency: CPU-heavy work goes to Isolate.run (not on the web), many values over time use a Stream, several independent results use a record's .wait, and a single later result is a Future.

The diagram above is the question behind questions 17 to 23: async does not add threads, so work that burns CPU belongs in an isolate, and work that waits belongs in a future or a stream.

24. Find the shortest path through a grid.

Task: '.' is open and '#' is a wall. Return the number of steps from the top-left to the bottom-right, or -1.

int shortestPath(List<String> grid) {
  final rows = grid.length, cols = grid[0].length;
  final dist = {(0, 0): 0};
  final queue = Queue<(int, int)>()..add((0, 0));
  while (queue.isNotEmpty) {
    final (r, c) = queue.removeFirst();
    if ((r, c) == (rows - 1, cols - 1)) return dist[(r, c)]!;
    for (final (dr, dc) in const [(1, 0), (-1, 0), (0, 1), (0, -1)]) {
      final next = (r + dr, c + dc);
      final (nr, nc) = next;
      if (nr < 0 || nc < 0 || nr >= rows || nc >= cols) continue;
      if (grid[nr][nc] == '#' || dist.containsKey(next)) continue;
      dist[next] = dist[(r, c)]! + 1;
      queue.add(next);
    }
  }
  return -1;
}

Breadth-first search finds the shortest path on an unweighted grid, and Queue from dart:collection gives O(1) removeFirst, where a List.removeAt(0) would be O(n).

Records make coordinates easy: (r, c) has value equality and a proper hash, so it works as a map key without a custom class. On the 3 by 3 test grid the answer is 4 steps.

25. Merge overlapping intervals.

Task: [(8, 10), (1, 3), (2, 6), (15, 18), (6, 7)] returns [(1, 7), (8, 10), (15, 18)].

List<(int, int)> mergeIntervals(List<(int, int)> input) {
  final sorted = [...input]..sort((a, b) => a.$1.compareTo(b.$1));
  final merged = <(int, int)>[];
  for (final (start, end) in sorted) {
    if (merged.isNotEmpty && start <= merged.last.$2) {
      final (s, e) = merged.last;
      merged.last = (s, end > e ? end : e);
    } else {
      merged.add((start, end));
    }
  }
  return merged;
}

Sort by start, then extend the last merged interval or start a new one: O(n log n) for the sort, O(n) after. Copying with [...input] before sorting avoids mutating the caller's list, a side effect interviewers should ask about.

Records are immutable, so extending an interval means replacing merged.last, not changing its end in place.

What Changed Recently

Language versions. Each feature below needs the package's SDK constraint lower bound at that version or higher, for example sdk: ^3.13.0. A newer SDK alone does not turn it on. Dates come from the Dart SDK changelog.
Dec 11, 2024
Dart 3.6: digit separators such as 1_000_000
Jan 29, 2025
Macros stopped: the Dart team ends work on macros
Feb 12, 2025
Dart 3.7: wildcard variables and the new "tall" formatter style
May 20, 2025
Dart 3.8: null-aware elements in collection literals
Nov 12, 2025
Dart 3.10: dot shorthands
May 20, 2026
Dart 3.12: private named parameters
Aug 12, 2026
Dart 3.13: primary constructors

26. What are dot shorthands, and where can you use them?

Since Dart 3.10, you can drop the type name before a static member when the context already expects that type: .suspended instead of Status.suspended, .zero instead of Duration.zero.

enum Status { active, suspended }

class Account {
  Account(this.status, {this.limit = const Duration(minutes: 30)});
  final Status status;
  final Duration limit;
}

Account guest() => Account(.suspended, limit: .zero);

String label(Status s) => switch (s) {
  .active => 'Active',
  .suspended => 'Suspended',
};

The context type is what makes it work: a parameter type, a declared variable type or the scrutinee of a switch. var s = .active; does not compile, because nothing tells the compiler which type to look in.

In Flutter code the gain is large, since crossAxisAlignment: .start replaces CrossAxisAlignment.start across thousands of widget arguments.

Ask a candidate where they would not use it: when the type is not obvious to a reader from nearby code.

27. Rewrite a class with a primary constructor.

Primary constructors, new in Dart 3.13, put the constructor's parameters in the class header, and each final or var parameter also declares a field. An assert or other initializer goes in a this : body.

class Point(final int x, final int y) {
  this : assert(x >= 0 && y >= 0);

  Point get flipped => Point(y, x);
}

class Temperature {
  final double _celsius;
  Temperature({required this._celsius});
  double get fahrenheit => _celsius * 9 / 5 + 32;
}

The second class uses the Dart 3.12 change: a named parameter can now initialize a private field directly, and callers still write the public name, Temperature(celsius: 100). Before 3.12 that needed an initializer list.

The breaking part of 3.13 is small but real: final on an ordinary parameter, which some teams wrote to stop reassignment, is now an error, and the parameter_assignments lint takes its place.

28. Build a map that leaves out null values.

Task: only include query parameters that were passed. Null-aware elements, added in Dart 3.8, do it with a ? in front of the element.

Map<String, Object> buildQuery({String? search, int? page, String? sort}) => {
  'search': ?search,
  'page': ?page,
  'sort': ?sort,
};

buildQuery(search: 'dart', page: 2) returns {search: dart, page: 2} with no sort key. The old form, if (sort != null) 'sort': sort, repeats every name twice.

In maps the ? can go on the key, the value or both, and the value type narrows to non-nullable, which is why the map can be Map<String, Object>.

29. What changed about the underscore in Dart 3.7?

Local variables and parameters named _ became wildcards: they no longer bind a value, so one function can declare several. Before 3.7, (_, _) was a duplicate-name error and code used (_, __).

const maxRows = 1_000_000; // digit separators, Dart 3.6

var resizes = 0;
void onResize(_, _) => resizes++; // two wildcard parameters, Dart 3.7

List<String> names(List<(int, String)> rows) => [
  for (final (_, name) in rows.take(maxRows)) name,
];

The same snippet uses the Dart 3.6 digit separator in 1_000_000.

Dart 3.7 also shipped the rewritten formatter with the "tall" style, which adds and removes trailing commas itself; the style follows each package's language version, so upgrading the SDK constraint reformats code on the next dart format run.

Warn a team about that before they open a pull request with 400 changed files.

30. What happened to Dart macros, and how do you generate JSON code today?

The Dart team stopped work on macros on January 29, 2025.

The announcement says the team decided "to focus on other features we're more confident about shipping", named faster build_runner code generation as one of them, and kept augmentations, a feature prototyped for macros, as a separate language feature.

So the answer in 2026 is still code generation: json_serializable or freezed with dart run build_runner build, generated part files, and a CI check that the generated code is current.

A candidate who says "we will use macros for that" learned from 2024 material; one who hand-writes fromJson with the map patterns from question 12 for a handful of models, and generates code for the rest, is making a reasonable trade.

Signs of a Strong Answer

  • They reach for LinkedHashMap, Queue, nonNulls and indexed before writing the same thing by hand.
  • They model closed sets of cases as sealed classes and let the exhaustive switch catch the missing case.
  • They can explain the microtask queue and say why a busy chain of microtasks delays a timer or a Flutter frame.
  • They start independent futures before awaiting them, and put a concurrency limit on anything that talks to a database or an API.
  • They know what a record's field names do and do not give them, and when a small class is clearer than a record.
  • They name the SDK version their code assumes, and know which features need a raised SDK constraint.

Hiring Dart Developers

Most Dart developers work in Flutter, so the same people who pass these challenges build the app.

Second Talent matches companies with pre-vetted Flutter and Dart developers in Asia, screened with problems like the ones above; the Flutter developer cost guide lists typical rates by country.

Tell us the stack and we send a shortlist within 24 hours. Start hiring, or pair this page with our Flutter developer interview questions.

Hiring developers in Southeast Asia?

Get Cost Guide

How would you like to talk?

WhatsApp us Prefer texting at your own pace? Just hit us up on WhatsApp. We promise no spam and a hassle-free experience.

Loading available times…