Основные типы Dart: int и double (оба подтипы num), String, bool и коллекции List, Set и Map. Любое значение является объектом, даже 3 и null, поэтому у чисел есть методы вроде 3.isOdd.
**int / double**: целые и дробные числа; num принимает и те и другие. На мобильных и десктопе int 64-битный, а в вебе точен только до 2^53
**String**: неизменяемый текст в UTF-16; '$name' и '${a + b}' подставляют значения
**bool**: только true и false; «истинных» значений в Dart нет, поэтому if (list.length) не скомпилируется
**List, Set, Map**: обобщённые коллекции: List<int>, Map<String, User>
Особые типы: Object служит корнем всех не-null значений, dynamic отключает статические проверки, void значит «полезного значения нет», Never означает «нормально не возвращается»
FlutterJunior
Основы Dart
Переменные, типы, функции, null safety и базовый синтаксис Dart
Закрыто вопросов
0 / 22
- 0Повторить
- 5Часто спрашивают
Вопросы
var просит компилятор вывести статический тип из инициализатора; dynamic выключает статическую проверку типов; Object? является настоящим типом, который принимает любое значение, но почти ничего не даёт вызвать, пока вы не проверите, что внутри.
**var**: var name = 'Alice' навсегда String; если позже присвоить туда 42, будет ошибка компиляции. final выводит тип так же
**dynamic**: компилируется любой вызов, а опечатка превращается в NoSuchMethodError в рантайме. Обычно его законно применяют только к результату jsonDecode, и даже там каждое поле приводят к типу при разборе
**Object?**: пока вы не сузите тип через if (value is String), доступны только toString(), hashCode, == и runtimeType; после проверки открывается API String
Частая ошибка: var x; без инициализатора получает тип dynamic; задайте значение или напишите тип
Оба значат, что переменной присваивают значение один раз. final получает значение в рантайме, когда выполняется эта строка; const должен быть константой времени компиляции, а созданный объект глубоко неизменяем и канонизирован: это один общий экземпляр.
**final**: любое значение, включая DateTime.now() или ответ сервера; сам объект при этом может меняться, так что final List принимает add
**const**: только литералы, другие константы и const-конструкторы; const now = DateTime.now() не скомпилируется
Канонизация: два одинаковых const-выражения дают один и тот же объект (identical возвращает true), поэтому повторяющаяся константа хранится один раз
Поля: поле экземпляра может быть final, но не const; константу уровня класса объявляют как static const
Во Flutter: const-виджет на каждом билде остаётся тем же экземпляром, поэтому фреймворк не перестраивает это поддерево
Тип без ? никогда не может содержать null, и компилятор это гарантирует; String? явно разрешает null. «Sound» (надёжная) означает, что гарантия действует и в рантайме: non-nullable переменная никогда не бывает null, и компилятор может на это опираться.
Non-nullable по умолчанию: String name; нужно присвоить до использования; String? email; изначально равна null
Компилятор заставляет проверить: вызвать email.length нельзя, пока вы не обработаете null через if, ?. или ??
Продвижение типа (type promotion): после if (email != null) локальная переменная считается String. Публичные поля и геттеры не продвигаются, потому что значение может измениться между проверкой и использованием; приватные final-поля продвигаются начиная с Dart 3.2
**late**: non-nullable переменная, которую вы обещаете присвоить до первого чтения; чтение раньше бросает LateInitializationError
Зачем это нужно: краши из-за null переезжают из продакшена в анализатор, а начиная с Dart 3 любая программа работает с sound null safety; ненадёжного режима больше нет
Они обрабатывают null без if: ?. обращается к члену, только если получатель не null, ?? подставляет запасное значение, ??= присваивает, только если переменная null, а ! утверждает, что значение не null, и бросает исключение, если это не так.
**a?.b**: даёт null, если a равно null, иначе a.b; остаток цепочки тоже пропускается
**a ?? b**: a, если оно не null, иначе b; b не вычисляется, когда у a есть значение
**a ??= b**: присваивает b, только если a равно null; удобно для кэша, который создаётся при первом обращении
**a?..b()**: null-aware каскад: весь каскад пропускается, если a равно null
**[...?list]**: разворачивает коллекцию, которая может быть null; для null ничего не добавляет
**a!**: снимает nullability; если a равно null, в рантайме будет TypeError: «Null check operator used on a null value»
Частая ошибка: ставить !, чтобы заглушить анализатор; лучше ?., ?? или ранний return, а ! оставить для значений, которые вы только что проверили на null
is и is! проверяют тип значения в рантайме и возвращают bool, а успешная проверка продвигает переменную до этого типа. as выполняет приведение: оно велит компилятору считать значение этим типом и бросает TypeError в рантайме, если догадка неверна.
**is**: if (shape is Circle) продвигает shape до Circle внутри ветки, приведение не нужно
**is!**: отрицание: после if (value is! String) return; ниже переменная уже String
**as: не даёт ничего, на что можно отреагировать: неверная догадка падает с «type 'int' is not a subtype of type 'double' in type cast»
as? нет**: безопасного приведения в Dart нет; пишите value is String ? value : null
Ловушка с JSON: на мобильных и десктопе jsonDecode превращает 10 в int, а 10.0 в double, поэтому json['price'] as double работает для одного товара и падает на другом; пишите (json['price'] as num).toDouble()
Правило: предпочитайте is с продвижением типа; as используйте только там, где неверный тип действительно означает баг
По умолчанию параметр позиционный и обязательный. Параметры в [ ] необязательные и позиционные; параметры в { } именованные, необязательные, если не помечены required, и передаются по имени в любом порядке.
Именованные: void greet({required String name, int age = 0}), вызов greet(age: 5, name: 'Ann'); required заставляет вызывающий код его передать
Необязательные позиционные: void log(String msg, [String level = 'INFO']), вызов log('hi') или log('hi', 'ERROR'); они идут последними
Значения по умолчанию: необязательный параметр без значения по умолчанию должен быть nullable, ведь его могут не передать
Смешивать нельзя: у одной функции необязательные параметры либо в [ ], либо в { }, но не оба вида сразу
Почему Flutter любит именованные: виджет с десятью аргументами остаётся читаемым, а новый необязательный параметр не ломает существующие вызовы
В Dart функция является значением: её можно положить в переменную, передать в другую функцию и вернуть. Стрелочная функция использует короткий синтаксис тела, у анонимной функции нет имени, а замыканием называют функцию, которая продолжает пользоваться переменными той области, где её создали.
Стрелочная функция: int twice(int x) => x * 2;, где => возвращает значение одного выражения
Анонимная функция: пишется прямо там, где нужна: list.map((x) => x * 2), onPressed: () { ... }
Замыкание: захватывает сами окружающие переменные, а не их копии, и держит их живыми, пока жива функция, даже после того как внешняя функция вернула управление
Почему это важно во Flutter: каждый onPressed, builder и слушатель стрима оказывается замыканием; колбэк, захвативший State, держит его в памяти, пока сам колбэк не освобождён
Частая ошибка: думать, что => значит «анонимная»: именованная функция тоже может использовать =>, а у анонимной может быть тело в { }
Тип функции описывает сигнатуру: void Function(String) значит «принимает String, ничего не возвращает». typedef даёт имя любому типу, чаще всего сигнатуре колбэка, чтобы написать её один раз, а не повторять везде.
Типы функций: ReturnType Function(ArgTypes); используются для переменных, параметров и возвращаемых значений
**typedef**: typedef OnSaved = void Function(String value);, а дальше required OnSaved onSaved
Любой тип, не только функции: typedef Json = Map<String, dynamic>; сокращает длинные обобщённые типы; это лишь псевдоним, а не новый тип
Зачем: контракт объявлен в одном месте; поменяйте одну строку, и компилятор покажет каждый вызов, который нужно обновить
Во Flutter: VoidCallback и ValueChanged<T> объявлены во фреймворке как typedef'ы; используйте их вместо ручного void Function()
Начиная с Dart 2.17 enum является классом с фиксированным набором константных экземпляров: у него бывают final-поля, const-конструктор, методы и геттеры, и он может реализовать интерфейсы. value.name даёт String, а Plan.values.byName(s) превращает строку обратно.
Встроенное: values перечисляет значения в порядке объявления, index хранит позицию, name содержит идентификатор; toString() возвращает Plan.pro, поэтому ключи из него не строят
Незнакомые строки: byName бросает ArgumentError на имя, которого не знает, например на тариф, который сервер добавил после вашего релиза; для внешних данных берите values.asNameMap()[raw] с запасным значением
**Сохраняйте name, а не index: значение, вставленное в середину, сдвигает все индексы после него
Исчерпывающий switch**: без ветки _ switch обязан перечислить все значения, поэтому с новым значением компилятор покажет каждое место, которое нужно обновить
Ограничения: поля только final, == и hashCode переопределить нельзя, и от enum нельзя наследоваться; если каждый вариант несёт свои данные, берите sealed-класс
Выбирайте по тому, как будете читать данные: List хранит порядок и допускает дубликаты, Set хранит уникальные значения с быстрым contains, Map ищет значения по уникальному ключу. Все три обобщённые, а у List и Set общие методы Iterable.
**List**: list[i], add, insert, removeAt, sort; contains перебирает все элементы, то есть O(n)
**Set**: add возвращает false для дубликата, contains в среднем O(1); union, intersection, difference. Пустой {} создаёт Map, поэтому пустое множество пишут как <int>{}
**Map**: map[key] возвращает null для отсутствующего ключа; putIfAbsent, update, containsKey, entries. Map и Set по умолчанию сохраняют порядок вставки
**Методы Iterable**: map, where, firstWhere, any, every, fold, expand; map и where ленивые и возвращают Iterable, поэтому вызывайте toList(), когда нужен список
Преобразования: list.toSet() убирает дубликаты; {for (final u in users) u.id: u} строит map по id
Частая ошибка: list.contains внутри цикла по другому большому списку даёт O(n·m); сначала превратите сторону поиска в Set
Каскад .. выполняет несколько операций над одним объектом и возвращает этот объект. Spread ... вставляет все элементы одной коллекции в литерал другой, а collection if / for добавляют элементы по условию или в цикле прямо внутри литерала.
Каскад: в Paint()..color = c..strokeWidth = 2 каждое .. действует на тот же Paint, и выражение возвращает его, а не результат последнего вызова
Spread: [...a, ...b], работает и в литералах Set и Map; ...?maybe пропускает коллекцию, равную null, а ?.. пропускает каскад, если получатель null
**Collection if / for**: [header, if (isAdmin) adminTile, for (final i in items) Tile(i)]; так во Flutter собирают children без временных списков
Частая ошибка: final l = [1].add(2); даёт void, а final l = [1]..add(2); даёт список
Выведет [1, 2, 3, 0, 4]. Переменные хранят ссылки, и Dart передаёт копию ссылки: b и оба параметра указывают на один и тот же список, поэтому add его меняет, а items = [99] лишь перепривязывает локальный параметр.
**final b = a**: копирует ссылку, а не список; b.add(0) видно через a
Присваивание параметру: меняет только локальное имя; вызывающий код по-прежнему держит исходный список
Мутация через параметр: add, remove, user.name = ... меняют единственный общий объект
Нужна копия? Используйте List.of(a) или [...a]; копия поверхностная, сами элементы остаются общими
Ловушка во Flutter: notifier.value.add(x) меняет тот же список, поэтому ValueNotifier не уведомляет слушателей; присваивайте новый список
Строки B, C и E бросят UnsupportedError; A и D отработают. final замораживает только переменную, const и List.unmodifiable замораживают содержимое, а List.filled создаёт список фиксированной длины: элементы менять можно, а длину нельзя.
В реальной программе: выполнение остановится на строке B, и C с E не запустятся; оценивайте каждую строку отдельно
**A: final-список**: растущий; переприсвоить a нельзя, а менять список можно
**B: const-список**: глубоко неизменяемый; add бросает «Cannot add to an unmodifiable list»
**C: List.unmodifiable**: копия только для чтения, созданная в рантайме; c[0] = 9 бросает «Cannot modify an unmodifiable list»; удобна, чтобы отдавать внутреннее состояние из геттера
**D и E: List.filled**: фиксированная длина: d[0] = 9 можно, а add бросает «Cannot add to a fixed-length list»; передайте growable: true, чтобы список мог расти
Ни одна строка не ошибка компиляции: статический тип везде List<int>, поэтому анализатор их не различает
List.filled вычисляет значение-заполнитель один раз, поэтому все строки указывают на один и тот же объект внутреннего списка. Запись в cells[0][1] меняет этот единственный список, и X появляется в каждой строке.
Почему: List.filled(n, value) кладёт одну и ту же ссылку во все n ячеек; для неизменяемых значений вроде 0, '.' или null это безвредно, а для всего изменяемого (List, Map или объекта модели) это ошибка
Исправление: List.generate(size, (_) => List.filled(size, '.')) вызывает генератор для каждого индекса, и каждая строка получается новым списком; collection-for, [for (var i = 0; i < size; i++) List.filled(size, '.')], делает то же самое
Та же ловушка в других местах: List.filled(3, <String>[]) для списков элементов по вкладкам или один экземпляр Map, переиспользованный как значение по умолчанию для многих ключей
Как поймать: в отладчике identical(cells[0], cells[1]) возвращает true; юнит-тест, который ставит одну отметку и проверяет, что остальные строки не изменились, ловит баг одной строкой
Опасный код оборачивают в try, конкретные типы обрабатывают через on Type, объект и стек-трейс получают через catch (e, st), а очистку делают в finally. Exception обозначает ожидаемый сбой, который нужно обработать; Error служит признаком бага, который надо чинить, а не ловить.
**on и catch**: on FormatException фильтрует по типу; catch (e, st) даёт объект и StackTrace; можно совместить: on FormatException catch (e)
Exception: FormatException, TimeoutException, SocketException: кривой ввод или упавший запрос; покажите сообщение и работайте дальше
Error: ArgumentError, StateError, RangeError, TypeError: баг; пусть дойдёт до краш-репортера, а не будет проглочен
**rethrow**: передаёт ту же ошибку выше с исходным стек-трейсом; throw e начинает новый трейс с текущей строки
Свои ошибки: бросайте класс, который implements Exception, а не String, чтобы вызывающий код мог поймать его по типу
Async: try / catch ловит ошибки Future, которые вы ждёте через await; ошибка Future, который вы не ждали через await, мимо него проходит
assert(condition, message) проверяет ваши собственные допущения во время разработки. Он работает только в debug-сборке и вырезается из profile и release, поэтому плохое значение, которое assert ловит у вас на машине, в сборке из стора проходит молча.
Только debug: в release выражение даже не вычисляется, так что побочный эффект внутри assert пропадает
Пишите сообщение: assert(percent <= 1, 'got $percent') показывает плохое значение прямо в ошибке
Инварианты, а не ввод: данным от пользователя, из JSON или публичного API нужен if (...) throw ArgumentError(...) или FormatException, которые работают и в release
Код только для debug: используйте if (kDebugMode) { ... } из package:flutter/foundation.dart; это константа, поэтому компилятор выкидывает ветку из release-сборки
Правило: если плохое значение может прийти от реального пользователя, нужен throw, а не assert
Выведет 0 1 2 3 3 3, по одному числу на строку. Цикл for в Dart создаёт свежую i на каждой итерации, поэтому каждое замыкание хранит своё значение; цикл while использует одну j, объявленную снаружи, поэтому все три замыкания делят её и видят итоговое значение 3.
Замыкание захватывает переменную, а не значение: оно читает j в момент вызова, а не в момент создания
**for особенный**: Dart даёт каждой итерации свою копию переменной цикла, поэтому классического бага с var из JavaScript здесь нет
Всё, что объявлено вне цикла, общее: счётчики, переменные while, поля
Как исправить: скопировать в локальную переменную внутри тела: final current = j; callbacks.add(() => print(current));
Где это бьёт во Flutter: колбэки, собранные в цикле while или по общему индексу: каждый onPressed в итоге использует последнее значение
user является полем, а await даёт выполниться другому коду: если logout() обнулит его, пока запрос в пути, user! в последней строке бросит исключение. Проверка на null до await ничего не гарантирует про поле после него.
Почему анализатор отказал: публичное не-final поле не сужается до non-nullable типа (type promotion) после user == null именно потому, что его значение может измениться между проверкой и использованием; ! заглушил эту ошибку, а не устранил причину
**await открывает окно**: пока выполняется fetchAvatarUrl, цикл событий обрабатывает нажатие «Выйти», событие стрима или таймер, и любой из них может сбросить поле
Исправление: один раз прочитайте поле в локальную переменную, final current = user;: её тип сужается до User, ! не нужен, и измениться, пока вы с ней работаете, она не может
Устаревший результат: после await проверьте user != current, чтобы аватар, загруженный для вышедшего или сменившегося аккаунта, отбросить, а не записать
Как поймать: считайте каждый ! на поле поводом для вопроса на ревью (это проверяет правило DCM avoid-non-null-assertion); тест, который вызывает refreshAvatar(), а затем logout() без await между ними, воспроизводит падение
identical(a, b) проверяет, указывают ли две ссылки на один и тот же объект в памяти; == проверяет равенство так, как его определил класс. По умолчанию == проверяет идентичность, пока класс его не переопределит, как это делают String, int и value-классы.
**== по умолчанию**: унаследован от Object и совпадает с identical; два объекта User('Ann') не равны, пока User не переопределит == и hashCode
**Переопределённый ==**: 'ab' == 'a' + 'b' даёт true. Коллекции содержимое не сравнивают: [1] == [1] даёт false; используйте listEquals (из flutter/foundation.dart) или DeepCollectionEquality (из package:collection)
**Канонизация const**: одинаковые const-выражения дают один объект, поэтому identical(const Point(1, 2), const Point(1, 2)) возвращает true без всякого переопределения ==
Flutter опирается на идентичность: если экземпляр виджета identical предыдущему, как у const-виджета, фреймворк пропускает обновление этого поддерева
Частая ошибка: переопределить == без hashCode: тогда Set и ключи Map считают равные объекты разными
double является 64-битным двоичным числом с плавающей точкой: у 0.1 нет точного двоичного представления, и ошибки округления накапливаются. Деньги храните как int в минимальных единицах (центы, тиыны) или в десятичном типе, форматируя только для вывода.
**Не сравнивайте double через ==**: сравнивайте с допуском: (a - b).abs() < 1e-9
**toStringAsFixed(2): прячет ошибку в UI, но не исправляет хранимое значение; после новых вычислений погрешность возвращается
Десятичная арифметика**: пакет decimal даёт точные вычисления по основанию 10 для процентов и конвертации валют
Нюанс веба: во Flutter web int точен только до 2^53
Правило: double для измерений, int для счётчиков и денег в минимальных единицах, String для идентификаторов
DateTime хранит момент времени (микросекунды с 1970 года по UTC) и флаг isUtc; часового пояса в нём нет. Храните и передавайте UTC с явным Z, вызывайте toLocal() только для отображения, а моменты сравнивайте через isAtSameMomentAs.
**DateTime.now(): локальное время; смещение устройства применяется, когда вы читаете поля или форматируете значение
DateTime.parse**: возвращает UTC, если строка заканчивается на Z или смещение, и локальное время, если нет, поэтому '2026-01-01T10:00' в разных странах означает разные моменты
**== сравнивает и флаг**: UTC-значение и его локальный двойник не равны; используйте isAtSameMomentAs
**toIso8601String()**: пишет Z только для UTC, так что сериализованное локальное значение теряет смещение; отправляйте toUtc().toIso8601String()
Ловушка перехода на летнее время: add(Duration(days: 1)) добавляет ровно 24 часа; для «то же время завтра» используйте DateTime(y, m, d + 1, hour)
Другой часовой пояс: DateTime не может представить «9:00 в Алматы» на телефоне с другим поясом; используйте пакет timezone и TZDateTime