Irbisa · Cheatsheet

Live Coding Round in Dart

Debounce, Result types, LRU cache, backoff, concurrency limits, tries, stream transformers

Middle Developer · 19 questions

  1. 01

    Live task: write a reusable primary button with a label, an onPressed callback and a loading state. What does a good one look like?

    Wrap a FilledButton in a small StatelessWidget that takes a label, a nullable `VoidCallback?

    class PrimaryButton extends StatelessWidget {  const PrimaryButton({    super.key,    required this.label,    required this.onPressed,    this.loading = false,…
  2. 02

    Live task: write a widget that shows the current time and updates every second. How do you keep it accurate and leak-free?

    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().

    class Clock extends StatefulWidget {  const Clock({super.key});   @override  State<Clock> createState() => _ClockState();}…
  3. 03

    This widget should show what the user types in the Text under the field, but the Text stays empty: what is wrong, and how do you fix it?

    class Echo extends StatelessWidget {  const Echo({super.key});   @override  Widget build(BuildContext context) {    final controller = TextEditingController();    var typed = '';    return Column(children: [      TextField(controller: controller, onChanged: (v) => typed = v),      Text(typed),    ]);  }}

    typed is a local variable of build: assigning to it schedules no rebuild, and every rebuild would reset it to '' anyway.

  4. 04

    Users report they cannot sign up with addresses that work everywhere else: what does this email validator get wrong, and what would you ship instead?

    final _email = RegExp(r'^[a-zA-Z0-9.]+@[a-zA-Z]+\.[a-zA-Z]{2,3}$'); String? validateEmail(String? value) {  if (_email.hasMatch(value!)) return null;  return 'Enter a valid email';}

    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.

  5. 05

    Live task: parse a JSON string holding an array of products into List<Product> and back. How do you write fromJson and toJson by hand?

    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.

    import 'dart:convert'; class Product {  const Product({required this.id, required this.title, required this.price, this.note});   final int id;…
  6. 06

    Live task: build a TODO list screen with add, toggle and remove. How would you structure it in the time you have?

    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.

    class Todo {  const Todo(this.id, this.title, {this.done = false});  final String id, title;  final bool done;  Todo copyWith({bool? done}) => Todo(id, title, done: done ?? this.done);}…
  7. 07

    Find the bug: some users report that the Save button keeps spinning forever, and only reopening the screen helps.

    bool _saving = false; Future<void> _save() async {  setState(() => _saving = true);  await widget.repo.saveName(_name.text);  if (!mounted) return;  setState(() => _saving = false);  Navigator.of(context).pop();} @overrideWidget build(BuildContext context) =>    PrimaryButton(label: 'Save', loading: _saving, onPressed: _save);

    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.

  8. 08

    The API returns a flat list of transactions and the design wants them under date headers: how do you group them, and what makes the DateTime key betray you?

    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.

    DateTime dayOf(DateTime t) {  final local = t.toLocal(); // the server sends UTC  return DateTime(local.year, local.month, local.day); // time stripped} List<({DateTime day, List<Txn> items})> sections(List<Txn> txns) {…
  9. 09

    A search field fires a request per keystroke and you cannot add rxdart: how do you write debounce and throttle, and which one loses the last keystroke?

    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.

    class Debouncer {  Debouncer(this.delay);  final Duration delay;  Timer? _timer;   void run(void Function() action) {…
  10. 10

    Every caller of your repository wraps it in try/catch and half of them forget: how would you write a Result<T, E> with map, mapError and fold instead?

    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 class Result<T, E> {  const Result();   R fold<R>(R Function(T value) onOk, R Function(E error) onErr) => switch (this) {        Ok(:final value) => onOk(value),        Err(:final error) => onErr(error),…
  11. 11

    This memoized pricing function should skip a 40 ms computation, yet it never gets faster and the map keeps growing: why does the cache never hit?

    final _cache = <List<Object>, double>{}; double shippingCost(String zone, double kg) {  final key = [zone, kg];  final hit = _cache[key];  if (hit != null) return hit;  return _cache[key] = _computeShipping(zone, kg); // 40 ms}

    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.

  12. 12

    Write an in-memory cache whose entries expire after a TTL: where do you check expiry, and what stops it from growing forever?

    Store each value with its expiry time and check it on read: an expired entry is removed and reported as a miss.

    import 'package:clock/clock.dart'; class TtlCache<K, V> {  TtlCache({required this.ttl, this.maxEntries = 200});   final Duration ttl;…
  13. 13

    Find the bug: with this LruCache behind the avatar loader, the avatar a user has just changed is often the next entry evicted.

    class LruCache<K, V extends Object> {  LruCache(this.capacity);  final int capacity;  final _map = <K, V>{}; // insertion order: first key = least recent   V? get(K key) {    final value = _map.remove(key);    if (value != null) _map[key] = value; // move to the end: most recent    return value;  }   void put(K key, V value) {    _map[key] = value;    if (_map.length > capacity) _map.remove(_map.keys.first);  }}

    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.

  14. 14

    Your cache holds messages sorted by time and the server returns another sorted page: how do you merge them and drop duplicate ids in one pass?

    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.

    List<Message> mergeSorted(List<Message> cached, List<Message> fresh) {  final out = <Message>[];  final seen = <String>{};  var i = 0, j = 0;   void take(Message m) {…
  15. 15

    A comment thread arrives as a nested tree and ListView.builder needs a flat list with indentation: how do you flatten it without recursion?

    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.

    class Comment {  Comment(this.id, this.text, {this.children = const []});  final String id;  final String text;  final List<Comment> children;}…
  16. 16

    Write a typed event bus with subscribe and unsubscribe: what keeps a listener alive after the screen that registered it is gone?

    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.

    class EventBus {  final _ctrl = StreamController<Object>.broadcast();   void emit(Object event) => _ctrl.add(event);   // where + cast: dart:async has no whereType on Stream…
  17. 17

    Write a function that deep-compares two decoded JSON values and returns the path of the first difference instead of a bool: what are the traps?

    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.

    String? firstDiff(Object? expected, Object? actual, [String path = r'$']) {  if (expected is Map && actual is Map) {    final keys = <Object?>{...expected.keys, ...actual.keys}.toList()      ..sort((a, b) => '$a'.compareTo('$b')); // insertion order would make "first" random    for (final k in keys) {      if (!expected.containsKey(k)) return '$path.$k: unexpected (${actual[k]})';…
  18. 18

    Write a limiter that lets at most five uploads start per rolling minute and queues the rest: why is a counter that resets every minute the wrong answer?

    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.

    import 'package:clock/clock.dart'; class RateLimiter {  RateLimiter({required this.max, required this.window});   final int max;…
  19. 19

    A GPS stream fires ten fixes a second: how do you write a transformer that emits at most one every five seconds and skips fixes within ten metres of the last one kept?

    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.

    class Fix {  const Fix(this.lat, this.lon, this.at);  final double lat, lon;  final DateTime at; // the fix's own timestamp, not DateTime.now()}…