Wrap a FilledButton in a small StatelessWidget that takes a label, a nullable VoidCallback? onPressed and a loading flag; while loading, pass null to onPressed so the button disables itself, and show a spinner over the invisible label.
**null means disabled**: that is the Material convention, so the theme greys the button out and blocks taps; no separate enabled flag is needed
Stateless on purpose: the parent owns loading, because the parent is the one awaiting the request; the button only renders it, and a disabled button cannot be tapped twice
Keep the size stable: the invisible label still takes its width, so the button does not shrink when the spinner appears
Style comes from the theme: no hard-coded colours, so dark mode and the brand theme apply; a const constructor with super.key, like any reusable widget
FlutterMiddle
Live Coding Round in Dart
Debounce, Result types, LRU cache, backoff, concurrency limits, tries, stream transformers
Questions closed
0 / 19
- 0Review
- 1Asked often
Questions
Start a one-second Timer.periodic in initState, read DateTime.now() inside setState on every tick, and cancel the timer in dispose; without that the timer keeps firing on a dead State and you get setState() called after dispose().
Read the clock, do not count ticks: a periodic timer is never exactly on time, so adding one second per tick drifts
Zero-pad the parts: n.toString().padLeft(2, '0'), or DateFormat.Hms() from package:intl when the format should follow the locale
Keep the clock in its own small widget: the per-second setState then rebuilds one Text, not the whole screen
The alternative: a StreamBuilder over Stream.periodic(const Duration(seconds: 1)); it cancels its subscription on dispose for you
typed is a local variable of build: assigning to it schedules no rebuild, and every rebuild would reset it to '' anyway. The text has to live in state that outlives build and tells the widget to rebuild when it changes.
**Locals in build are recreated on every build**: anything that must survive a frame belongs in a State object or a notifier
The controller is in the wrong place too: created in build, it is replaced on every parent rebuild and never disposed
The fix: a StatefulWidget that creates the controller once as a field and disposes it in dispose
**Rebuild only the Text**: TextEditingController is a ValueNotifier<TextEditingValue>, so a ValueListenableBuilder on it updates the label without a setState for the whole widget
It rejects real addresses: + tags like anna+shop@gmail.com, hyphens and digits in the domain, subdomains like mail.example.co.uk and every TLD longer than three letters. On the client, check the shape loosely and let the confirmation email do the real validation.
What the pattern misses: the local part allows only letters, digits and dots; the domain is one label of letters; {2,3} excludes .studio and .travel
**value! and no trim()**: a trailing space from keyboard autocomplete fails the match, and a null crashes instead of showing an error
A full RFC 5322 regex is not the answer: it is enormous, still accepts addresses nobody can deliver to, and nobody can review it
Separate messages: "Email is required" for empty input, "Enter a valid email" for a malformed one
Test the list: a test over valid and invalid inputs proves both and catches regressions
jsonDecode the body, cast the result to List<dynamic>, and map each element through a factory Product.fromJson(Map<String, dynamic> json); toJson returns a Map<String, dynamic> that jsonEncode accepts.
Cast element by element: jsonDecode builds a List<dynamic>, so as List<Map<String, dynamic>> on the whole list throws a TypeError; cast each item inside map
Numbers: JSON 10 decodes to int and 10.5 to double, so read a price as (json['price'] as num).toDouble()
Optional fields: json['note'] as String? covers both a missing key and an explicit null; a required field should fail loudly instead of defaulting to ''
At scale: json_serializable generates exactly this code; writing it by hand is what the live round checks
One StatefulWidget that owns a List<Todo> of immutable items, a TextField with a controller for adding, and a ListView.builder whose rows toggle through a CheckboxListTile and delete through Dismissible. Every change goes through setState with a new list.
**Immutable Todo with an id**: toggling is copyWith(done: !t.done), and the id is what the row's key is built from
**Key Dismissible by the item, never by the index: an index key hands the dismissed row's state to the next item, and Flutter throws "A dismissed Dismissible widget is still part of the tree"
Guard the input**: trim(), ignore an empty title, then clear() the controller and dispose it in dispose
Persistence, if asked: jsonEncode the list into shared_preferences after each change and load it in initState; mention that a real app moves the list out of the widget into a notifier or a Bloc
If saveName throws, the exception skips every line after the await, so _saving stays true: the button stays disabled under a spinner until the screen is reopened, and the user never learns the save failed.
Why: an exception from an awaited Future is rethrown at the await, and nothing below it runs; onPressed ignores the returned Future, so the error reaches PlatformDispatcher.onError as an uncaught async error instead of the UI
The fix: catch the expected failure with on Exception, reset the flag there and show a SnackBar, so the user can retry; on success leave the spinner until the route is gone, so a quick second tap cannot start a second save and pop (try/finally also works if the success path does not re-enable the button)
**The mounted checks stay**: the screen may be gone when the request ends; if (mounted) before setState, Navigator and ScaffoldMessenger after the await is exactly what the use_build_context_synchronously lint checks
How to catch it: a widget test with a fake repository that throws: tap Save, pump, and expect the button to be enabled again; a happy-path test never takes this branch
Fold the list into a Map<DateTime, List<Txn>> keyed by DateTime(y, m, d) taken after toLocal(), then sort the keys; a raw timestamp as key never groups, because DateTime equality is exact to the microsecond.
The fold: putIfAbsent(key, () => []).add(item) per item; groupBy from package:collection is the same thing with a name
What the key does to you: DateTime equality is exact to the microsecond and compares isUtc too, so two stamps from one day never collide. Truncate with DateTime(d.year, d.month, d.day).
Group in the user's zone: call toLocal() first, or a purchase at 01:30 local time lands under yesterday's header
Sort the keys yourself: a map literal is a LinkedHashMap, so keys come out in insertion order
Flatten once for the list: build the header and item rows outside build, so ListView.builder gets one flat list
Debounce waits for the burst to end and runs only the last call; a leading-edge throttle runs the first call and drops everything after it, including the last keystroke.
Debounce: one Timer?; every call cancels the pending one and starts a new timer, so the action runs after delay of silence. About 300 ms for search.
Throttle: run now, note the time, ignore calls until the window passes. Right for scroll and drag, wrong for search: flu goes out and flutter is dropped.
Two lifetime traps: cancel the timer in dispose, or the callback fires on a dead State; and keep the debouncer as a field, since one created inside build gets a fresh timer per rebuild and debounces nothing
Debounce does not order responses: a slow flu can land after flutter; tag each request with an increasing id and drop any response whose id is no longer the latest
A sealed class Result<T, E> with Ok and Err subtypes moves failure into the return type, so the compiler (not code review) forces every caller to deal with it.
**sealed makes the switch exhaustive**: subtypes can only live in this library, so a switch over Ok and Err needs no default; mark Ok and Err final so outside code cannot extend them
Two type parameters, not one: an E typed as Object is try/catch with extra syntax; make E its own sealed error type so the UI can switch on it
The operators: map transforms the value and passes Err through, mapError mirrors it, fold collapses both arms into one value, usually a widget
**flatMap for a step that can fail**: plain map just nests two Results, and it does not catch, so a throwing function escapes the Result entirely
Wrap I/O, not bugs: one try/catch in the repository; a bad index or a broken invariant should still crash, not land in a snackbar
A List key compares by identity: ['eu', 2.0] == ['eu', 2.0] is false, so every call builds a new key, misses, computes again and adds one more entry. Key the map with a record (zone, kg) instead; records get structural == and hashCode from the language.
Why records work: (String, double) equality compares the fields, so a later call with equal arguments finds the entry
**The old habit '$zone|$kg': works until an argument can contain the separator
putIfAbsent, not ??=**: cache[key] ??= compute() recomputes forever when the function can legitimately return null, because a stored null looks like a miss
Memoise only pure functions, and bound the map: a result that depends on DateTime.now() or the locale needs that input in the key, and an unbounded map in a long-lived repository is a leak
Store each value with its expiry time and check it on read: an expired entry is removed and reported as a miss. That lazy expiry needs no timers, and a size cap that evicts the oldest entry stops keys nobody reads again from piling up.
**Expire on read, not with a Timer per entry**: a thousand timers cost far more than one comparison per get, and they keep firing after the cache itself is gone
Lazy expiry alone leaks: an entry that is never read again is never checked, so bound the map; a Dart Map keeps insertion order, so keys.first is the oldest entry and removing it is O(1)
FIFO or LRU: re-insert the key on every hit and the same cap becomes least-recently-used eviction
Inject the clock: use clock.now() from package:clock or a DateTime Function() parameter, so a test moves time with withClock or fakeAsync instead of sleeping
Name what stale means for the product: a product catalogue can live for minutes, an account balance should not be cached at all
put on a key that is already there does not move it: assigning to an existing key of a Dart Map keeps the key's original insertion slot, so a freshly updated entry still looks least recently used and is the next to go.
Why: the default Map is a LinkedHashMap: keys are ordered by first insertion, and map[key] = value on an existing key only replaces the value. get gets this right with remove and re-insert; put does not
The fix: _map.remove(key) before the assignment, the same move get already makes; both operations stay O(1)
What it costs in production: with capacity 2, put('anna'), put('bob'), put('anna', v2), put('carl') evicts anna, the entry touched last; for updated keys the cache degrades to FIFO and the hit rate drops quietly, with no error anywhere
How to catch it: a unit test for exactly that sequence; tests that only put and then get fresh keys pass with either version
Walk both lists with two indices, always taking the smaller head, and keep a Set<String> of the ids already emitted: O(n + m), one pass, no second sort.
Two cursors: both lists sorted by (sentAt, id); compare the heads with compareTo, append the smaller, advance that cursor; when one list runs out, drain the other
Say why not concatenate and sort: that is O((n + m) log(n + m)) and throws away the one fact you were given, that both inputs are already ordered
**Set.add returns false when the id was already there**: test and insert in one hash lookup; out.any((m) => m.id == id) is what makes the merge quadratic
Decide which copy wins: the server copy wins when time and id match; if a message can change its timestamp, index the fresh page by id first
**The DateTime trap**: == compares the isUtc flag, so the same instant in UTC and local time is not equal; use compareTo or isAtSameMomentAs and store one zone
Pre-order DFS with an explicit stack: push the roots reversed, then pop a node, emit (comment, depth), and push its children reversed so they come back out in reading order.
**Push children.reversed: a stack reverses whatever you put into it, and that one detail is what the exercise is testing
Why not recursion**: comment depth is user-controlled input, and very deep recursion ends in a StackOverflowError, an Error you should not plan to catch
Depth belongs on the stack entry, not the model: the same comment is depth 0 in a permalink and depth 7 inside the thread
Flatten once per data change: keep the flat list in state, so build only indexes into it; cap the indent with min(depth, 6) * 12
**If the API sends parentId**: group the children with groupBy first; searching the flat list inside the loop is the O(n²) version that dies at a thousand comments
The app-wide bus keeps every subscription, and the listener closure captures the State, so the screen stays reachable until cancel() is called; type the bus by event class and return the StreamSubscription so callers can cancel it.
The shape: one StreamController<Object>.broadcast(), and on<T>() returns _ctrl.stream.where((e) => e is T).cast<T>(); events are classes, so a typo is a compile error
How the leak happens: the callback closes over this, so the State and everything it references live as long as the singleton bus
**A missed cancel() is not harmless**: the callback keeps firing, calls setState on a disposed State and throws on every event, and each reopen adds one more subscriber and duplicates the side effect
Make it impossible: return Stream<T> and let StreamBuilder own the subscription; when you must listen, cancel every subscription in dispose with the cancel_subscriptions lint on
Walk both structures together carrying a path and return the first mismatch as a string like $.user.tags[1], or null when equal; == on two maps is identity, and DeepCollectionEquality only answers yes or no.
What the SDK gives you: DeepCollectionEquality().equals(a, b) from package:collection answers "are they equal" correctly and never answers "where", which is the whole exercise
The walk: recurse into Map and List, compare everything else with ==, and report in a fixed order: type mismatch, key on one side only, length, then leaf inequality
Sort the union of the keys first: Dart maps iterate in insertion order, so two payloads built in different orders would report different "first" differences
Values that lie: double.nan != double.nan, and jsonDecode returns int for 1 but double for 1.0, so compare nums by value and everything else by type as well
**Return null for equal**: then expect(firstDiff(expected, actual), isNull) makes the failure message the path itself
**expect(a, equals(b)) is already deep**: it prints a Which: line, but it dumps both payloads; a path-only helper is for logs, contract checks and custom matchers
Keep a queue of the start times you have already granted: a call goes through immediately while fewer than five of them fall inside the last minute, otherwise it is booked for the moment the oldest one ages out. A resetting counter lets ten through in two seconds across the boundary.
Rolling window, not a resetting counter: a counter cleared every minute admits five at 11:59:59 and five more at 12:00:01
**Memory is bounded by max plus the calls currently waiting: prune everything older than the window on each admission, so the queue never exceeds five plus the slots booked ahead
Keeping it race-free**: compute the slot and push it onto the queue with no await between those two steps, then wait; wait first and two concurrent callers both see room
Inject the clock: clock.now() from package:clock plus Future.delayed lets a fakeAsync test run the whole minute in microseconds
A token bucket is the other correct answer: capacity five, refilled at five per window, O(1) state, and it allows short bursts; say which one you are building and why
Build it with StreamTransformer.fromBind over an async* function, so the filter state (the last fix you kept) is created fresh on every bind, and gate on the fix's own timestamp rather than the device clock.
**Compare against the last emitted fix: comparing against the last received one turns a slow walk into a stream that drops everything and never emits again
Check time before distance: a subtraction is free, the haversine is several trig calls; the first fix passes both gates
Why not fromHandlers: its handlers are created once, so state they capture is shared by every stream the transformer is bound to, and two subscriptions corrupt each other
fromBind gives backpressure too**: await for pauses the source when the listener pauses; the result is single-subscription, so add .asBroadcastStream() if several widgets listen
Run on the data's clock: after a tunnel fixes arrive in a burst with DateTime.now() milliseconds apart, but fix.at still spaces them correctly and tests can feed a fabricated list
Ask the platform first: geolocator's LocationSettings(distanceFilter: 10) drops fixes before they wake Dart; the transformer is how you combine both rules and test them