Irbisa · Шпаргалка

Live coding на Dart

Debounce, Result-типы, LRU-кэш, backoff, ограничение параллелизма, trie, трансформеры

Middle-разработчик · 19 вопросов

  1. 01

    Лайвкодинг: напишите переиспользуемую основную кнопку с текстом, колбэком onPressed и состоянием загрузки. Как выглядит хорошая реализация?

    Оберните FilledButton в небольшой StatelessWidget, который принимает label, nullable `VoidCallback?

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

    Лайвкодинг: напишите виджет, который показывает текущее время и обновляется каждую секунду. Как сделать его точным и без утечек?

    Запустите Timer.periodic на одну секунду в initState, на каждом тике читайте DateTime.now() внутри setState и отменяйте таймер в dispose: без этого таймер продолжит срабатывать на уничтоженном State, и вы получите setState() called after dispose().

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

    Этот виджет должен показывать введённый текст в Text под полем, но Text остаётся пустым: что не так и как это исправить?

    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 объявлена как локальная переменная build: присваивание ей не запускает перестройку, а каждая перестройка всё равно сбросила бы её в ''.

  4. 04

    Пользователи жалуются, что не могут зарегистрироваться с адресами, которые работают везде: что не так с этим валидатором email и что выпустить вместо него?

    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';}

    Он отклоняет настоящие адреса: теги через + вроде anna+shop@gmail.com, дефисы и цифры в домене, поддомены вроде mail.example.co.uk и любой домен верхнего уровня длиннее трёх букв.

  5. 05

    Лайвкодинг: распарсите JSON-строку с массивом товаров в List<Product> и обратно. Как написать fromJson и toJson вручную?

    Пропустите тело через jsonDecode, приведите результат к List<dynamic> и превратите каждый элемент через factory Product.fromJson(Map<String, dynamic> json); toJson возвращает Map<String, dynamic>, который принимает jsonEncode.

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

    Лайвкодинг: соберите экран TODO-списка с добавлением, отметкой и удалением. Как вы его построите за отведённое время?

    Один StatefulWidget, который владеет List<Todo> из неизменяемых элементов, TextField с контроллером для добавления и ListView.builder, где строки отмечаются через CheckboxListTile и удаляются через 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

    Найдите баг: часть пользователей жалуется, что кнопка «Сохранить» (Save) крутится бесконечно, и помогает только повторное открытие экрана.

    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);

    Если saveName бросит исключение, все строки после await пропускаются, и _saving остаётся true: кнопка так и висит неактивной со спиннером, пока экран не откроют заново, а пользователь не узнаёт, что сохранить не удалось.

  8. 08

    API отдаёт плоский список транзакций, а дизайн хочет их под заголовками дат: как сгруппировать и чем вас подставит ключ типа DateTime?

    Сверните список в Map<DateTime, List<Txn>> с ключом DateTime(y, m, d), взятым после toLocal(), а потом отсортируйте ключи; сырая метка времени как ключ никогда не сгруппирует, потому что равенство DateTime точно до микросекунды.

    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

    Поле поиска шлёт запрос на каждое нажатие, а rxdart добавить нельзя: как написать debounce и throttle и какой из них теряет последнее нажатие?

    Debounce ждёт конца серии вызовов и выполняет только последний; throttle по переднему фронту выполняет первый вызов и выбрасывает всё остальное, включая последнее нажатие.

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

    Каждый вызывающий оборачивает репозиторий в try/catch, и половина про это забывает: как написать Result<T, E> с map, mapError и fold взамен?

    sealed class Result<T, E> с наследниками Ok и Err переносит ошибку в тип возвращаемого значения, и обработать её заставляет компилятор, а не ревью.

    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

    Эта мемоизированная функция цены должна пропускать вычисление на 40 мс, но быстрее не становится, а мапа всё растёт: почему кэш ни разу не срабатывает?

    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}

    Ключ-List сравнивается по идентичности: ['eu', 2.0] == ['eu', 2.0] даёт false, поэтому каждый вызов строит новый ключ, промахивается, считает заново и добавляет ещё одну запись.

  12. 12

    Напишите кэш в памяти, записи которого истекают по TTL: где проверять истечение и что не даёт ему расти бесконечно?

    Храните каждое значение вместе со временем истечения и проверяйте его при чтении: истёкшая запись удаляется и считается промахом.

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

    Найдите баг: с этим LruCache перед загрузчиком аватаров только что изменённый пользователем аватар часто вытесняется следующим.

    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 для уже существующего ключа не переносит его в конец: присваивание существующему ключу в Dart Map сохраняет его исходную позицию вставки, поэтому только что обновлённая запись по-прежнему выглядит самой давно использованной и вытесняется следующей.

  14. 14

    В кэше лежат сообщения, отсортированные по времени, и сервер присылает ещё одну отсортированную страницу: как слить их и выкинуть дубли по id за один проход?

    Идите по обоим спискам двумя индексами, каждый раз забирая меньшую голову, и держите Set<String> уже выданных id: O(n + m), один проход, без повторной сортировки.

    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

    Ветка комментариев приходит вложенным деревом, а ListView.builder нужен плоский список с отступами: как развернуть дерево без рекурсии?

    Обход в глубину в прямом порядке с явным стеком: кладём корни в обратном порядке, снимаем узел, выдаём (comment, depth) и кладём его детей тоже в обратном порядке, чтобы они вышли в порядке чтения.

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

    Напишите типизированную шину событий с подпиской и отпиской: что удерживает слушателя живым после того, как зарегистрировавший его экран уничтожен?

    Шина живёт всё приложение и хранит каждую подписку, а замыкание слушателя захватывает State, поэтому экран остаётся достижимым, пока не вызван cancel(); типизируйте шину классами событий и возвращайте StreamSubscription, чтобы вызывающий мог его отменить.

    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

    Напишите функцию, которая глубоко сравнивает два раскодированных JSON-значения и возвращает путь первого расхождения вместо bool: в чём ловушки?

    Обходите обе структуры одновременно, неся с собой путь, и возвращайте первое расхождение строкой вроде $.user.tags[1] или null, если равны; == у двух map сравнивает идентичность, а DeepCollectionEquality отвечает только «да» или «нет».

    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

    Напишите лимитер, который пускает не больше пяти загрузок в скользящую минуту, а остальные ставит в очередь: почему счётчик, сбрасываемый раз в минуту, неверный ответ?

    Держите очередь уже выданных времён старта: вызов проходит сразу, пока внутри последней минуты их меньше пяти, иначе он бронируется на момент, когда самый старый выпадет из окна.

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

    Поток GPS отдаёт десять точек в секунду: как написать трансформер, который отдаёт не чаще одной точки в пять секунд и пропускает точки ближе десяти метров к последней оставленной?

    Соберите его через StreamTransformer.fromBind поверх async*-функции, чтобы состояние фильтра (последняя оставленная точка) создавалось заново на каждом bind, и сравнивайте по времени самой точки, а не по часам устройства.

    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()}…