Примеры кода на Dart

Этот сборник не является исчерпывающим — это всего лишь краткое введение в язык для людей, которые любят учиться на собственном примере.

Hello World

Каждое приложение имеет функцию main(). Для отображения текста на консоли вы можете использовать функцию верхнего уровня print():

void main() {
  print('Hello, World!');
}

Переменные

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

var name = 'Voyager I';
var year = 1977;
var antennaDiameter = 3.7;
var flybyObjects = ['Jupiter', 'Saturn', 'Uranus', 'Neptune'];
var image = {
  'tags': ['saturn'],
  'url': '//path/to/saturn.jpg'
};

Узнайте больше о переменных в Dart, включая значения по умолчанию, ключевые слова final и const, а также статические типы.

Условные операторы

Dart поддерживает обычные операторы потока управления:

if (year >= 2001) {
  print('21st century');
} else if (year >= 1901) {
  print('20th century');
}

for (var object in flybyObjects) {
  print(object);
}

for (int month = 1; month <= 12; month++) {
  print(month);
}

while (year < 2016) {
  year += 1;
}

Узнайте больше об операторах потока управления в Dart, включая break и continue, switch и case, а также assert.

Функции

Хорошей практикой в языке Dart является явное указание возвращаемых типов и передаваемых аргументов функций:

int fibonacci(int n) {
  if (n == 0 || n == 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

var result = fibonacci(20);

Сокращенный синтаксис => (стрелка) удобен для функций, содержащих один оператор. Этот синтаксис особенно полезен при передаче анонимных функций в качестве аргументов:

flybyObjects.where((name) => name.contains('turn')).forEach(print);

Помимо отображения анонимной функции (аргумент для where()), этот код показывает, что вы можете использовать функцию в качестве аргумента: функция верхнего уровня print() является аргументом для forEach().

Узнайте больше о функциях в Dart, включая необязательные параметры, значения параметров по умолчанию и лексическую область видимости.

Комментарии

Комментарии Dart обычно начинаются с // .

// Это обычный однострочный комментарий. 
/// Это комментарий к документации, используемый для документирования библиотек, 
/// классов и их членов. Инструментов, такие как IDE и dartdoc. 
/* Комментарии, подобные этим, также поддерживаются. */

Узнайте больше о комментариях в Dart, в том числе о том, как работает инструментальная документация.

Импорты Imports

Чтобы получить доступ к API, определенным в других библиотеках, используйте импорт через ключевое слово import.

//Импорт основных библиотек
import 'dart:math';

//Импорт библиотек из внешних пакетов
import 'package:test/test.dart';

//Импорт файлов
import 'path/to/my_other_file.dart';

Узнайте больше о библиотеках и видимости в Dart, в том числе о префиксах библиотек, show и hide и отложенной загрузке через ключевое слово deferred.

Классы

Вот пример класса с тремя свойствами, двумя конструкторами и методом. Одно из свойств не может быть установлено напрямую, поэтому оно определяется с помощью метода-геттера (вместо переменной).

class Spacecraft {
  String name;
  DateTime launchDate;

  //Конструктор, с синтаксическим сахаром для назначения членам.
  Spacecraft(this.name, this.launchDate) {
    // Initialization code goes here.
  }

  //Именованный конструктор, который перенаправляет на конструктор по умолчанию.
  Spacecraft.unlaunched(String name) : this(name, null);

  int get launchYear =>
      launchDate?.year; // read-only non-final property

  //Метод.
  void describe() {
    print('Spacecraft: $name');
    if (launchDate != null) {
      int years =
          DateTime.now().difference(launchDate).inDays ~/
              365;
      print('Launched: $launchYear ($years years ago)');
    } else {
      print('Unlaunched');
    }
  }
}

Вы можете использовать вышеописанный класс Spacecraft следующим образом:

var voyager = Spacecraft('Voyager I', DateTime(1977, 9, 5));
voyager.describe();

var voyager3 = Spacecraft.unlaunched('Voyager III');
voyager3.describe();

Узнайте больше о классах в Dart, включая списки инициализаторов, необязательные new и const, перенаправляющие конструкторы, фабричные конструкторы factory, геттеры, сеттеры и многое другое.

Наследования

Dart имеет простое наследование

class Orbiter extends Spacecraft {
  num altitude;
  Orbiter(String name, DateTime launchDate, this.altitude)
      : super(name, launchDate);
}

Читайте больше о расширениях классов и об дополнительной аннотации @override.

Миксины

Миксины - это способ повторного использования кода в нескольких иерархиях классов. Следующий класс может действовать как миксин:

class Piloted {
  int astronauts = 1;
  void describeCrew() {
    print('Number of astronauts: $astronauts');
  }
}

Чтобы добавить возможности миксина в класс, просто расширьте класс миксином.

class PilotedCraft extends Spacecraft with Piloted {
  // ···
}

У PilotedCraft теперь есть поле astronauts, а также метод descriptionCrew().

Узнайте больше о миксинах.

Интерфейсы и абстрактные классы

Dart не имеет ключевого слова interface. Вместо этого все классы неявно определяют интерфейс. Следовательно, вы можете реализовать любой класс.

class MockSpaceship implements Spacecraft {
  // ···
}

Узнайте больше о неявных интерфейсах.

Вы можете создать абстрактный класс, который будет расширен (или реализован) конкретным классом. Абстрактные классы могут содержать абстрактные методы (с пустыми телами).

abstract class Describable {
  void describe();

  void describeWithEmphasis() {
    print('=========');
    describe();
    print('=========');
  }
}

Теперь, любой класс, расширяющий Describable, имеет метод descriptionWithEmphasis(), который вызывает реализацию расширителей для метода description().

Узнайте больше об абстрактных классах и методах.

Асинхронность

Избегайте ада обратного вызова и сделайте ваш код более читабельным, используя async и await.

const oneSecond = Duration(seconds: 1);
// ···
Future printWithDelay(String message) async {
  await Future.delayed(oneSecond);
  print(message);
}

Вышеуказанный метод эквивалентен:

Future printWithDelay(String message) {
  return Future.delayed(oneSecond).then((_) {
    print(message);
  });
}

Как показывает следующий пример, async и await помогают сделать асинхронный код легко читаемым.

Future createDescriptions(Iterable objects) async {
  for (var object in objects) {
    try {
      var file = File('$object.txt');
      if (await file.exists()) {
        var modified = await file.lastModified();
        print(
            'File for $object already exists. It was modified on $modified.');
        continue;
      }
      await file.create();
      await file.writeAsString('Start describing $object in this file.');
    } on IOException catch (e) {
      print('Cannot create description for $object: $e');
    }
  }
}

Вы также можете использовать async *, который дает вам хороший читаемый способ создания потоков.

Stream report(Spacecraft craft, Iterable objects) async* {
  for (var object in objects) {
    await Future.delayed(oneSecond);
    yield '${craft.name} flies by $object';
  }
}

Узнайте больше о поддержке асинхронности, включая асинхронные функции, Future, Stream и асинхронный цикл ( await for ).

Исключения

Чтобы вызвать исключение, используйте команду throw:

if (astronauts == 0) {
  throw StateError('No astronauts.');
}

Чтобы поймать исключение, используйте оператор try с on или catch (или с обоими):

try {
  for (var object in flybyObjects) {
    var description = await File('$object.txt').readAsString();
    print(description);
  }
} on IOException catch (e) {
  print('Could not describe object: $e');
} finally {
  flybyObjects.clear();
}

Обратите внимание, что приведенный выше код является асинхронным; try работает как для синхронного кода, так и для кода в async функции.

Узнайте больше об исключениях, включая трассировку стека, переброс( rethrow ) и разницу между ошибками и исключениями.

Другие темы

Еще много примеров кода в языковой и библиотечной экскурсиях. Также смотрите ссылку на API Dart, которая часто содержит примеры.

Краткий тур по языку Dart

В мире родился еще один динозаврик и имя ему Dart ? ?

Для начала тура скопипастим предмет обсуждения из Wiki

Dart — язык программирования, созданный Google. Dart позиционируется в качестве замены/альтернативы JavaScript. Один из разработчиков языка Марк Миллер написал, что JavaScript «имеет фундаментальные изъяны», которые невозможно исправить. Поэтому и был создан Dart.

На этой странице показано, как использовать все основные функции Dart, от переменных и операторов до классов и библиотек, при условии, что вы уже знаете, как программировать на другом языке. Более краткое, менее полное введение в язык см. На странице примеров языка.

Чтобы узнать больше об основных библиотеках Dart, см. Обзор библиотек. Всякий раз, когда вы хотите узнать больше о языковой функции, обратитесь к спецификации языка Dart.

Примечание. С помощью DartPad вы можете экспериментировать с большинством языковых функций Dart (подробнее). Эта страница использует встроенные DartPad редактор с возможностью запускать код в браузере. Если вместо DartPad отображаются пустые поля, перейдите на страницу устранения неполадок DartPad.

Базовая программа на Dart

Следующий код использует многие из основных функций Dart:

// Определяем функцию.
printInteger(int aNumber) {
  print('The number is $aNumber.'); // Print to console.
}

// Это то место, где программа начинает запускаться.
main() {
  var number = 42; // Declare and initialize a variable.
  printInteger(number); // Call a function.
}

Вот что использует эта программа, которая применяется ко всем (или почти ко всем) приложениям Dart:

  • // — с этого начинается любой однострочный комментарий;
  • int — тип целочисленных данных. Есть и другие встроенные типы, к примеру String, List и bool;
  • 42 — числовой литерал. Числовые литералы являются своего рода константой времени компиляции;
  • print() — удобный способ отображения вывода;
  • ‘…’ (или «…») — строковый литерал;
  • $variableName (или ${expression}) — строковая интерполяция: в том числе переменная или строковый эквивалент выражения внутри строкового литерала. Для получения дополнительной информации см. Строки.
  • main() — Специальная обязательная функция верхнего уровня, с которой начинается выполнение приложения. Для получения дополнительной информации см. Функцию main();
  • var — способ объявления переменной без указания ее типа.
Примечание. Код этого сайта соответствует правилам руководства по стилю написания на Dart.

Важные понятия

Когда вы будете изучать язык Dart, помните эти факты и понятия:

  • Все, что вы можете поместить в переменную, является объектом, а каждый объект является экземпляром класса. Четные числа, функции и null являются объектами. Все объекты наследуются от класса Object.
  • Хотя Dart строго типизирован, аннотации типов являются необязательными, потому что Dart может выводить типы. В приведенном выше коде число выводится как тип int. Если вы хотите явно сказать, что тип не ожидается, используйте специальный тип dynamic.
  • Dart поддерживает универсальные типы, такие как List<int> (список целых чисел) или List<dynamic> (список объектов любого типа).
  • Dart поддерживает функции верхнего уровня (такие как main()), а также функции, связанные с классом или объектом (статические свойства и методы экземпляра, соответственно). Вы также можете создавать функции внутри функций (вложенные или локальные функции).
  • Точно так же Dart поддерживает переменные верхнего уровня, а также переменные, привязанные к классу или объекту (статические переменные и переменные экземпляра). Переменные экземпляра иногда называют полями или свойствами.
  • В отличие от Java, в Dart нет ключевых слов public, protected и private. Если идентификатор начинается с подчеркивания (_), он является приватным для своей библиотеки. Для получения дополнительной информации см. Библиотеки и видимость.
  • Идентификаторы могут начинаться с буквы или подчеркивания (_), за которыми следует любая комбинация этих символов плюс цифры.
  • Dart имеет как выражения (которые имеют значения времени выполнения), так и операторы (которые не имеют). Например, условный оператор condition ? expr1 : expr2 имеет значение expr1 или expr2. Сравните это с оператором if-else, который не имеет значения. Инструкция часто содержит одно или несколько выражений, но выражение не может напрямую содержать инструкцию.
  • Инструменты Dart могут сообщать о двух видах проблем: предупреждения и ошибки. Предупреждения являются лишь признаками того, что ваш код может не работать, но они не препятствуют выполнению вашей программы. Ошибки могут быть как во время компиляции, так и во время выполнения. Ошибка во время компиляции вообще не позволяет выполнить код; ошибка во время выполнения приводит к возникновению исключения во время выполнения кода.

Ключевые слова

В следующей таблице перечислены слова, которые язык Dart обрабатывает особым образом

abstract 2 dynamic 2 implements 2 show 1
as 2 else import 2 static 2
assert enum in super
async 1 export 2 interface 2 switch
await 3 extends is sync 1
break external 2 library 2 this
case factory 2 mixin 2 throw
catch false new true
class final null try
const finally on 1 typedef 2
continue for operator 2 var
covariant 2 Function 2 part 2 void
default get 2 rethrow while
deferred 2 hide 1 return with
do if set 2 yield 3

Избегайте использования этих слов в качестве идентификаторов. Однако при необходимости ключевые слова, помеченные надстрочными индексами, могут быть идентификаторами:

  • Слова с верхним индексом 1 являются контекстными ключевыми словами, которые имеют значение только в определенных местах. Они действительные идентификаторы везде;
  • Слова с верхним индексом 2 являются встроенными идентификаторами. Чтобы упростить задачу переноса кода JavaScript в Dart, эти ключевые слова являются допустимыми идентификаторами в большинстве мест, но их нельзя использовать в качестве имен классов или типов или в качестве префиксов импорта;
  • Слова с верхним индексом 3 являются новыми, ограниченными зарезервированными словами, относящимися к поддержке асинхронности, которая была добавлена ​​после выпуска Dart 1.0. Вы не можете использовать await или yield в качестве идентификатора в любом теле функции, помеченном async, async * или sync *.
  • Все остальные слова в таблице являются зарезервированными словами, которые не могут быть идентификаторами.

Переменные

Вот пример создания переменной и ее инициализации:

var name = 'Bob';

Переменные хранят ссылки. Переменная с именем name содержит ссылку на объект String со значением «Bob».

Тип переменной name выводится как String, но вы можете изменить этот тип, указав его. Если объект не ограничен одним типом, укажите объект Object или динамический тип dynamic, следуя рекомендациям по проектированию.

dynamic name = 'Bob';

Другой вариант — явно объявить тип, который будет выведен:

String name = 'Bob';
Примечание. На этой странице соблюдаются рекомендации руководства по стилю, касающиеся использования переменных var вместо аннотаций типов для локальных переменных.
Значение по умолчанию

Неинициализированные переменные имеют начальное значение null. Даже переменные с числовыми типами изначально равны нулю, потому что числа — как и все остальное в Dart — являются объектами.

int lineCount;
assert(lineCount == null);
Примечание: производственный код игнорирует вызов assert(). С другой стороны, во время разработки assert(условие) выдает исключение, если условие ложно. Для получения дополнительной информации см. Assert.
Данные final и const

Если вы никогда не собираетесь менять переменную, используйте final или const, либо вместо var, либо в дополнение к типу. Окончательная переменная может быть установлена ​​только один раз; константная переменная — это константа времени компиляции. (Константные переменные неявно являются окончательными.) Последняя переменная верхнего уровня или класса инициализируется при первом использовании.

Примечание. Переменные экземпляра могут быть final, но не const. Переменные final экземпляра должны быть инициализированы до запуска тела конструктора — при объявлении переменной, параметром конструктора или в списке инициализатора конструктора.

Вот пример создания и установки окончательной переменной:

final name = 'Bob'; // Without a type annotation
final String nickname = 'Bobby';

Вы не можете изменить значение окончательной переменной:

name = 'Alice'; // Error: a final variable can only be set once.

Используйте const для переменных, которые вы хотите быть константами во время компиляции. Если переменная const находится на уровне класса, пометьте ее как static const. Когда вы объявляете переменную, присвойте ей значение постоянной времени компиляции, такой как числовой или строковый литерал, константная переменная или результат арифметической операции с постоянными числами:

const bar = 1000000; // Unit of pressure (dynes/cm2)
const double atm = 1.01325 * bar; // Standard atmosphere

Ключевое слово const предназначено не только для объявления константных переменных. Вы также можете использовать его для создания постоянных значений, а также для объявления конструкторов, которые создают постоянные значения. Любая переменная может иметь постоянное значение.

var foo = const [];
final bar = const [];
const baz = []; // Equivalent to `const []`

Вы можете опустить const из инициализирующего выражения объявления const, как для baz выше. Для получения дополнительной информации см. НЕ используйте const с избыточностью.

Вы можете изменить значение неконечной, неконстантной переменной, даже если она имела постоянное значение:

foo = [1, 2, 3]; // Was const []

Вы не можете изменить значение переменной const:

baz = [42]; // Error: Constant variables can't be assigned a value.

Начиная с Dart 2.5, вы можете определять константы, которые используют проверку типов и приведение (is и as), коллекция if и коллекция for и спред — операторы распространения ( и …?):

// Valid compile-time constants as of Dart 2.5.
const Object i = 3; // Where i is a const Object with an int value...
const list = [i as int]; // Use a typecast.
const map = {if (i is int) i: "int"}; // Use is and collection if.
const set = {if (list is List) ...list}; // ...and a spread.

Для получения дополнительной информации об использовании const для создания постоянных значений см. Списки(Lists), Карты(Maps) и Классы(Classes).

Встроенные типы

Язык Dart имеет специальную поддержку для следующих типов:

  • numbers — числа;
  • strings — строки;
  • booleans — булевые значения;
  • lists (also known as arrays) — списки, который также упоминаются массивами;
  • sets — наборы;
  • maps — карты;
  • runes — нужны для выражения символов Юникода в строке;
  • symbols — символы.

Вы можете инициализировать объект любого из этих специальных типов, используя литерал. Например, «это строка» — строковый литерал, true — логический литерал.

Поскольку каждая переменная в Dart ссылается на объект — экземпляр класса — обычно вы можете использовать конструкторы для инициализации переменных. Некоторые из встроенных типов имеют свои собственные конструкторы. Например, вы можете использовать конструктор Map() для создания карты.

Числа — Numbers

Номера дротиков бывают двух видов:

int — целочисленные значения не более 64 бит, в зависимости от платформы. На виртуальной машине Dart значения могут быть от -263 до 263 — 1. В Dart, скомпилированном в JavaScript, используются числа JavaScript, допускающие значения от -253 до 253 — 1.

doube — 64-битные (с двойной точностью) числа с плавающей точкой, как указано в стандарте IEEE 754.

И int, и double являются подтипами num. Тип num включает в себя основные операторы, такие как +, -, / и *, а также, где вы найдете abs(), ceil() и floor(), среди других методов. (Битовые операторы, такие как >>, определены в классе int.) Если num и его подтипы не имеют того, что вы ищете, библиотека dart:math имеет.

Целые числа — это числа без десятичной точки. Вот несколько примеров определения целочисленных литералов:

var x = 1;
var hex = 0xDEADBEEF;

Если число включает в себя десятичную, это double. Вот несколько примеров определения двойных литералов:

double z = 1; // Equivalent to double z = 1.0.
Примечание к версии: До Dart 2.1 было ошибкой использовать целочисленный литерал в двойном контексте.

Вот как вы превращаете строку в число или наоборот:

// String -> int
var one = int.parse('1');
assert(one == 1);

// String -> double
var onePointOne = double.parse('1.1');
assert(onePointOne == 1.1);

// int -> String
String oneAsString = 1.toString();
assert(oneAsString == '1');

// double -> String
String piAsString = 3.14159.toStringAsFixed(2);
assert(piAsString == '3.14');

Тип int определяет традиционные побитовые операторы сдвига (<<, >>), AND (&) и OR (|). Например:

const msPerSecond = 1000;
const secondsUntilRetry = 5;
const msUntilRetry = secondsUntilRetry * msPerSecond;
Строки — Strings

Строка Dart представляет собой последовательность кодовых единиц UTF-16. Вы можете использовать одинарные или двойные кавычки для создания строки:

var s1 = 'Single quotes work well for string literals.';
var s2 = "Double quotes work just as well.";
var s3 = 'It\'s easy to escape the string delimiter.';
var s4 = "It's even easier to use the other delimiter.";

Вы можете поместить значение выражения в строку, используя $ {expression}. Если выражение является идентификатором, вы можете пропустить {}. Чтобы получить строку, соответствующую объекту, Dart вызовет метод объекта toString().

var s = 'string interpolation';
assert('Dart has $s, which is very handy.' ==
    'Dart has string interpolation, ' +
        'which is very handy.');
assert('That deserves all caps. ' +
        '${s.toUpperCase()} is very handy!' ==
    'That deserves all caps. ' +
        'STRING INTERPOLATION is very handy!');
Примечание. Оператор == проверяет, эквивалентны ли два объекта. Две строки эквивалентны, если они содержат одинаковую последовательность единиц кода.

Вы можете объединять строки, используя примыкающие строковые литералы или оператор +:

var s1 = 'String '
    'concatenation'
    " works even over line breaks.";
assert(s1 ==
    'String concatenation works even over '
        'line breaks.');

var s2 = 'The + operator ' + 'works, as well.';
assert(s2 == 'The + operator works, as well.');

Другой способ создания многострочной строки: используйте тройную кавычку с одинарными или двойными кавычками:

var s1 = '''
You can create
multi-line strings like this one.
''';
var s2 = """This is also a
multi-line string.""";

Вы можете создать «сырую» строку, добавив к ней префикс r:

var s = r'In a raw string, not even \n gets special treatment.';

Посмотрите Руны для деталей о том, как выразить символы Unicode в строке.

Литеральные строки являются константами времени компиляции, если любое интерполированное выражение является константой времени компиляции, которая оценивается как нулевое или числовое, строковое или логическое значение.

// These work in a const string.
const aConstNum = 0;
const aConstBool = true;
const aConstString = 'a constant string';

// These do NOT work in a const string.
var aNum = 0;
var aBool = true;
var aString = 'a string';
const aConstList = [1, 2, 3];

const validConstString = '$aConstNum $aConstBool $aConstString';
// const invalidConstString = '$aNum $aBool $aString $aConstList';

Для получения дополнительной информации об использовании строк см. Строки и регулярные выражения.

Булевые значения — Booleans

Для представления логических значений Dart имеет тип с именем bool. Только два объекта имеют тип bool: логические литералы true и false, которые являются константами времени компиляции.

Безопасность типов Dart означает, что вы не можете использовать код вроде if (nonbooleanValue) или assert (nonbooleanValue). Вместо этого явно проверьте значения, как тут:

// Check for an empty string.
var fullName = '';
assert(fullName.isEmpty);

// Check for zero.
var hitPoints = 0;
assert(hitPoints <= 0);

// Check for null.
var unicorn;
assert(unicorn == null);

// Check for NaN.
var iMeantToDoThis = 0 / 0;
assert(iMeantToDoThis.isNaN);
Списки - Lists

Возможно, самая распространенная коллекция почти в каждом языке программирования - это массив или упорядоченная группа объектов. В Dart массивы являются объектами List, поэтому большинство людей просто называют их списками.

var list = [1, 2, 3];
Примечание: Dart делает вывод, что список имеет тип List <int>. Если вы попытаетесь добавить нецелые объекты в этот список, анализатор или среда выполнения выдаст ошибку. Для получения дополнительной информации читайте о выводе типа.

Списки используют индексацию с нуля, где 0 - это индекс первого элемента, а list.length - 1 - это индекс последнего элемента. Вы можете получить длину списка и ссылаться на элементы списка так же, как в JavaScript:

var list = [1, 2, 3];
assert(list.length == 3);
assert(list[1] == 2);

list[1] = 1;
assert(list[1] == 1);

Чтобы создать список, который является константой времени компиляции, добавьте const перед литералом списка:

var constantList = const [1, 2, 3];
// constantList[1] = 1; // Uncommenting this causes an error.

В Dart 2.3 появился оператор распространения spread (...) и оператор распространения с нулевым значением (...?), Которые обеспечивают краткий способ вставки нескольких элементов в коллекцию.

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

var list = [1, 2, 3];
var list2 = [0, ...list];
assert(list2.length == 4);

Если выражение справа от оператора распространения может быть нулевым, вы можете избежать исключений, используя оператор распространения с нулевым значением (...?):

var list;
var list2 = [0, ...?list];
assert(list2.length == 1);

Для получения более подробной информации и примеров использования оператора распространения, см. Предложение оператора распространения.

В Dart 2.3 также была представлена collection if и collection for, которые можно использовать для построения коллекций с использованием условий (if) и повторения (for).

Вот пример использования collection if создания списка с тремя или четырьмя элементами в нем:

var nav = [
  'Home',
  'Furniture',
  'Plants',
  if (promoActive) 'Outlet'
];

Вот пример использования collection if для управления элементами списка перед их добавлением в другой список:

var listOfInts = [1, 2, 3];
var listOfStrings = [
  '#0',
  for (var i in listOfInts) '#$i'
];
assert(listOfStrings[1] == '#1');

Дополнительные сведения и примеры использования коллекции if и for см. В предложении коллекции потоков управления.

Тип List имеет много удобных методов для работы со списками. Для получения дополнительной информации о списках см. Генераторы Generics и коллекции Collections.

Наборы - Sets

Набор в Dart - это неупорядоченная коллекция уникальных предметов. Поддержка Dart для наборов обеспечивается литералами set и типом Set.

Примечание к версии: Хотя тип Set всегда был основной частью Dart, литералы набора были введены в Dart 2.2.

Вот простой набор Dart, созданный с использованием литерала набора:

var halogens = {'fluorine', 'chlorine', 'bromine', 'iodine', 'astatine'};
Примечание: Dart делает вывод, что данные в halogens имеют тип Set <String>. Если вы попытаетесь добавить неправильный тип значения в набор, анализатор или среда выполнения выдаст ошибку. Для получения дополнительной информации читайте о выводе типа.

Чтобы создать пустой набор, используйте {} с предшествующим аргументом типа или присвойте {} переменной типа Set:

var names = {};
// Set names = {}; // This works, too.
// var names = {}; // Creates a map, not a set.
Набор или карта? Синтаксис для литералов карты аналогичен синтаксису для литералов наборов. Поскольку литералы карты были на первом месте, {} по умолчанию использует тип карты Map. Если вы забудете аннотацию типа {} или переменную, которой она назначена, Dart создаст объект типа Map <dynamic, dynamic>.

Добавьте элементы в существующий набор, используя методы add() или addAll():

var elements = {};
elements.add('fluorine');
elements.addAll(halogens);

Используйте .length, чтобы получить количество предметов в наборе:

var elements = {};
elements.add('fluorine');
elements.addAll(halogens);
assert(elements.length == 5);

Чтобы создать набор, который является константой времени компиляции, добавьте const перед литералом набора:

final constantSet = const {
  'fluorine',
  'chlorine',
  'bromine',
  'iodine',
  'astatine',
};
// constantSet.add('helium'); // Uncommenting this causes an error.

Начиная с Dart 2.3, наборы поддерживают операторы распространения (... и ...?) И коллекции if и for, как это делают списки. Для получения дополнительной информации см. Обсуждения оператора распространения списка и оператора сбора списка.

Для получения дополнительной информации о наборах см. Обобщения Generics и наборы Sets .

Карты - Maps

В общем, карта - это объект, который связывает ключи и значения. И ключи, и значения могут быть объектами любого типа. Каждый ключ встречается только один раз, но вы можете использовать одно и то же значение несколько раз. Поддержка Dart для карт обеспечивается литералами карты и типом Map.

var gifts = {
  // Key:    Value
  'first': 'partridge',
  'second': 'turtledoves',
  'fifth': 'golden rings'
};
var nobleGases = {
  2: 'helium',
  10: 'neon',
  18: 'argon',
};
Примечание: Dart делает вывод, что gifts имеет тип Map <String, String>, а nobleGases имеет тип Map <int, String>. Если вы попытаетесь добавить неверный тип значения на карту, анализатор или среда выполнения выдаст ошибку. Для получения дополнительной информации читайте о выводе типа.

Вы можете создать те же объекты, используя конструктор Map:

var gifts = Map();
gifts['first'] = 'partridge';
gifts['second'] = 'turtledoves';
gifts['fifth'] = 'golden rings';

var nobleGases = Map();
nobleGases[2] = 'helium';
nobleGases[10] = 'neon';
nobleGases[18] = 'argon';
Примечание. Возможно, вы ожидаете увидеть new Map() вместо простой Map(). Начиная с Dart 2, ключевое слово new является необязательным. Для получения дополнительной информации см. Использование конструкторов.

Добавьте новую пару ключ-значение на существующую карту так же, как в JavaScript:

var gifts = {'first': 'partridge'};
gifts['fourth'] = 'calling birds'; // Add a key-value pair

Получить значение из карты так же, как в JavaScript:

var gifts = {'first': 'partridge'};
assert(gifts['first'] == 'partridge');

Если вы ищете ключ, которого нет на карте, вы получите нулевое взамен:

var gifts = {'first': 'partridge'};
assert(gifts['fifth'] == null);

Используйте .length, чтобы получить количество пар ключ-значение на карте:

var gifts = {'first': 'partridge'};
gifts['fourth'] = 'calling birds';
assert(gifts.length == 2);

Чтобы создать карту с постоянной времени компиляции, добавьте const перед литералом карты:

final constantMap = const {
  2: 'helium',
  10: 'neon',
  18: 'argon',
};

// constantMap[2] = 'Helium'; // Uncommenting this causes an error.

Начиная с Dart 2.3, карты поддерживают операторы распространения (... и ...?) И коллекции, если и для, как списки. Для получения подробной информации и примеров см. Предложение оператора распространения и предложение коллекций потока управления.

Для получения дополнительной информации о картах см. Обобщения Generics и Карты Maps.

Руны - Runes

В Dart руны - это кодовые позиции UTF-32 строки.

Unicode определяет уникальное числовое значение для каждой буквы, цифры и символа, используемых во всех мировых системах письма. Поскольку строка Dart представляет собой последовательность единиц кода UTF-16, для выражения 32-битных значений Unicode в строке требуется специальный синтаксис.

Обычный способ выражения кодовой позиции Unicode - это \uXXXX, где XXXX - это шестнадцатеричное 4-значное значение. Например, символ сердца (♥) - это \u2665. Чтобы указать более или менее 4 шестнадцатеричных цифр, поместите значение в фигурные скобки. Например, смеющийся смайлик (?) - это \u{1f600}.

Класс String имеет несколько свойств, которые вы можете использовать для извлечения информации о рунах. Свойства codeUnitAt и codeUnit возвращают 16-битные единицы кода. Используйте свойство runes, чтобы получить руны строки.

Следующий пример иллюстрирует взаимосвязь между рунами, 16-разрядными кодовыми единицами и 32-разрядными кодовыми позициями

main() {
  var clapping = '\u{1f44f}';
  print(clapping);
  print(clapping.codeUnits);
  print(clapping.runes.toList());

  Runes input = new Runes(
      '\u2665  \u{1f605}  \u{1f60e}  \u{1f47b}  \u{1f596}  \u{1f44d}');
  print(new String.fromCharCodes(input));
}
Примечание. Будьте осторожны при манипулировании рунами с помощью операций со списком. Этот подход может легко сломаться, в зависимости от конкретного языка, набора символов и операции. Для получения дополнительной информации см. Как перевернуть строку в дротике? Переполнение стека.
Символы - Symbols

Объект Symbol представляет оператор или идентификатор, объявленный в программе Dart. Возможно, вам никогда не понадобится использовать символы, но они неоценимы для API, которые ссылаются на идентификаторы по имени, потому что минификация меняет имена идентификаторов, но не символы идентификаторов.

Чтобы получить символ для идентификатора, используйте символьный литерал, который просто пишется, как #, за которым следует идентификатор:

#radix
#bar

Символьные литералы являются константами времени компиляции.

Функции

Dart - это настоящий объектно-ориентированный язык, поэтому даже функции являются объектами и имеют тип Function.

Это означает, что функции могут быть назначены переменным или переданы в качестве аргументов другим функциям. Вы также можете вызвать экземпляр класса Dart, как если бы это была функция. Для получения дополнительной информации см. Callable классы.

Вот пример реализации функции:

bool isNoble(int atomicNumber) {
  return _nobleGases[atomicNumber] != null;
}

Хотя Effective Dart рекомендует аннотации типов для общедоступных API-интерфейсов, функция все равно работает, если вы опустите типы:

isNoble(atomicNumber) {
  return _nobleGases[atomicNumber] != null;
}

Для функций, которые содержат только одно выражение, вы можете использовать сокращенный синтаксис:

bool isNoble(int atomicNumber) => _nobleGases[atomicNumber] != null;

Синтаксис => expr является сокращением для {return expr; }. Обозначение => иногда называют синтаксисом стрелки.

Примечание. Между стрелкой (=>) и точкой с запятой (;) может появляться только выражение, но не утверждение. Например, вы не можете поместить туда оператор if, но вы можете использовать условное выражение.

Функция может иметь два типа параметров: обязательные и необязательные. Обязательные параметры перечислены первыми, а затем все необязательные параметры. Необязательные параметры могут быть именованными или позиционными.

Примечание. Некоторые API, в частности конструкторы виджетов Flutter, используют только именованные параметры, даже для обязательных параметров. Смотрите следующий раздел для деталей.
Дополнительные параметры

Необязательные параметры могут быть именованными или позиционными, но не обоими.

Именованные параметры

При вызове функции вы можете указать именованные параметры, используя paramName: value. Например:

enableFlags(bold: true, hidden: false);

При определении функции используйте {param1, param2,…} для указания именованных параметров:

/// Sets the [bold] and [hidden] flags ...
void enableFlags({bool bold, bool hidden}) {...}

Хотя именованные параметры являются своего рода необязательным параметром, их можно аннотировать с помощью @required, чтобы указать, что параметр является обязательным, - что пользователи должны предоставить значение для параметра. Например:

const Scrollbar({Key key, @required Widget child})

Если кто-то пытается создать Scrollbar без указания дочернего аргумента child, то анализатор сообщает о проблеме.

Чтобы использовать аннотацию @required, зависит от пакета meta и импорта: package:meta/meta.dart.

Позиционные параметры

Обертывание набора параметров функции в [ ] помечает их как необязательные позиционные параметры:

String say(String from, String msg, [String device]) {
  var result = '$from says $msg';
  if (device != null) {
    result = '$result with a $device';
  }
  return result;
}

Вот пример вызова этой функции без необязательного параметра:

assert(say('Bob', 'Howdy') == 'Bob says Howdy');

И вот пример вызова этой функции с третьим параметром:

assert(say('Bob', 'Howdy', 'smoke signal') ==
    'Bob says Howdy with a smoke signal');
Значения параметров по умолчанию

Ваша функция может использовать = для определения значений по умолчанию для именованных и позиционных параметров. Значения по умолчанию должны быть константами времени компиляции. Если значение по умолчанию не указано, значением по умолчанию является null.

Вот пример установки значений по умолчанию для именованных параметров:

/// Sets the [bold] and [hidden] flags ...
void enableFlags({bool bold = false, bool hidden = false}) {...}

// bold will be true; hidden will be false.
enableFlags(bold: true);
Примечание об устаревании: Старый код может использовать двоеточие (:) вместо = для установки значений по умолчанию именованных параметров. Причина в том, что изначально, только : был поддержан для именованных параметров. Эта поддержка может быть устаревшей, поэтому мы рекомендуем использовать знак =, чтобы указать значения по умолчанию.

В следующем примере показано, как установить значения по умолчанию для позиционных параметров:

String say(String from, String msg,
    [String device = 'carrier pigeon', String mood]) {
  var result = '$from says $msg';
  if (device != null) {
    result = '$result with a $device';
  }
  if (mood != null) {
    result = '$result (in a $mood mood)';
  }
  return result;
}

assert(say('Bob', 'Howdy') ==
    'Bob says Howdy with a carrier pigeon');

Вы также можете передать списки или карты в качестве значений по умолчанию. В следующем примере определяется функция doStuff(), которая задает список по умолчанию для параметра list и карту по умолчанию для параметра gifts.

void doStuff(
    {List list = const [1, 2, 3],
    Map gifts = const {
      'first': 'paper',
      'second': 'cotton',
      'third': 'leather'
    }}) {
  print('list:  $list');
  print('gifts: $gifts');
}
The main() function

Каждое приложение должно иметь функцию main() верхнего уровня, которая служит точкой входа в приложение. Функция main() возвращает void и имеет необязательный параметр List <String> для аргументов.

Вот пример функции main() для веб-приложения:

void main() {
  querySelector('#sample_text_id')
    ..text = 'Click me!'
    ..onClick.listen(reverseText);
}
Примечание. Синтаксис .. в предыдущем коде называется каскадом cascade . С помощью каскадов вы можете выполнять несколько операций над элементами одного объекта.

Вот пример функции main() для приложения командной строки, которое принимает аргументы:

// Run the app like this: dart args.dart 1 test
void main(List arguments) {
  print(arguments);

  assert(arguments.length == 2);
  assert(int.parse(arguments[0]) == 1);
  assert(arguments[1] == 'test');
}

Вы можете использовать библиотеку args для определения и анализа аргументов командной строки.

Функции как первоклассные объекты

Вы можете передать функцию в качестве параметра другой функции. Например:

void printElement(int element) {
  print(element);
}

var list = [1, 2, 3];

// Pass printElement as a parameter.
list.forEach(printElement);

Вы также можете назначить функцию переменной, например:

var loudify = (msg) => '!!! ${msg.toUpperCase()} !!!';
assert(loudify('hello') == '!!! HELLO !!!');

В этом примере используется анонимная функция. Подробнее об этом в следующем разделе.

Анонимные функции

Большинство функций имеют имена, такие как main() или printElement(). Вы также можете создать анонимную функцию, называемую анонимной функцией, иногда лямбда-выражением или замыканием. Вы можете назначить анонимную функцию переменной, чтобы, например, вы могли добавить или удалить ее из коллекции.

Анонимная функция выглядит аналогично именованной функции - ноль или более параметров, разделенных запятыми и необязательными аннотациями типов, заключаются в круглые скобки.

Следующий блок кода содержит тело функции:

([[Type] param1[, …]]) {
  codeBlock;
};

В следующем примере определяется анонимная функция с нетипизированным параметром item. Функция, вызываемая для каждого элемента в списке, печатает строку, содержащую значение по указанному индексу.

var list = ['apples', 'bananas', 'oranges'];
list.forEach((item) {
  print('${list.indexOf(item)}: $item');
});
void main() {
  var list = ['apples', 'bananas', 'oranges'];
  list.forEach((item) {
    print('${list.indexOf(item)}: $item');
  });
}

Если функция содержит только один оператор, вы можете сократить его, используя обозначение стрелки. Вставьте следующую строку в DartPad и нажмите «Выполнить», чтобы убедиться, что она функционально эквивалентна.

list.forEach(
    (item) => print('${list.indexOf(item)}: $item'));
Лексическая область

Dart - это язык с лексической областью, что означает, что область видимости переменных определяется статически, просто компоновкой кода. Вы можете «следовать за фигурными скобками наружу», чтобы увидеть, находится ли переменная в области видимости.

Вот пример вложенных функций с переменными на каждом уровне области действия:

bool topLevel = true;

void main() {
  var insideMain = true;

  void myFunction() {
    var insideFunction = true;

    void nestedFunction() {
      var insideNestedFunction = true;

      assert(topLevel);
      assert(insideMain);
      assert(insideFunction);
      assert(insideNestedFunction);
    }
  }
}

Обратите внимание, как nestedFunction() может использовать переменные с каждого уровня, вплоть до верхнего уровня.

Лексические крышки

Замыкание - это объект функции, который имеет доступ к переменным в своей лексической области видимости, даже если функция используется за пределами своей исходной области видимости.

Функции могут закрывать переменные, определенные в окружающих областях. В следующем примере makeAdder() захватывает переменную addBy. Куда бы ни возвращалась возвращаемая функция, она запоминает addBy.

/// Returns a function that adds [addBy] to the
/// function's argument.
Function makeAdder(num addBy) {
  return (num i) => addBy + i;
}

void main() {
  // Create a function that adds 2.
  var add2 = makeAdder(2);

  // Create a function that adds 4.
  var add4 = makeAdder(4);

  assert(add2(3) == 5);
  assert(add4(3) == 7);
}
Проверка функций на равенство

Вот пример тестирования функций верхнего уровня, статических методов и методов экземпляра на равенство:

void foo() {} // A top-level function

class A {
  static void bar() {} // A static method
  void baz() {} // An instance method
}

void main() {
  var x;

  // Comparing top-level functions.
  x = foo;
  assert(foo == x);

  // Comparing static methods.
  x = A.bar;
  assert(A.bar == x);

  // Comparing instance methods.
  var v = A(); // Instance #1 of A
  var w = A(); // Instance #2 of A
  var y = w;
  x = w.baz;

  // These closures refer to the same instance (#2),
  // so they're equal.
  assert(y.baz == x);

  // These closures refer to different instances,
  // so they're unequal.
  assert(v.baz != w.baz);
}
Возвращаемые значения

Все функции возвращают значение. Если возвращаемое значение не указано, инструкция return null; неявно добавляется к телу функции.

foo() {}

assert(foo() == null);

Операторы

Dart определяет операторы, показанные в следующей таблице. Вы можете переопределить многие из этих операторов, как описано в разделе «Переопределяемые операторы».

Описание Оператор
одинарный постфикс expr++ expr-- () [] . ?.
одинарный префикс -expr! expr ~ expr ++ expr --expr
мультипликативный / % ~/
адитивный+ -
сдвиг << >> >>>
побитовое И &
побитовый XOR ^
побитовое ИЛИ |
реляционное и испытание типа = > <= < as is is!
равенство == !=
логическое И &&
логическое ИЛИ ||
если null ??
условный expr1 ? expr2 : expr3
каскад ..
назначение = * = / = + = - = = ^ и т.д.
Предупреждение. Приоритет оператора - это пример поведения анализатора Dart. Для окончательных ответов обратитесь к грамматике в спецификации языка Dart.

Когда вы используете операторы, вы создаете выражения. Вот несколько примеров выражений операторов:

a++
a + b
a = b
a == b
c ? a : b
a is T

В таблице операторов каждый оператор имеет более высокий приоритет, чем операторы в строках, следующих за ним. Например, мультипликативный оператор % имеет более высокий приоритет, чем (и, следовательно, выполняется раньше) оператор равенства ==, который имеет более высокий приоритет, чем логический оператор AND &&. Этот приоритет означает, что следующие две строки кода выполняются одинаково:

// Parentheses improve readability.
if ((n % i == 0) && (d % i == 0)) ...

// Harder to read, but equivalent.
if (n % i == 0 && d % i == 0) ...
Предупреждение. Для операторов, работающих с двумя операндами, крайний левый операнд определяет, какая версия оператора используется. Например, если у вас есть объект Vector и объект Point, aVector + aPoint использует версию Vector вектора +.
Арифметические операторы

Dart поддерживает обычные арифметические операторы, как показано в следующей таблице.

ОператорЗначение
+добавлять
вычитать
-expr унарный минус, также известный как отрицание
*умножение
/деление
~/ деление, возвращая целочисленный результат
% получить остаток от целочисленного деления

Пример:

assert(2 + 3 == 5);
assert(2 - 3 == -1);
assert(2 * 3 == 6);
assert(5 / 2 == 2.5); // Result is a double
assert(5 ~/ 2 == 2); // Result is an int
assert(5 % 2 == 1); // Remainder

assert('5/2 = ${5 ~/ 2} r ${5 % 2}' == '5/2 = 2 r 1');

Dart также поддерживает префиксные и постфиксные операторы увеличения и уменьшения.

ОператорЗначение
++var var = var + 1 (значение выражения var + 1)
var++ var = var + 1 (значение выражения var)
--var var = var - 1 (значение выражения var - 1)
var--var = var - 1 (значение выражения var)

Пример:

var a, b;

a = 0;
b = ++a; // Increment a before b gets its value.
assert(a == b); // 1 == 1

a = 0;
b = a++; // Increment a AFTER b gets its value.
assert(a != b); // 1 != 0

a = 0;
b = --a; // Decrement a before b gets its value.
assert(a == b); // -1 == -1

a = 0;
b = a--; // Decrement a AFTER b gets its value.
assert(a != b); // -1 != 0
Равенство и реляционные операторы

В следующей таблице перечислены значения равенства и реляционных операторов.

ОператорЗначение
== равное, см. обсуждение ниже
!= не равный
>больше, чем
<меньше, чем
>=больше, чем или равно
<=меньше, чем или равно

Чтобы проверить, представляют ли два объекта x и y одно и то же, используйте оператор ==. (В редком случае, когда вам нужно знать, являются ли два объекта одним и тем же объектом, используйте вместо него функцию identifier().) Вот как работает оператор ==:

  1. Если x или y равен null, верните true, если оба имеют значение null, и false, если только один равен нулю.
  2. Вернуть результат вызова метода x. == (y). (Это верно, операторы, такие как ==, являются методами, которые вызываются для их первого операнда. Вы даже можете переопределить многие операторы, включая ==, как вы увидите в переопределенных операторах.)

Вот пример использования каждого из операторов равенства и отношений:

assert(2 == 2);
assert(2 != 3);
assert(3 > 2);
assert(2 < 3);
assert(3 >= 3);
assert(2 <= 3);
Тип тестовых операторов

Как as, is, и is! Операторы удобны для проверки типов во время выполнения.

ОператорЗначение
as Typecast (также используется для указания префиксов библиотеки)
is True, если объект имеет указанный тип
is! False, если объект имеет указанный тип

Результатом obj is T - true, если obj реализует интерфейс, указанный в T. Например, obj is Object - всегда true.

Используйте оператор as для приведения объекта к определенному типу. В общем, вы должны использовать его как сокращение для теста is для объекта, за которым следует выражение, использующее этот объект. Например, рассмотрим следующий код:

if (emp is Person) {
  // Type check
  emp.firstName = 'Bob';
}

Вы можете сделать код короче, используя оператор as:

(emp as Person).firstName = 'Bob';
Примечание. Код не эквивалентен. Если emp является нулем или не Person, первый пример (с is) ничего не делает; вторая (с as) выдает исключение.
Операторы присваивания

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

// Assign value to a
a = value;
// Assign value to b if b is null; otherwise, b stays the same
b ??= value;

Составные операторы присваивания, такие как +=, объединяют операцию с присваиванием.

=–=/=%=>>=^=
+=*=~/=<<=&=|=

Вот как работают составные операторы присваивания:

Сложное назначениеЭквивалентное выражение
Для оператора op: a op= b a = a op b
Пример: a += b a = a + b

В следующем примере используются операторы присваивания и составного присваивания:

var a = 2; // Assign using =
a *= 3; // Assign and multiply: a = a * 3
assert(a == 6);
Логические операторы

Вы можете инвертировать или комбинировать логические выражения, используя логические операторы.

ОператорЗначение
!exprинвертирует следующее выражение (изменяет ложь на истину и наоборот)
|| логическое ИЛИ
&& логическое И

Вот пример использования логических операторов:

if (!done && (col == 0 || col == 3)) {
  // ...Do something...
} 
Побитовые и сдвиговые операторы

Вы можете манипулировать отдельными битами чисел в Dart. Обычно вы используете эти побитовые и сдвиговые операторы с целыми числами.

ОператорЗначение
&AND
|OR
^XOR
~expr Унарное побитовое дополнение (0 становятся 1; 1 становятся 0)
<<Сдвиг влево
>>Сдвиг вправо

Вот пример использования побитовых и сдвиговых операторов:

final value = 0x22;
final bitmask = 0x0f;

assert((value & bitmask) == 0x02); // AND
assert((value & ~bitmask) == 0x20); // AND NOT
assert((value | bitmask) == 0x2f); // OR
assert((value ^ bitmask) == 0x2d); // XOR
assert((value << 4) == 0x220); // Shift left
assert((value >> 4) == 0x02); // Shift right
Условные выражения

В Dart есть два оператора, которые позволяют вам кратко оценить выражения, для которых в противном случае могут потребоваться операторы if-else:

условие ? expr1: expr2

Если условие истинно, вычисляет expr1 (и возвращает его значение); в противном случае вычисляет и возвращает значение expr2.

expr1 ?? expr2

Если expr1 не равен NULL, возвращает его значение; в противном случае вычисляет и возвращает значение expr2.

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

var visibility = isPublic ? 'public' : 'private';

Если логическое выражение проверяет наличие нуля, рассмотрите возможность использования ??.

String playerName(String name) => name ?? 'Guest';

Предыдущий пример мог быть написан как минимум двумя другими способами, но не так кратко:

// Slightly longer version uses ?: operator.
String playerName(String name) => name != null ? name : 'Guest';

// Very long version uses if-else statement.
String playerName(String name) {
  if (name != null) {
    return name;
  } else {
    return 'Guest';
  }
}
Каскадная запись (..)

Каскады (..) позволяют выполнять последовательность операций над одним и тем же объектом. В дополнение к вызовам функций вы также можете получить доступ к полям этого же объекта. Это часто избавляет вас от необходимости создавать временную переменную и позволяет писать более гибкий код.

Рассмотрим следующий код:

querySelector('#confirm') // Get an object.
  ..text = 'Confirm' // Use its members.
  ..classes.add('important')
  ..onClick.listen((e) => window.alert('Confirmed!'));

Первый вызов метода querySelector() возвращает объект селектора. Код, который следует за каскадной нотацией, работает с этим объектом селектора, игнорируя любые последующие значения, которые могут быть возвращены.

Предыдущий пример эквивалентен:

var button = querySelector('#confirm');
button.text = 'Confirm';
button.classes.add('important');
button.onClick.listen((e) => window.alert('Confirmed!'));

Вы также можете вкладывать свои каскады. Например:

final addressBook = (AddressBookBuilder()
      ..name = 'jenny'
      ..email = 'jenny@example.com'
      ..phone = (PhoneNumberBuilder()
            ..number = '415-555-0100'
            ..label = 'home')
          .build())
    .build();

Будьте осторожны, чтобы построить каскад на функции, которая возвращает реальный объект. Например, следующий код завершается ошибкой:

var sb = StringBuffer();
sb.write('foo')
  ..write('bar'); // Error: method 'write' isn't defined for 'void'.

Вызов sb.write() возвращает void, и вы не можете создать каскад void.

Примечание. Строго говоря, нотация «двойная точка» для каскадов не является оператором. Это всего лишь часть синтаксиса Dart.
Другие операторы

Вы видели большинство оставшихся операторов в других примерах:

ОператорИмяЗначение
( ) Применение функции Представляет вызов функции
[ ] Доступ к списку Относится к значению по указанному индексу в списке
. Доступ к свойствуОтносится к свойству выражения. Пример: foo.bar выбирает bar свойство из выражения foo
? . Условный доступ к свойствуКак и . , Но самый левый операнд может быть нулевым. Пример: foo? .bar выбирает bar свойство из выражения foo, если foo не равно нулю (в этом случае значение foo? .bar равно нулю)

Для получения дополнительной информации об операторах . , ?. И .. см. Классы.

Операторы управления потоком

Вы можете контролировать поток своего кода Dart, используя любое из следующих конструкций:

  • if и else
  • циклы for
  • циклы while и do-while
  • break и continue
  • switch и case
  • assert

Вы также можете влиять на поток управления, используя try-catch и throw, как описано в разделе «Исключения».

Условные операторы if и else

Dart поддерживает операторы if с дополнительными операторами else, как показано в следующем примере. Также см. Условные выражения.

if (isRaining()) {
  you.bringRainCoat();
} else if (isSnowing()) {
  you.wearJacket();
} else {
  car.putTopDown();
}

В отличие от JavaScript, условия должны использовать логические значения, больше ничего. Посмотрите тип данных Booleans для получения дополнительной информации.

Циклы for

Вы можете выполнять итерации со стандартным циклом for. Например:

var message = StringBuffer('Dart is fun');
for (var i = 0; i < 5; i++) {
  message.write('!');
}

Замыкания внутри циклов for в Dart фиксируют значение индекса, избегая распространенной ошибки, обнаруженной в JavaScript. Например, рассмотрим:

var callbacks = [];
for (var i = 0; i < 2; i++) {
  callbacks.add(() => print(i));
}
callbacks.forEach((c) => c());

Выход 0, а затем 1, как и ожидалось. Напротив, в примере будет напечатано 2, а затем 2 в JavaScript.

Если объект, который вы перебираете, является Iterable, вы можете использовать метод forEach(). Использование forEach() является хорошим вариантом, если вам не нужно знать текущий счетчик итераций:

candidates.forEach((candidate) => candidate.interview());

Итерируемые классы, такие как List и Set, также поддерживают форму итерации for-in:

var collection = [0, 1, 2];
for (var x in collection) {
  print(x); // 0 1 2
}
Циклы While и do-while

Цикл while оценивает условие перед циклом:

while (!isDone()) {
  doSomething();
}

Цикл do-while оценивает условие после цикла:

do {
  printLine();
} while (!atEndOfPage());
Конструкции break и continue

Используйте break, чтобы остановить цикл:

while (true) {
  if (shutDownRequested()) break;
  processIncomingRequests();
}

Используйте continue, чтобы перейти к следующей итерации цикла:

for (int i = 0; i < candidates.length; i++) {
  var candidate = candidates[i];
  if (candidate.yearsExperience < 5) {
    continue;
  }
  candidate.interview();
}

Вы можете написать этот пример по-другому, если вы используете Iterable, такой как список или набор:

candidates
    .where((c) => c.yearsExperience >= 5)
    .forEach((c) => c.interview());
Конструкции switch и case

Операторы switch в Dart сравнивают целочисленные, строковые или константы времени компиляции, используя ==. Все сравниваемые объекты должны быть экземплярами одного и того же класса (а не каких-либо его подтипов), и класс не должен переопределять ==. Перечисленные типы хорошо работают в операторах switch.

Примечание: операторы switch в Dart предназначены для ограниченных обстоятельств, таких как переводчики или сканеры.

Каждое непустое предложение case, как правило, заканчивается оператором break. Другими допустимыми способами завершить непустое предложение case являются операторы continue, throw или return.

Используйте предложение по умолчанию для выполнения кода, когда не найдено ни одного предложения case:

var command = 'OPEN';
switch (command) {
  case 'CLOSED':
    executeClosed();
    break;
  case 'PENDING':
    executePending();
    break;
  case 'APPROVED':
    executeApproved();
    break;
  case 'DENIED':
    executeDenied();
    break;
  case 'OPEN':
    executeOpen();
    break;
  default:
    executeUnknown();
}

В следующем примере в операторе case пропущен оператор break, что приводит к ошибке:

var command = 'OPEN';
switch (command) {
  case 'OPEN':
    executeOpen();
    // ERROR: Missing break

  case 'CLOSED':
    executeClosed();
    break;
}

Тем не менее, Dart поддерживает пустые предложения case, допуская форму провала:

var command = 'CLOSED';
switch (command) {
  case 'CLOSED': // Empty case falls through.
  case 'NOW_CLOSED':
    // Runs for both CLOSED and NOW_CLOSED.
    executeNowClosed();
    break;
}

Если вы действительно хотите потерпеть неудачу, вы можете использовать оператор continue и метку:

var command = 'CLOSED';
switch (command) {
  case 'CLOSED':
    executeClosed();
    continue nowClosed;
  // Continues executing at the nowClosed label.

  nowClosed:
  case 'NOW_CLOSED':
    // Runs for both CLOSED and NOW_CLOSED.
    executeNowClosed();
    break;
}

Предложение case может иметь локальные переменные, которые видны только внутри области действия этого предложения.

Утверждения assert

Во время разработки используйте оператор утверждения - assert (condition, optionalMessage); - нарушить нормальное выполнение, если логическое условие ложно. Вы можете найти примеры утверждений в течение всего данного тура. Вот еще немного:

// Make sure the variable has a non-null value.
assert(text != null);

// Make sure the value is less than 100.
assert(number < 100);

// Make sure this is an https URL.
assert(urlString.startsWith('https'));

Чтобы прикрепить сообщение к утверждению, добавьте строку в качестве второго аргумента для утверждения assert.

assert(urlString.startsWith('https'),
    'URL ($urlString) should start with "https".');

Первым аргументом assert может быть любое выражение, которое разрешается в логическое значение. Если значение равно false, утверждение не выполняется и генерируется исключение (AssertionError).

Когда именно работают утверждения? Это зависит от используемых вами инструментов и инфраструктуры:

  • Flutter включает утверждения в режиме отладки.
  • Инструменты только для разработки, такие как dartdevc, обычно включают утверждения по умолчанию.
  • Некоторые инструменты, такие как dart и dart2js, поддерживают утверждения через флаг командной строки: --enable-asserts.

В рабочем коде утверждения игнорируются, а аргументы assert не оцениваются.

Исключения

Ваш код Dart может генерировать и ловить исключения. Исключением являются ошибки, указывающие на то, что произошло что-то неожиданное. Если исключение не обнаружено, изолятор, вызвавший исключение, приостанавливается, и обычно изолят и его программа завершаются.

В отличие от Java, все исключения Dart являются непроверенными исключениями. Методы не объявляют, какие исключения они могут генерировать, и вы не обязаны перехватывать какие-либо исключения.

Dart предоставляет типы Exception и Error, а также множество предопределенных подтипов. Вы можете, конечно, определить свои собственные исключения. Тем не менее, программы Dart могут выбрасывать любой ненулевой объект - не только объекты Exception и Error - в качестве исключения.

Throw

Вот пример выброса или повышения риска исключения:

throw FormatException('Expected at least 1 section');

Вы также можете бросать произвольные объекты:

throw 'Out of llamas!';
Примечание. Код производственного качества обычно генерирует типы, в которых реализована ошибка Error или исключение Exception.

Поскольку генерирование исключения является выражением, вы можете генерировать исключения в операторах =>, а также в любом другом месте, где разрешены выражения:

void distanceTo(Point other) => throw UnimplementedError();
Catch

Поймать или захватить исключение останавливает распространение исключения (если вы не сбросите исключение). Поймать исключение дает вам возможность обработать это:

try {
  breedMoreLlamas();
} on OutOfLlamasException {
  buyMoreLlamas();
}

Для обработки кода, который может генерировать более одного типа исключений, вы можете указать несколько предложений catch. Первое предложение catch, соответствующее типу брошенного объекта, обрабатывает исключение. Если в предложении catch не указан тип, это предложение может обрабатывать любой тип брошенного объекта:

try {
  breedMoreLlamas();
} on OutOfLlamasException {
  // A specific exception
  buyMoreLlamas();
} on Exception catch (e) {
  // Anything else that is an exception
  print('Unknown exception: $e');
} catch (e) {
  // No specified type, handles all
  print('Something really unknown: $e');
}

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

Вы можете указать один или два параметра для catch(). Первое - это исключение, а второе - трассировка стека (объект StackTrace).

try {
  // ···
} on Exception catch (e) {
  print('Exception details:\n $e');
} catch (e, s) {
  print('Exception details:\n $e');
  print('Stack trace:\n $s');
}

Чтобы частично обработать исключение и разрешить его распространение, используйте ключевое слово rethrow.

void misbehave() {
  try {
    dynamic foo = true;
    print(foo++); // Runtime error
  } catch (e) {
    print('misbehave() partially handled ${e.runtimeType}.');
    rethrow; // Allow callers to see the exception.
  }
}

void main() {
  try {
    misbehave();
  } catch (e) {
    print('main() finished handling ${e.runtimeType}.');
  }
}
Finally

Чтобы убедиться, что какой-то код выполняется независимо от того, было ли выброшено исключение, используйте предложение finally. Если ни одно предложение catch не соответствует исключению, исключение распространяется после выполнения предложения finally:

try {
  breedMoreLlamas();
} finally {
  // Always clean up, even if an exception is thrown.
  cleanLlamaStalls();
}

Предложение finally запускается после любых соответствующих предложений catch:

try {
  breedMoreLlamas();
} catch (e) {
  print('Error: $e'); // Handle the exception first.
} finally {
  cleanLlamaStalls(); // Then clean up.
}

Узнайте больше, прочитав раздел «Исключения» в туре по библиотеке.

Классы

Dart - это объектно-ориентированный язык с классами и смешанным наследованием. Каждый объект является экземпляром класса, и все классы происходят от объекта Object. Смешанное наследование означает, что, хотя каждый класс (кроме Object) имеет ровно один суперкласс, тело класса может быть повторно использовано в нескольких иерархиях классов.

Использование членов классов

У объектов есть члены, состоящие из функций и данных (методы и переменные экземпляра соответственно). Когда вы вызываете метод, вы вызываете его для объекта: у метода есть доступ к функциям и данным этого объекта.

Используйте точку (.) Для ссылки на переменную или метод экземпляра:

var p = Point(2, 2);

// Set the value of the instance variable y.
p.y = 3;

// Get the value of y.
assert(p.y == 3);

// Invoke distanceTo() on p.
num distance = p.distanceTo(Point(4, 4));

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

// If p is non-null, set its y value to 4.
p?.y = 4;
Использование конструкторов

Вы можете создать объект, используя конструктор. Имена конструктора могут быть ClassName или ClassName.identifier. Например, следующий код создает объекты Point с помощью конструкторов Point() и Point.fromJson():

var p1 = Point(2, 2);
var p2 = Point.fromJson({'x': 1, 'y': 2});

Следующий код имеет тот же эффект, но использует необязательное ключевое слово new перед именем конструктора:

var p1 = new Point(2, 2);
var p2 = new Point.fromJson({'x': 1, 'y': 2});
Примечание к версии: новое ключевое слово стало необязательным в Dart 2.

Некоторые классы предоставляют константные конструкторы. Чтобы создать постоянную времени компиляции с помощью конструктора констант, поместите ключевое слово const перед именем конструктора:

var p = const ImmutablePoint(2, 2);

Построение двух идентичных констант времени компиляции приводит к одному, каноническому экземпляру:

var a = const ImmutablePoint(1, 1);
var b = const ImmutablePoint(1, 1);

assert(identical(a, b)); // They are the same instance!

В постоянном контексте вы можете опустить const перед конструктором или литералом. Например, посмотрите на этот код, который создает карту const:

// Lots of const keywords here.
const pointAndLine = const {
  'point': const [const ImmutablePoint(0, 0)],
  'line': const [const ImmutablePoint(1, 10), const ImmutablePoint(-2, 11)],
};

Вы можете опустить все, кроме первого использования ключевого слова const:

// Only one const, which establishes the constant context.
const pointAndLine = {
  'point': [ImmutablePoint(0, 0)],
  'line': [ImmutablePoint(1, 10), ImmutablePoint(-2, 11)],
};

Если константный конструктор находится вне константного контекста и вызывается без const, он создает неконстантный объект:

var a = const ImmutablePoint(1, 1); // Creates a constant
var b = ImmutablePoint(1, 1); // Does NOT create a constant

assert(!identical(a, b)); // NOT the same instance!
Примечание к версии: ключевое слово const стало необязательным в постоянном контексте в Dart 2.
Получение типа объекта

Чтобы получить тип объекта во время выполнения, вы можете использовать свойство объектов runtimeType, которое возвращает объект Type.

print('The type of a is ${a.runtimeType}');

До этого вы видели, как использовать классы. В оставшейся части этого раздела показано, как реализовать классы.

Переменные экземпляра

Вот как вы объявляете переменные экземпляра:

class Point {
  num x; // Declare instance variable x, initially null.
  num y; // Declare y, initially null.
  num z = 0; // Declare z, initially 0.
}

Все неинициализированные переменные экземпляра имеют значение null.

Все переменные экземпляра генерируют неявный метод получения. Не финальные переменные экземпляра также генерируют неявный метод установки. Для получения дополнительной информации см. геттеры и сеттеры.

class Point {
  num x;
  num y;
}

void main() {
  var point = Point();
  point.x = 4; // Use the setter method for x.
  assert(point.x == 4); // Use the getter method for x.
  assert(point.y == null); // Values default to null.
}

Если вы инициализируете переменную экземпляра там, где она объявлена ​​(а не в конструкторе или методе), значение устанавливается при создании экземпляра, то есть перед выполнением конструктора и его списка инициализатора.

Конструкторы

Объявите конструктор, создав функцию с тем же именем, что и его класс (плюс, необязательно, дополнительный идентификатор, как описано в Именованных конструкторах). Наиболее распространенная форма конструктора, порождающий конструктор, создает новый экземпляр класса:

class Point {
  num x, y;

  Point(num x, num y) {
    // There's a better way to do this, stay tuned.
    this.x = x;
    this.y = y;
  }
}

Ключевое слово this ссылается на текущий экземпляр.

Примечание. Используйте this только в случае конфликта имен. В противном случае стиль Dart опускает это.

Шаблон назначения аргумента конструктора для переменной экземпляра настолько распространен, что у Dart есть синтаксический сахар, чтобы упростить его:

class Point {
  num x, y;

  // Syntactic sugar for setting x and y
  // before the constructor body runs.
  Point(this.x, this.y);
}
Конструкторы по умолчанию

Если вы не объявляете конструктор, вам предоставляется конструктор по умолчанию. Конструктор по умолчанию не имеет аргументов и вызывает конструктор без аргументов в суперклассе.

Конструкторы не наследуются

Подклассы не наследуют конструкторы от своего суперкласса. У подкласса, который не объявляет конструкторов, есть только конструктор по умолчанию (без аргумента, без имени).

Именованные конструкторы

Используйте именованный конструктор для реализации нескольких конструкторов для класса или для дополнительной ясности:

class Point {
  num x, y;

  Point(this.x, this.y);

  // Named constructor
  Point.origin() {
    x = 0;
    y = 0;
  }
}

Помните, что конструкторы не наследуются, что означает, что именованный конструктор суперкласса не наследуется подклассом. Если вы хотите, чтобы подкласс создавался с помощью именованного конструктора, определенного в суперклассе, вы должны реализовать этот конструктор в подклассе.

Вызов конструктора суперкласса не по умолчанию

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

  1. список инициализаторов
  2. конструктор суперкласса без аргументов
  3. конструктор основного класса без аргументов

Если у суперкласса нет безымянного конструктора без аргументов, то вы должны вручную вызвать один из конструкторов в суперклассе. Укажите конструктор суперкласса после двоеточия (:), непосредственно перед телом конструктора (если есть).

В следующем примере конструктор для класса Employee вызывает именованный конструктор для своего суперкласса Person. Нажмите Run, чтобы выполнить код.

class Person {
  String firstName;

  Person.fromJson(Map data) {
    print('in Person');
  }
}

class Employee extends Person {
  // Person does not have a default constructor;
  // you must call super.fromJson(data).
  Employee.fromJson(Map data) : super.fromJson(data) {
    print('in Employee');
  }
}

main() {
  var emp = new Employee.fromJson({});

  // Prints:
  // in Person
  // in Employee
  if (emp is Person) {
    // Type check
    emp.firstName = 'Bob';
  }
  (emp as Person).firstName = 'Bob';
}

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

class Employee extends Person {
  Employee() : super.fromJson(getDefaultData());
  // ···
}
Предупреждение: аргументы конструктора суперкласса не имеют доступа к this. Например, аргументы могут вызывать статические методы, но не методы экземпляра.
Список инициализаторов

Помимо вызова конструктора суперкласса, вы также можете инициализировать переменные экземпляра до запуска тела конструктора. Разделяйте инициализаторы запятыми.

// Initializer list sets instance variables before
// the constructor body runs.
Point.fromJson(Map json)
    : x = json['x'],
      y = json['y'] {
  print('In Point.fromJson(): ($x, $y)');
}
Предупреждение: правая часть инициализатора не имеет доступа к этому.

Во время разработки вы можете проверить входные данные, используя assert в списке инициализатора.

Point.withAssert(this.x, this.y) : assert(x >= 0) {
  print('In Point.withAssert(): ($x, $y)');
}

Списки инициализатора удобны при настройке конечных полей. В следующем примере инициализируются три последних поля в списке инициализатора. Пример этого показан ниже

import 'dart:math';

class Point {
  final num x;
  final num y;
  final num distanceFromOrigin;

  Point(x, y)
      : x = x,
        y = y,
        distanceFromOrigin = sqrt(x * x + y * y);
}

main() {
  var p = new Point(2, 3);
  print(p.distanceFromOrigin);
}
Перенаправление конструкторов

Иногда единственной целью конструктора является перенаправление в другой конструктор того же класса. Тело перенаправляющего конструктора пустое, а вызов конструктора появляется после двоеточия (:).

class Point {
  num x, y;

  // The main constructor for this class.
  Point(this.x, this.y);

  // Delegates to the main constructor.
  Point.alongXAxis(num x) : this(x, 0);
}
Константные конструкторы

Если ваш класс создает объекты, которые никогда не меняются, вы можете сделать эти объекты константами времени компиляции. Для этого определите конструктор const и убедитесь, что все переменные экземпляра являются финальными final.

class ImmutablePoint {
  static final ImmutablePoint origin =
      const ImmutablePoint(0, 0);

  final num x, y;

  const ImmutablePoint(this.x, this.y);
}

Константные конструкторы не всегда создают константы. Подробности смотрите в разделе об использовании конструкторов.

Фабричные конструкторы

Используйте ключевое слово factory при реализации конструктора, который не всегда создает новый экземпляр своего класса. Например, конструктор фабрики может возвратить экземпляр из кэша или он может вернуть экземпляр подтипа.

В следующем примере демонстрируется конструктор фабрики, возвращающий объекты из кэша:

class Logger {
  final String name;
  bool mute = false;

  // _cache is library-private, thanks to
  // the _ in front of its name.
  static final Map _cache =
      {};

  factory Logger(String name) {
    return _cache.putIfAbsent(
        name, () => Logger._internal(name));
  }

  Logger._internal(this.name);

  void log(String msg) {
    if (!mute) print(msg);
  }
}
Примечание: фабричные конструкторы не имеют доступ к this.

Вызовите конструктор фабрики так же, как любой другой конструктор:

var logger = Logger('UI');
logger.log('Button clicked');

Методы

Методы - это функции, которые обеспечивают поведение объекта.

Методы экземпляра

Методы экземпляра на объектах могут обращаться к переменным экземпляра, и this. Метод distanceTo() в следующем примере является примером метода экземпляра:

import 'dart:math';

class Point {
  num x, y;

  Point(this.x, this.y);

  num distanceTo(Point other) {
    var dx = x - other.x;
    var dy = y - other.y;
    return sqrt(dx * dx + dy * dy);
  }
}
Геттеры и сеттеры

Методы получения и установки - это специальные методы, обеспечивающие доступ на чтение и запись к свойствам объекта. Напомним, что каждая переменная экземпляра имеет неявный метод получения, а также, в случае необходимости, метод установки. Вы можете создать дополнительные свойства, реализовав методы получения и установки, используя ключевые слова get и set:

class Rectangle {
  num left, top, width, height;

  Rectangle(this.left, this.top, this.width, this.height);

  // Define two calculated properties: right and bottom.
  num get right => left + width;
  set right(num value) => left = value - width;
  num get bottom => top + height;
  set bottom(num value) => top = value - height;
}

void main() {
  var rect = Rectangle(3, 4, 20, 15);
  assert(rect.left == 3);
  rect.right = 12;
  assert(rect.left == -8);
}

С помощью методов получения и установки вы можете начать с переменных экземпляра, а затем обернуть их методами, и все это без изменения клиентского кода.

Примечание. Такие операторы, как increment (++), работают ожидаемым образом, независимо от того, определен ли получатель явно. Чтобы избежать неожиданных побочных эффектов, оператор вызывает геттер ровно один раз, сохраняя его значение во временной переменной.
Абстрактные методы

Методы экземпляра, геттер и сеттер могут быть абстрактными, определяя интерфейс, но оставляя его реализацию другим классам. Абстрактные методы могут существовать только в абстрактных классах.

Чтобы сделать метод абстрактным, используйте точку с запятой (;) вместо тела метода:

abstract class Doer {
  // Define instance variables and methods...

  void doSomething(); // Define an abstract method.
}

class EffectiveDoer extends Doer {
  void doSomething() {
    // Provide an implementation, so the method is not abstract here...
  }
}
Абстрактные классы

Используйте модификатор abstract для определения абстрактного класса - класса, который не может быть создан. Абстрактные классы полезны для определения интерфейсов, часто с некоторой реализацией. Если вы хотите, чтобы ваш абстрактный класс казался инстанцируемым, определите конструктор фабрики.

Абстрактные классы часто имеют абстрактные методы. Вот пример объявления абстрактного класса, который имеет абстрактный метод:

// This class is declared abstract and thus
// can't be instantiated.
abstract class AbstractContainer {
  // Define constructors, fields, methods...

  void updateChildren(); // Abstract method.
}
Неявные интерфейсы

Каждый класс неявно определяет интерфейс, содержащий все элементы экземпляра класса и любые интерфейсы, которые он реализует. Если вы хотите создать класс A, который поддерживает API класса B, не наследуя реализацию B, класс A должен реализовать интерфейс B.

Класс реализует один или несколько интерфейсов, объявив их в предложении Implements, а затем предоставив API-интерфейсы, требуемые интерфейсами. Например:

// A person. The implicit interface contains greet().
class Person {
  // In the interface, but visible only in this library.
  final _name;

  // Not in the interface, since this is a constructor.
  Person(this._name);

  // In the interface.
  String greet(String who) => 'Hello, $who. I am $_name.';
}

// An implementation of the Person interface.
class Impostor implements Person {
  get _name => '';

  String greet(String who) => 'Hi $who. Do you know who I am?';
}

String greetBob(Person person) => person.greet('Bob');

void main() {
  print(greetBob(Person('Kathy')));
  print(greetBob(Impostor()));
}

Вот пример указания, что класс реализует несколько интерфейсов:

class Point implements Comparable, Location {...}
Расширение класса

Используйте extends для создания подкласса, и super для ссылки на суперкласс:

class Television {
  void turnOn() {
    _illuminateDisplay();
    _activateIrSensor();
  }
  // ···
}

class SmartTelevision extends Television {
  void turnOn() {
    super.turnOn();
    _bootNetworkInterface();
    _initializeMemory();
    _upgradeApps();
  }
  // ···
}
Переопределяющие члены

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

class SmartTelevision extends Television {
  @override
  void turnOn() {...}
  // ···
}

Чтобы сузить тип параметра метода или переменной экземпляра в коде, который является безопасным типом, вы можете использовать ключевое слово covariant.

<+|[]
>/^[]=
<=~/&~
>=*<<==
%>> 
Примечание. Возможно, вы заметили, что != Не является переопределенным оператором. Выражение e1! = E2 является просто синтаксическим сахаром для !(E1 == e2).

Вот пример класса, который переопределяет операторы + и -:

class Vector {
  final int x, y;

  Vector(this.x, this.y);

  Vector operator +(Vector v) => Vector(x + v.x, y + v.y);
  Vector operator -(Vector v) => Vector(x - v.x, y - v.y);

  // Operator == and hashCode not shown. For details, see note below.
  // ···
}

void main() {
  final v = Vector(2, 3);
  final w = Vector(2, 2);

  assert(v + w == Vector(4, 5));
  assert(v - w == Vector(0, 1));
}

Если вы переопределите ==, вам также следует переопределить метод получения hashCode объекта. Пример переопределения == и hashCode см. В разделе «Реализация ключей карты».

Для получения дополнительной информации о переопределении, в общем, см. Расширение класса.

noSuchMethod()

Чтобы обнаружить или отреагировать всякий раз, когда код пытается использовать несуществующий метод или переменную экземпляра, вы можете переопределить noSuchMethod():

class A {
  // Unless you override noSuchMethod, using a
  // non-existent member results in a NoSuchMethodError.
  @override
  void noSuchMethod(Invocation invocation) {
    print('You tried to use a non-existent member: ' +
        '${invocation.memberName}');
  }
}

Вы не можете вызвать нереализованный метод, если не выполнено одно из следующих условий:

  • Приемник имеет статический тип динамический dynamic.
  • Получатель имеет статический тип, который определяет нереализованный метод (аннотация в порядке), а динамический тип получателя имеет реализацию noSuchMethod(), которая отличается от реализации в классе Object.

Для получения дополнительной информации см. Неофициальную спецификацию пересылки noSuchMethod.

Перечисляемые типы

Перечислимые типы, часто называемые перечислениями или перечислениями, представляют собой особый вид класса, используемый для представления фиксированного числа постоянных значений.

Использование перечислений

Объявите перечислимый тип, используя ключевое слово enum:

enum Color { red, green, blue }

Каждое значение в перечислении имеет получатель индекса, который возвращает позицию значения, начинающуюся с нуля, в объявлении перечисления. Например, первое значение имеет индекс 0, а второе значение имеет индекс 1.

assert(Color.red.index == 0);
assert(Color.green.index == 1);
assert(Color.blue.index == 2);

Чтобы получить список всех значений в перечислении, используйте константу значений values перечисления.

List colors = Color.values;
assert(colors[2] == Color.blue);

Вы можете использовать enums в выражениях switch и вы получите предупреждение, если вы не обработаете все значения enum:

var aColor = Color.blue;

switch (aColor) {
  case Color.red:
    print('Red as roses!');
    break;
  case Color.green:
    print('Green as grass!');
    break;
  default: // Without this, you see a WARNING.
    print(aColor); // 'Color.blue'
}

Перечисляемые типы имеют следующие ограничения:

  • Вы не можете создавать подклассы, смешивать или реализовывать перечисление.
  • Вы не можете явно создать экземпляр enum.

Для получения дополнительной информации см. Спецификацию языка Dart.

Добавление возможностей в класс: mixins

Миксины - это способ многократного использования кода класса в нескольких иерархиях классов.

Чтобы использовать миксин, используйте ключевое слово with, за которым следует одно или несколько имен миксинов. В следующем примере показаны два класса, которые используют mixins:

class Musician extends Performer with Musical {
  // ···
}

class Maestro extends Person
    with Musical, Aggressive, Demented {
  Maestro(String maestroName) {
    name = maestroName;
    canConduct = true;
  }
}

Чтобы реализовать миксин, создайте класс, который расширяет Object и не объявляет конструкторов. Если вы не хотите, чтобы ваш миксин использовался как обычный класс, используйте ключевое слово mixin вместо class. Например:

mixin Musical {
  bool canPlayPiano = false;
  bool canCompose = false;
  bool canConduct = false;

  void entertainMe() {
    if (canPlayPiano) {
      print('Playing piano');
    } else if (canConduct) {
      print('Waving hands');
    } else {
      print('Humming to self');
    }
  }
}

Чтобы указать, что только некоторые типы могут использовать миксин - например, чтобы ваш миксин мог вызывать метод, который он не определяет - используйте on для указания требуемого суперкласса:

mixin MusicalPerformer on Musician {
  // ···
}
Примечание к версии: Поддержка ключевого слова mixin была введена в Dart 2.1. Код в более ранних выпусках обычно использовал abstract class. Дополнительную информацию об изменениях в миксинах 2.1 см. В журнале изменений Dart SDK и спецификации миксинов 2.1.

Переменные и методы класса

Используйте ключевое слово static для реализации общеклассовых переменных и методов.

Статические переменные

Статические переменные (переменные класса) полезны для общеклассового состояния и констант:

class Queue {
  static const initialCapacity = 16;
  // ···
}

void main() {
  assert(Queue.initialCapacity == 16);
}

Статические переменные не инициализируются до тех пор, пока они не будут использованы.

[pre] Примечание. На этой странице соблюдается рекомендация руководства по стилю о предпочтении lowerCamelCase для постоянных имен. [/pre]

Статические методы

Статические методы (методы класса) не работают с экземпляром и, следовательно, не имеют доступа к this. Например:

import 'dart:math';

class Point {
  num x, y;
  Point(this.x, this.y);

  static num distanceBetween(Point a, Point b) {
    var dx = a.x - b.x;
    var dy = a.y - b.y;
    return sqrt(dx * dx + dy * dy);
  }
}

void main() {
  var a = Point(2, 2);
  var b = Point(4, 4);
  var distance = Point.distanceBetween(a, b);
  assert(2.8 < distance && distance < 2.9);
  print(distance);
}

[pre] Примечание: рассмотрите возможность использования функций верхнего уровня вместо статических методов для общих или широко используемых утилит и функций. [/pre]

Вы можете использовать статические методы как константы времени компиляции. Например, вы можете передать статический метод в качестве параметра константному конструктору.

Обобщения - Generics

Если вы посмотрите на документацию API для базового типа массива List, вы увидите, что тип на самом деле List<E>. Нотация <…> помечает List как универсальный (или параметризованный) тип - тип, имеющий параметры формального типа. По соглашению, большинство переменных типа имеют однобуквенные имена, такие как E, T, S, K и V.

Зачем использовать дженерики?

Обобщения часто требуются для обеспечения безопасности типов, но они имеют больше преимуществ, чем просто запуск кода:

  • Правильное указание универсальных типов приводит к лучшему сгенерированному коду.
  • Вы можете использовать дженерики для уменьшения дублирования кода.

Если вы хотите, чтобы список содержал только строки, вы можете объявить его как List <String> (читается как «список строк»). Таким образом, вы, ваши коллеги-программисты и ваши инструменты можете обнаружить, что присвоение нестрокового списка, вероятно, является ошибкой. Вот пример:

var names = List();
names.addAll(['Seth', 'Kathy', 'Lars']);
names.add(42); // Error

Еще одна причина использования обобщений заключается в уменьшении дублирования кода. Обобщения позволяют вам совместно использовать единый интерфейс и реализацию для многих типов, в то же время используя преимущества статического анализа. Например, допустим, вы создали интерфейс для кэширования объекта:

abstract class ObjectCache {
  Object getByKey(String key);
  void setByKey(String key, Object value);
}

Вы обнаружите, что вам нужна специфичная для строки версия этого интерфейса, поэтому вы создаете другой интерфейс:

abstract class StringCache {
  String getByKey(String key);
  void setByKey(String key, String value);
}

Позже вы решите, что вам нужна специфичная для номера версия этого интерфейса ... Вы поняли идею.

Универсальные типы могут избавить вас от необходимости создавать все эти интерфейсы. Вместо этого вы можете создать один интерфейс, который принимает параметр типа:

abstract class Cache {
  T getByKey(String key);
  void setByKey(String key, T value);
}

В этом коде T является типом stand-in. Это заполнитель, который вы можете рассматривать как тип, который разработчик определит позже.

Использование литералов коллекции

Список, набор и литералы карты могут быть параметризованы. Параметризованные литералы аналогичны литералам, которые вы уже видели, за исключением того, что вы добавляете <type> (для списков и наборов) или <keyTypevalueType> (для карт) перед открывающей скобкой. Вот пример использования типизированных литералов:

var names = ['Seth', 'Kathy', 'Lars'];
var uniqueNames = {'Seth', 'Kathy', 'Lars'};
var pages = {
  'index.html': 'Homepage',
  'robots.txt': 'Hints for web robots',
  'humans.txt': 'We are people, not machines'
};

Использование параметризованных типов с конструкторами

Чтобы указать один или несколько типов при использовании конструктора, поместите типы в угловые скобки (<...>) сразу после имени класса. Например:

var nameSet = Set.from(names);

Следующий код создает карту с целочисленными ключами и значениями типа View:

var views = Map();

Коллекции обобщений и типы, которые они содержат

Универсальные типы Dart являются реализованными, что означает, что они переносят информацию своего типа во время выполнения. Например, вы можете проверить тип коллекции:

var names = List();
names.addAll(['Seth', 'Kathy', 'Lars']);
print(names is List); // true
Примечание. В отличие от универсальных в Java используется стирание, что означает, что параметры универсального типа удаляются во время выполнения. В Java вы можете проверить, является ли объект списком, но вы не можете проверить, является ли он списком List<String>.

Ограничение параметризованного типа

При реализации универсального типа вы можете захотеть ограничить типы его параметров. Вы можете сделать это, используя extends.

class Foo {
  // Implementation goes here...
  String toString() => "Instance of 'Foo<$T>'";
}

class Extender extends SomeBaseClass {...}

Можно использовать SomeBaseClass или любой из его подклассов в качестве универсального аргумента:

var someBaseClassFoo = Foo();
var extenderFoo = Foo();

Также можно указать общий аргумент:

var foo = Foo();
print(foo); // Instance of 'Foo'

Указание любого типа, отличного от SomeBaseClass, приводит к ошибке:

var foo = Foo<Object>();

Использование методов обобщений

Первоначально общая поддержка Dart была ограничена классами. Более новый синтаксис, называемый обобщенными методами, допускает аргументы типа для методов и функций:

T first(List ts) {
  // Do some initial work or error checking, then...
  T tmp = ts[0];
  // Do some additional checking or processing...
  return tmp;
}

Здесь параметр общего типа для first (<T>) позволяет вам использовать аргумент типа T в нескольких местах:

  • В возвращаемом типе функции (T).
  • В типе аргумента (List <T>).
  • В типе локальной переменной (T tmp).

Для получения дополнительной информации о дженериках см. Использование универсальных методов.

Библиотеки и видимость

Директивы import и library могут помочь вам создать модульную и разделяемую базу кода. Библиотеки не только предоставляют API-интерфейсы, но и являются элементом конфиденциальности: идентификаторы, которые начинаются со знака подчеркивания (_), видны только внутри библиотеки. Каждое приложение Dart является библиотекой, даже если оно не использует директиву библиотеки library.

Библиотеки могут распространяться с использованием пакетов.

Использование библиотек

Используйте импорт import, чтобы указать, как пространство имен из одной библиотеки используется в области действия другой библиотеки.

Например, веб-приложения Dart обычно используют библиотеку dart: html, которую они могут импортировать следующим образом:

import 'dart:html';
Примечание: URI обозначает унифицированный идентификатор ресурса. URL-адреса (унифицированные указатели ресурсов) являются распространенным типом URI.
Указание префикса библиотеки

Если вы импортируете две библиотеки с конфликтующими идентификаторами, вы можете указать префикс для одной или обеих библиотек. Например, если library1 и library2 оба имеют класс Element, то у вас может быть такой код:

import 'package:lib1/lib1.dart';
import 'package:lib2/lib2.dart' as lib2;

// Uses Element from lib1.
Element element1 = Element();

// Uses Element from lib2.
lib2.Element element2 = lib2.Element();
Импорт только части библиотеки

Если вы хотите использовать только часть библиотеки, вы можете выборочно импортировать библиотеку. Например:

// Import only foo.
import 'package:lib1/lib1.dart' show foo;

// Import all names EXCEPT foo.
import 'package:lib2/lib2.dart' hide foo;
Библиотека с отложенной загрузкой(lazily loading)

Отложенная загрузка (также называемая отложенной загрузкой) позволяет веб-приложению загружать библиотеку по требованию, если и когда она необходима. Вот несколько случаев, когда вы можете использовать отложенную загрузку:

  • Чтобы уменьшить начальное время запуска веб-приложения.
  • Выполнить A / B-тестирование - например, попробовать альтернативные реализации алгоритма.
  • Для загрузки редко используемых функций, таких как дополнительные экраны и диалоги.
Только dart2js поддерживает отложенную загрузку. Flutter, Dart VM и dartdevc не поддерживают отложенную загрузку. Для получения дополнительной информации см. Выпуск № 33118 и выпуск № 27776.

Чтобы отложено загрузить библиотеку, вы должны сначала импортировать ее, используя deferred as.

import 'package:greetings/hello.dart' deferred as hello;

Когда вам нужна библиотека, вызовите loadLibrary(), используя идентификатор библиотеки.

Future greet() async {
  await hello.loadLibrary();
  hello.printGreeting();
}

В предыдущем коде ключевое слово await приостанавливает выполнение до загрузки библиотеки. Для получения дополнительной информации об async и await см. Поддержку асинхронности.

Вы можете вызывать loadLibrary() несколько раз в библиотеке без проблем. Библиотека загружается только один раз.

При использовании отложенной загрузки помните следующее:

  • Константы отложенной библиотеки не являются константами в импортируемом файле. Помните, что эти константы не существуют, пока не будет загружена отложенная библиотека.
  • Вы не можете использовать типы из отложенной библиотеки в импортируемом файле. Вместо этого рассмотрите возможность перемещения типов интерфейса в библиотеку, импортированную как отложенной библиотекой, так и импортирующим файлом.
  • Dart неявно вставляет loadLibrary() в пространство имен, которое вы определяете, используя deferred as namespace. Функция loadLibrary() возвращает Future.

Реализация библиотек

Посмотрите Создание Библиотечных пакетов для совета относительно того, как реализовать пакет библиотеки, включая:

  • Как организовать исходный код библиотеки.
  • Как использовать директиву export.
  • Когда использовать директиву part.
  • Когда использовать директиву library.

Поддержка асинхронности

Библиотеки Dart полны функций, которые возвращают объекты Future или Stream. Эти функции являются асинхронными: они возвращаются после настройки, возможно, трудоемкой операции (например, ввода-вывода), не дожидаясь завершения этой операции.

Ключевые слова async и await поддерживают асинхронное программирование, позволяя вам писать асинхронный код, который похож на синхронный код.

Обработка фьючерсов

Когда вам нужен результат завершенного будущего, у вас есть два варианта:

Код, который использует async и await, является асинхронным, но очень похож на синхронный код. Например, вот код, который использует await для ожидания результата асинхронной функции:

await lookUpVersion();

Чтобы использовать await, код должен быть в функции async - функции, помеченной как async:

Future checkVersion() async {
  var version = await lookUpVersion();
  // Do something with version
}
Примечание. Хотя функция async может выполнять длительные операции, она не ожидает этих операций. Вместо этого функция async выполняется только до тех пор, пока не встретит свое первое выражение ожидания (подробности). Затем он возвращает объект Future, возобновляя выполнение только после завершения выражения await.

Используйте try, catch и finally для обработки ошибок и очистки в коде, который использует await:

try {
  version = await lookUpVersion();
} catch (e) {
  // React to inability to look up the version
}

Вы можете использовать await несколько раз в функции async. Например, следующий код трижды ожидает результатов функций:

var entrypoint = await findEntrypoint();
var exitCode = await runExecutable(entrypoint, args);
await flushThenExit(exitCode);

В await expression, значение expression обычно является будущим; если это не так, то значение автоматически переносится в будущее. Этот объект Future указывает на обещание вернуть объект. Значением await epression является возвращаемый объект. Выражение await останавливает выполнение до тех пор, пока этот объект не станет доступен.

Если вы получаете ошибку времени компиляции при использовании await, убедитесь, что await находится в функции async. Например, чтобы использовать await в функции main() вашего приложения, тело main() должно быть помечено как async:

Future main() async {
  checkVersion();
  print('In main: version is ${await lookUpVersion()}');
}

Объявление асинхронных функций

Функция async - это функция, тело которой помечено модификатором асинхронности async.

Добавление ключевого слова async в функцию возвращает Future. Например, рассмотрим эту синхронную функцию, которая возвращает строку:

String lookUpVersion() => '1.0.0';

Если вы измените его на асинхронную функцию через async- например, потому что будущая реализация будет занимать много времени - возвращаемое значение будет Future:

Future lookUpVersion() async => '1.0.0';

Обратите внимание, что телу функции не нужно использовать Future API. Dart создает объект Future, если это необходимо. Если ваша функция не возвращает полезное значение, сделайте возвращаемый тип Future <void>.

Интерактивное введение в использование futures, async и await см. В разделе кодовой метки асинхронного программирования.

Обработка потоков

Когда вам нужно получить значения из потока, у вас есть два варианта:

  • Используйте async и асинхронный цикл for (await for).
  • Используйте Stream API, как описано в обзоре библиотеки.
Примечание. Перед использованием await for убедитесь, что это делает код более понятным и что вы действительно хотите дождаться всех результатов потока. Например, вам обычно не следует использовать await for для слушателей событий пользовательского интерфейса, поскольку платформы пользовательского интерфейса отправляют бесконечные потоки событий.

Асинхронный цикл for имеет следующую форму:

await for (varOrType identifier in expression) {
  // Executes each time the stream emits a value.
}

Значение выражения expression должно иметь тип Stream. Исполнение происходит следующим образом:

  1. Подождите, пока поток не выдаст значение.
  2. Выполнить тело цикла for с переменной, установленной на это значение.
  3. Повторите 1 и 2, пока поток не будет закрыт.

Чтобы прекратить прослушивание потока, вы можете использовать оператор break или return, который выходит из цикла for и отписывается от потока.

Если вы получили ошибку во время компиляции при реализации асинхронного цикла for, убедитесь, что await for находится в асинхронной функции async. Например, чтобы использовать асинхронный цикл for в функции main() вашего приложения, тело main() должно быть помечено как async:

Future main() async {
  // ...
  await for (var request in requestServer) {
    handleRequest(request);
  }
  // ...
}

Для получения дополнительной информации об асинхронном программировании, в общем, смотрите раздел dart:async в обзоре библиотеки.

Генераторы

Когда вам нужно лениво создать последовательность значений, подумайте об использовании функции генератора. Дарт имеет встроенную поддержку двух видов функций генератора:

  • Синхронный генератор: возвращает итерируемый объект Iterable.
  • Асинхронный генератор: возвращает потоковый объект Stream Stream.

Чтобы реализовать функцию синхронного генератора, пометьте тело функции как sync * и используйте операторы yield для доставки значений:

Iterable naturalsTo(int n) sync* {
  int k = 0;
  while (k < n) yield k++;
}

Чтобы реализовать функцию асинхронного генератора, пометьте тело функции как async * и используйте операторы yield для доставки значений:

Stream asynchronousNaturalsTo(int n) async* {
  int k = 0;
  while (k < n) yield k++;
}

Если ваш генератор рекурсивный, вы можете улучшить его производительность, используя yield *:

Iterable naturalsDownFrom(int n) sync* {
  if (n > 0) {
    yield n;
    yield* naturalsDownFrom(n - 1);
  }
}

Вызываемые классы

Чтобы позволить экземпляру вашего класса Dart вызываться как функция, реализуйте метод call().

В следующем примере класс WannabeFunction определяет функцию call(), которая принимает три строки и объединяет их, разделяя каждую пробелом и добавляя восклицательный знак. Ниже представлен код, в виде примера

class WannabeFunction {
  call(String a, String b, String c) => '$a $b $c!';
}

main() {
  var wf = new WannabeFunction();
  var out = wf("Hi", "there,", "gang");
  print('$out');
}

Изоляты

Большинство компьютеров, даже на мобильных платформах, имеют многоядерные процессоры. Чтобы воспользоваться всеми этими ядрами, разработчики традиционно используют потоки совместно используемой памяти, работающие одновременно. Однако параллелизм общего состояния подвержен ошибкам и может привести к сложному коду.

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

Для получения дополнительной информации см. Следующее:

Определения типов Typedefs

В Dart функции являются объектами, так же как строки и числа являются объектами. Typedef, или псевдоним типа функции, дает типу функции имя, которое вы можете использовать при объявлении полей и возвращаемых типов.

Рассмотрим следующий код, который не использует typedef:

class SortedCollection {
  Function compare;

  SortedCollection(int f(Object a, Object b)) {
    compare = f;
  }
}

// Initial, broken implementation.
int sort(Object a, Object b) => 0;

void main() {
  SortedCollection coll = SortedCollection(sort);

  // All we know is that compare is a function,
  // but what type of function?
  assert(coll.compare is Function);
}

Информация о типе теряется при назначении f для compare. Тип f - (Object, Object)int (где → означает возврат), но тип сравнения compare - Function. Если мы изменим код, чтобы использовать явные имена и сохранили информацию о типе, и разработчики, и инструменты могут использовать эту информацию.

typedef Compare = int Function(Object a, Object b);

class SortedCollection {
  Compare compare;

  SortedCollection(this.compare);
}

// Initial, broken implementation.
int sort(Object a, Object b) => 0;

void main() {
  SortedCollection coll = SortedCollection(sort);
  assert(coll.compare is Function);
  assert(coll.compare is Compare);
}
Примечание. В настоящее время typedefs ограничены типами функций. Мы ожидаем, что это изменится.

Поскольку typedefs являются просто псевдонимами, они предлагают способ проверки типа любой функции. Например:

typedef Compare = int Function(T a, T b);

int sort(int a, int b) => a - b;

void main() {
  assert(sort is Compare); // True!
}

Метаданные

Используйте метаданные для предоставления дополнительной информации о вашем коде. Аннотация метаданных начинается с символа @, за которым следует либо ссылка на константу времени компиляции ( такие как deprecated), либо обращение к конструктору констант.

Для всего кода Dart доступны две аннотации: @deprecated и @override. Примеры использования @override см. В разделе Расширение класса. Вот пример использования аннотации @deprecated:

class Television {
  /// _Deprecated: Use [turnOn] instead._
  @deprecated
  void activate() {
    turnOn();
  }

  /// Turns the TV's power on.
  void turnOn() {...}
}

Вы можете определить свои собственные аннотации метаданных. Вот пример определения аннотации @todo, которая принимает два аргумента:

library todo;

class Todo {
  final String who;
  final String what;

  const Todo(this.who, this.what);
}

И вот пример использования этой аннотации @todo:

import 'todo.dart';

@Todo('seth', 'make this do something')
void doSomething() {
  print('do something');
}

Метаданные могут появляться перед объявлением library, class, typedef, параметра типа, конструктора, фабрики, функции, поля, параметра или переменной, а также перед директивой импорта или экспорта. Вы можете получить метаданные во время выполнения, используя отражение.

Комментарии

Dart поддерживает однострочные комментарии, многострочные комментарии и комментарии к документации.

Однострочные комментарии

Однострочный комментарий начинается с //. Все между // и концом строки игнорируется компилятором Dart.

void main() {
  // TODO: refactor into an AbstractLlamaGreetingFactory?
  print('Welcome to my Llama farm!');
}

Многострочные комментарии

Многострочный комментарий начинается с /* и заканчивается */. Все между /* и */ игнорируется компилятором Dart (если комментарий не является комментарием к документации; см. Следующий раздел). Многострочные комментарии могут быть вложенными.

void main() {
  /*
   * This is a lot of work. Consider raising chickens.

  Llama larry = Llama();
  larry.feed();
  larry.exercise();
  larry.clean();
   */
}

Комментарии к документации

Комментарии к документации - это многострочные или однострочные комментарии, начинающиеся с /// или / **. Использование /// в последовательных строках имеет тот же эффект, что и многострочный комментарий к документу.

Внутри комментария к документации компилятор Dart игнорирует весь текст, если он не заключен в квадратные скобки. Используя скобки, вы можете ссылаться на классы, методы, поля, переменные верхнего уровня, функции и параметры. Имена в скобках разрешаются в лексической области документированного программного элемента.

Вот пример комментариев документации со ссылками на другие классы и аргументы:

/// A domesticated South American camelid (Lama glama).
///
/// Andean cultures have used llamas as meat and pack
/// animals since pre-Hispanic times.
class Llama {
  String name;

  /// Feeds your llama [Food].
  ///
  /// The typical llama eats one bale of hay per week.
  void feed(Food food) {
    // ...
  }

  /// Exercises your llama with an [activity] for
  /// [timeLimit] minutes.
  void exercise(Activity activity, int timeLimit) {
    // ...
  }
}

В сгенерированной документации [Food] становится ссылкой на документы API для класса Food.

Для анализа кода Dart и генерации HTML-документации вы можете использовать инструмент генерации документации SDK. Пример сгенерированной документации см. В документации по Dart API. Для получения советов о том, как структурировать ваши комментарии, см. Рекомендации для комментариев Dart Doc.

Резюме

На этой странице представлены наиболее часто используемые функции языка дартс. Реализуются дополнительные функции, но мы ожидаем, что они не нарушат существующий код. Для получения дополнительной информации см. Спецификацию языка Dart и Effective Dart.

Чтобы узнать больше об основных библиотеках Dart, см. Тур по библиотекам Dart.

Руководство по языку Dart

Примеры и туториалы

  • Примеры кода на Dart
  • Codelabs
    • Список Dart Codelabs
    • Шпаргалка по языку Dart
    • Асинхронное программирование
  • Учебники

Язык Dart

Корневая библиотека Dart

  • Описание
  • Краткий тур по библиотекам Dart
  • Статьи
    • Введение в dart.io
    • Создание потоков

Пакеты языка Dart

  • Как использовать пакеты Dart
  • Обычно используемые пакеты Dart
  • Создание пакетов
  • Публикация пакетов

Разработка на языке Dart

  • Асинхронное программирование: futures, async, await
  • Interoperability
  • JSON
  • Потоки
  • Мобильные приложения
  • Командная строка и серверные приложения
  • Веб — приложения

Разъяснение понятий Widget, State, Context, InheritedWidget

В этой статье рассматриваются важные понятия Widget, State, Context и InheritedWidget в приложениях Flutter. Особое внимание уделяется InheritedWidget, который является одним из наиболее важных и менее документированных виджетов.

Предисловие

Понятия Widget, State и Context во Flutter являются одними из самых важных понятий, которые каждый разработчик Flutter должен полностью понимать.

Однако документация огромна, и эта концепция не всегда четко объяснена.

Я объясню эти понятия своими словами и ярлыками, зная, что это может привести к шоку некоторых пуристов, но настоящая цель этой статьи — попытаться прояснить следующие темы:

  • Разница между виджетами Stateful и Stateless
  • Что такое Context
  • Что такое State и как его использовать
  • Связь между контекстом и его объектом состояния
  • InheritedWidget и способ распространения информации внутри дерева виджетов
  • Понятие перепостроения

Часть 1. Концепция

Понятие виджета

Во Flutter все является виджетом.

Думайте о виджете как о визуальном компоненте (или компоненте, который взаимодействует с визуальным аспектом приложения).

Когда вам нужно создать что-то, что прямо или косвенно связано с макетом, вы используете виджеты.

Понятие дерева виджетов

Виджеты организованы в виде древовидной структуры.

Виджет, который содержит другие виджеты, называется родительским виджетом parent Widget (или контейнером виджетов). Виджеты, которые содержатся в родительском виджете, называются дочерними виджетами children Widgets.

Давайте проиллюстрируем это с помощью базового приложения, которое автоматически генерируется Flutter. Вот упрощенный код, ограниченный методом сборки build:

@override
Widget build(BuildContext){
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: new Center(
        child: new Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            new Text(
              'You have pushed the button this many times:',
            ),
            new Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: new FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: new Icon(Icons.add),
      ),
    );
}

Если мы теперь рассмотрим этот базовый пример, мы получим следующую древовидную структуру виджетов (ограничив список виджетов, присутствующих в коде):

Понятие контекста

Еще одним важным понятием является контекст Context виджета.

Контекст — это не что иное, как ссылка на расположение виджета в древовидной структуре всех созданных виджетов.

Короче говоря, думайте о контексте как о части дерева виджетов, где виджет прикреплен к этому дереву.

Контекст принадлежит только одному виджету.

Если виджет «А» имеет дочерние виджеты, контекст виджета «А» станет родительским контекстом непосредственно дочернего контекста.

Читая это, становится ясно, что контексты связаны между собой и составляют дерево контекстов отношением parent-children.

Если мы сейчас попытаемся проиллюстрировать понятие контекста Context на предыдущей диаграмме, мы получим такое представление(все еще в очень упрощенном виде), где каждый цвет представляет контекст (кроме MyApp, который отличается):

Видимость контекста (упрощенное утверждение). Виджет виден только в его собственном контексте или в контексте его родительского(их) контекста (ов).

Из этого утверждения мы можем извлечь, что из дочернего контекста легко найти виджет-предок (parent).

Примером является рассмотрение Scaffold > Center > Column > Text: context.ancestorWidgetOfExactType (Scaffold) => возвращает первый Scaffold путем перехода к древовидной структуре из контекста Text.

Из родительского контекста также можно найти дочерний (child) виджет, но делать это не рекомендуется (мы обсудим это позже).

Типы виджетов

Виджеты бывают двух типов:

  • Stateless Widget
  • Statefull Widget

Stateless Widget

Некоторые из этих визуальных компонентов не зависят ни от чего, кроме их собственной информации о конфигурации, которая предоставляется во время сборки ее непосредственным родителем.

Другими словами, эти виджеты не должны заботиться о каких-либо изменениях после их создания.

Эти виджеты называются виджетами без состояния Stateless Widget.

Типичными примерами таких виджетов могут быть «Text», «Row», «Column», «Container»… где во время сборки мы просто передаем им некоторые параметры.

Параметры могут быть чем-либо из декораций, размеров или даже других виджетов. Это не имеет значения. Единственное, что важно, это то, что эта конфигурация после применения не изменится до следующего процесса сборки.

Виджет без состояния может быть нарисован только один раз при загрузке / сборке виджета, что означает, что этот виджет нельзя перерисовать на основе каких-либо событий или действий пользователя.
Жизненный цикл виджета без сохранения состояния Stateless Widget

Вот типичная структура кода, связанного с виджетом без сохранения состояния Stateless Widget.

Как вы можете видеть, мы можем передать некоторые дополнительные параметры его конструктору. Однако имейте в виду, что эти параметры НЕ будут изменяться на более позднем этапе и должны использоваться только как есть.

class MyStatelessWidget extends StatelessWidget {

	MyStatelessWidget({
		Key key,
		this.parameter,
	}): super(key:key);

	final parameter;

	@override
	Widget build(BuildContext context){
		return new ...
	}
}

Даже если есть другой метод, который может быть переопределен (createElement), последний едва ли будет переопределен. Единственное, что нужно переопределить — это build.

Stateful Widget

Некоторые другие виджеты будут обрабатывать некоторые внутренние данные, которые будут меняться в течение срока службы виджета. Эти данные, следовательно, становятся динамическими.

Набор данных, который хранится в этом виджете и который может изменяться в течение срока службы этого виджета, называется состоянием State.

Эти виджеты называются Stateful Widgets.

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

Понятие State

State определяет «поведенческую» часть экземпляра StatefulWidget.

Он содержит информацию, направленную на взаимодействие/вмешательство interacting / interferring в виджете в терминах:

  • поведение
  • макет
Любые изменения, которые применяются к State, вынуждают Виджет перестраиваться(rebuild).

Взаимосвязь между State и Context

Для Stateful widgets состояние связано с контекстом Context. Эта ассоциация является постоянной permanent, и объект State никогда не изменит своего контекста.

Даже если контекст виджета можно перемещать по древовидной структуре, состояние State будет оставаться связанным с этим контекстом Context.

Когда состояние State связано с контекстом Context, оно считается подключенным(mounted).

ГИПЕР ВАЖНО: Поскольку объект State связан с контекстом, это означает, что объект State НЕ (непосредственно) доступен через другой контекст! (мы еще обсудим это через несколько минут).

Stateful Widget жизненный цикл

Теперь, когда основные понятия были введены, пришло время погрузиться немного глубже …

Вот типичная структура кода, связанного с виджетом с отслеживанием состояния Stateful Widget.

Поскольку основная цель этой статьи состоит в том, чтобы объяснить понятие State в терминах «переменных» данных, я намеренно пропущу любое объяснение, касающееся некоторых переопределяемых( overridable ) методов Stateful Widget, которые конкретно не связаны с этим. Эти переопределяемые методы: didUpdateWidget, deactivate, reassemble . Это будет обсуждаться в следующей статье

class MyStatefulWidget extends StatefulWidget {

	MyStatefulWidget({
		Key key,
		this.parameter,
	}): super(key: key);
	
	final parameter;
	
	@override
	_MyStatefulWidgetState createState() => new _MyStatefulWidgetState();
}

class _MyStatefulWidgetState extends State {

	@override
	void initState(){
		super.initState();
		
		// Additional initialization of the State
	}
	
	@override
	void didChangeDependencies(){
		super.didChangeDependencies();
		
		// Additional code
	}
	
	@override
	void dispose(){
		// Additional disposal code
		
		super.dispose();
	}
	
	@override
	Widget build(BuildContext context){
		return new ...
	}
}

На следующей диаграмме показана (упрощенная версия) последовательность действий / вызовов, связанных с созданием виджета с отслеживанием состояния. В правой части диаграммы вы заметите внутренний статус объекта State во время потока. Вы также увидите момент, когда контекст связан с состоянием и, таким образом, станет доступным (mounted ).

Итак, давайте объясним это с некоторыми дополнительными деталями:

Метод initState()

Метод initState() — это самый первый метод (после конструктора), который вызывается после создания объекта State. Этот метод должен быть переопределен, когда вам нужно выполнить дополнительные инициализации. Типичные инициализации связаны с анимацией, контроллерами … Если вы переопределяете этот метод, вам нужно вызвать метод super.initState() и обычно сначала.

В этом методе контекст context доступен, но вы по-настоящему не можете его использовать, так как фреймворк еще не полностью связал с ним состояние.

Когда метод initState() завершен, объект State теперь инициализируется и контекст становится доступным.

Когда метод initState() завершен, объект State теперь инициализируется и контекст становится доступным.

Этот метод больше не будет вызываться во время жизни этого объекта State.

Метод didChangeDependencies()

Метод didChangeDependencies() — это второй вызываемый метод.

На данном этапе, когда контекст context доступен, вы можете использовать его.

Этот метод обычно переопределяется, если ваш виджет связан с InheritedWidget и / или если вам нужно инициализировать некоторых слушателей listeners (в зависимости от контекста context).

Обратите внимание, что если ваш виджет связан с InheritedWidget, этот метод будет вызываться каждый раз, когда этот виджет будет перестроен.

Если вы переопределите этот метод, вы должны сначала вызвать super.didChangeDependencies().

Метод build()

Метод build(BuildContext context) вызывается после didChangeDependencies()didUpdateWidget).

Это место, где вы строите свой виджет (и, возможно, любое поддерево).

Этот метод будет вызываться каждый раз, когда изменяется ваш объект State (или когда InheritedWidget должен уведомить «зарегистрированные» виджеты) !!

Для принудительного перестроения вы можете вызвать метод setState (() {…}).

Метод dispose()

Метод dispose() вызывается, когда виджет отбрасывается.

Переопределите этот метод, если вам нужно выполнить некоторую очистку (например, прослушиватели), а затем вызвать super.dispose() сразу после.

Stateless или Stateful Widget?

Это вопрос, который многие разработчики должны задать себе: нужен ли мой виджет без сохранения состояния( Stateless ) или с сохранением состояния( Stateful )?

Чтобы ответить на этот вопрос, задайте себе вопрос:

Нужно ли учитывать переменную, которая меняется во время жизни моего виджета и, если он будет изменен, заставит виджет перестраиваться.

Если ответ на вопрос «да», то вам нужен виджет с сохранением состояния Stateful , в противном случае вам нужен виджет без сохранения состояния Stateless .

Некоторые примеры:

  • виджет для отображения списка флажков. Для отображения флажков необходимо учитывать массив элементов. Каждый элемент — это объект с заголовком и статусом. Если вы установите флажок, соответствующий элемент item.status будет переключен;

В этом случае вам нужно использовать виджет Stateful, чтобы запомнить состояние элементов, чтобы иметь возможность перерисовать флажки.

  • экран с формой. Экран позволяет пользователю заполнять виджеты формы и отправлять форму на сервер.

В этом случае, если вам не требуется проверять форму или выполнять какие-либо другие действия перед ее отправкой, может быть достаточно виджета без сохранения состоянияStateless .

Stateful Widget состоит из 2 частей

Помните структуру виджета с состоянием Stateful ? Есть 2 части:

1. Основное определение виджета
class MyStatefulWidget extends StatefulWidget {
    MyStatefulWidget({
		Key key,
		this.color,
	}): super(key: key);
	
	final Color color;

	@override
	_MyStatefulWidgetState createState() => new _MyStatefulWidgetState();
}

Первая часть «MyStatefulWidget» обычно является публичной( public ) частью виджета. Вы создаете экземпляр этой части, когда хотите добавить ее в дерево виджетов. Эта часть не изменяется во время жизни виджета, но может принимать параметры, которые могут использоваться его соответствующим экземпляром состояния State.

Обратите внимание, что любая переменная, определенная на уровне этой первой части виджета, обычно НЕ будет меняться в течение своего срока службы.
2. Определение состояния виджета
class _MyStatefulWidgetState extends State {
    ...
	@override
	Widget build(BuildContext context){
	    ...
	}
}

Вторая часть «MyStatefulWidgetState» — это часть, которая изменяется в течение срока службы виджета и вынуждает этот конкретный экземпляр виджета перестраиваться при каждом применении модификации. Символ «_» в начале имени делает класс закрытым для файла .dart.

Если вам нужно сделать ссылку на этот класс вне файла .dart, не используйте префикс «_» .

Класс _MyStatefulWidgetState может обращаться к любой переменной, которая хранится в MyStatefulWidget, используя widget. {Имя переменной}. В этом примере: widget.color .


Уникальная идентификация виджета

В Flutter каждый виджет уникален. Этот уникальный идентификатор определяется платформой во время сборки / рендеринга.

Этот уникальный идентификатор соответствует необязательному параметру Key. Если опущено, Flutter сгенерирует один для вас.

В некоторых случаях может потребоваться принудительное использование этого ключа, чтобы вы могли получить доступ к виджету по его ключу.

Для этого вы можете использовать один из следующих помощников: GlobalKey, LocalKey, UniqueKey или ObjectKey.

GlobalKey гарантирует, что ключ уникален во всем приложении.

Чтобы создать уникальную идентификацию виджета:

    GlobalKey myKey = new GlobalKey();
    ...
    @override
    Widget build(BuildContext context){
        return new MyWidget(
            key: myKey
        );
    }

Часть 2. Как получить доступ к объекту State во Flutter?

Как было объяснено ранее, состояние State связано с одним контекстом, а контекст связан с экземпляром виджета.

1. Сам виджет

Теоретически, единственным, кто может получить доступ к состоянию State , является сам State-виджет.

В этом случае нет никаких трудностей. Класс State виджета обращается к любой из своих переменных.

2. Непосредственный дочерний виджет

Иногда родительскому виджету может потребоваться получить доступ к состоянию одного из его прямых потомков для выполнения определенных задач.

В этом случае, чтобы получить доступ к этим прямым дочерним состояниям State, вам необходимо их знать.

Самый простой способ вызвать кого-то или чего-то — это через имя. Во Flutter каждый виджет имеет уникальную идентичность, которая определяется во время сборки / рендеринга платформой. Как было показано ранее, вы можете принудительно идентифицировать виджет, используя параметр ключа key .

    ...
    GlobalKey myWidgetStateKey = new GlobalKey();
    ...
    @override
    Widget build(BuildContext context){
        return new MyStatefulWidget(
            key: myWidgetStateKey,
            color: Colors.blue,
        );
    }

После идентификации родительский parent виджет может получить доступ к состоянию State своего потомка через myWidgetStateKey.currentState .

Давайте рассмотрим базовый пример, который показывает SnackBar, когда пользователь нажимает кнопку. Поскольку SnackBar является дочерним виджетом Scaffold , он не доступен напрямую любому другому потомку тела Scaffold (помните понятие контекста и его иерархию / древовидную структуру?). Следовательно, единственный способ получить к нему доступ — через ScaffoldState, который предоставляет открытый метод для отображения SnackBar.

    class _MyScreenState extends State {
        /// the unique identity of the Scaffold
        final GlobalKey _scaffoldKey = new GlobalKey();

        @override
        Widget build(BuildContext context){
            return new Scaffold(
                key: _scaffoldKey,
                appBar: new AppBar(
                    title: new Text('My Screen'),
                ),
                body: new Center(
                    new RaiseButton(
                        child: new Text('Hit me'),
                        onPressed: (){
                            _scaffoldKey.currentState.showSnackBar(
                                new SnackBar(
                                    content: new Text('This is the Snackbar...'),
                                )
                            );
                        }
                    ),
                ),
            );
        }
    }

3. Предок Виджет

Предположим, что у вас есть виджет, который принадлежит поддереву другого виджета, как показано на следующей диаграмме

Для этого необходимо выполнить 3 условия:

1. «Виджет со State» (в красном) должен раскрыть его состояние State

Чтобы раскрыть свое состояние State, виджет должен записать его во время создания следующим образом:

class MyExposingWidget extends StatefulWidget {

   MyExposingWidgetState myState;
	
   @override
   MyExposingWidgetState createState(){
      myState = new MyExposingWidgetState();
      return myState;
   }
}
2. «Состояние State виджета» должно выставлять некоторые методы getters/setters

Чтобы позволить «незнакомцу» установить / получить (get/set) свойство State, состоянию виджета State необходимо разрешить доступ посредством:

  • public property (not recommended)
  • getter / setter

Пример:

class MyExposingWidgetState extends State{
   Color _color;
	
   Color get color => _color;
   ...
}
3. «Виджет, заинтересованный в получении состояния State» (синим цветом) должен получить ссылку на состояние State
class MyChildWidget extends StatelessWidget {
   @override
   Widget build(BuildContext context){
      final MyExposingWidget widget = context.ancestorWidgetOfExactType(MyExposingWidget);
      final MyExposingWidgetState state = widget?.myState;
		
      return new Container(
         color: state == null ? Colors.blue : state.color,
      );
   }
}

Это решение легко реализовать, но как дочерний виджет узнает, когда его нужно перестроить?

С этим решением это не так. Придется подождать, пока произойдет перестроение, чтобы обновить его содержимое, что не очень удобно.

В следующем разделе рассматривается понятие Inherited Widget, которое дает решение этой проблемы.


InheritedWidget

Вкратце и простыми словами, InheritedWidget позволяет эффективно распространять информацию по дереву виджетов.

InheritedWidget — это специальный виджет, который вы помещаете в дерево виджетов как родительский элемент другого поддерева. Все виджеты, входящие в это поддерево, будут иметь возможность взаимодействовать с данными, предоставляемыми этим InheritedWidget.

Основы

Чтобы объяснить это, давайте рассмотрим следующий фрагмент кода:

class MyInheritedWidget extends InheritedWidget {
   MyInheritedWidget({
      Key key,
      @required Widget child,
      this.data,
   }): super(key: key, child: child);
	
   final data;
	
   static MyInheritedWidget of(BuildContext context) {
      return context.inheritFromWidgetOfExactType(MyInheritedWidget);
   }

   @override
   bool updateShouldNotify(MyInheritedWidget oldWidget) => data != oldWidget.data;
}

Этот код определяет виджет с именем «MyInheritedWidget», предназначенный для «совместного использования» некоторых данных между всеми виджетами, являющимися частью дочернего поддерева.

Как упоминалось ранее, InheritedWidget должен располагаться в верхней части дерева виджетов, чтобы иметь возможность распространять / делиться некоторыми данными, это объясняет дочерний элемент @required Widget, который передается базовому конструктору InheritedWidget.

Статический метод static MyInheritedWidget of(BuildContext context) позволяет всем дочерним виджетам получить экземпляр ближайшего MyInheritedWidget, который окружает контекст (см. Далее).

Наконец, переопределенный метод updateShouldNotify используется, чтобы сообщить InheritedWidget, нужно ли передавать уведомления всем дочерним виджетам (которые зарегистрированы / подписаны), если к данным будет применена модификация (см. Позже).

Поэтому нам нужно поместить его на уровне узла дерева следующим образом:

class MyParentWidget... {
   ...
   @override
   Widget build(BuildContext context){
      return new MyInheritedWidget(
         data: counter,
         child: new Row(
            children: [
               ...
            ],
         ),
      );
   }
}

Как дочерний элемент получает доступ к данным InheritedWidget?

Во время создания дочернего элемента последний получит ссылку на InheritedWidget следующим образом:

class MyChildWidget... {
   ...
	
   @override
   Widget build(BuildContext context){
      final MyInheritedWidget inheritedWidget = MyInheritedWidget.of(context);
		
      ///
      /// From this moment, the widget can use the data, exposed by the MyInheritedWidget
      /// by calling:  inheritedWidget.data
      ///
      return new Container(
         color: inheritedWidget.data.color,
      );
   }
}

Как сделать взаимодействие между виджетами?

Рассмотрим следующую диаграмму, которая показывает древовидную структуру виджетов

Чтобы проиллюстрировать тип взаимодействия, предположим следующее:

  • «Виджет А» — это кнопка, которая добавляет товар в корзину;
  • «Виджет B» — это текст, отображающий количество товаров в корзине;
  • «Виджет C» находится рядом с виджетом B и представляет собой текст с любым текстом внутри;
  • Мы хотим, чтобы «Виджет B» автоматически отображал правильное количество товаров в корзине, как только нажимается «Виджет А», но мы не хотим, чтобы «Виджет С» восстанавливался.

InheritedWidget — это именно тот виджет, который можно использовать для этого!

Пример по коду

Давайте сначала напишем код, и объяснения будут следовать:

class Item {
   String reference;

   Item(this.reference);
}

class _MyInherited extends InheritedWidget {
  _MyInherited({
    Key key,
    @required Widget child,
    @required this.data,
  }) : super(key: key, child: child);

  final MyInheritedWidgetState data;

  @override
  bool updateShouldNotify(_MyInherited oldWidget) {
    return true;
  }
}

class MyInheritedWidget extends StatefulWidget {
  MyInheritedWidget({
    Key key,
    this.child,
  }): super(key: key);

  final Widget child;

  @override
  MyInheritedWidgetState createState() => new MyInheritedWidgetState();

  static MyInheritedWidgetState of(BuildContext context){
    return (context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited).data;
  }
}

class MyInheritedWidgetState extends State{
  /// List of Items
  List _items = [];

  /// Getter (number of items)
  int get itemsCount => _items.length;

  /// Helper method to add an Item
  void addItem(String reference){
    setState((){
      _items.add(new Item(reference));
    });
  }

  @override
  Widget build(BuildContext context){
    return new _MyInherited(
      data: this,
      child: widget.child,
    );
  }
}

class MyTree extends StatefulWidget {
  @override
  _MyTreeState createState() => new _MyTreeState();
}

class _MyTreeState extends State {
  @override
  Widget build(BuildContext context) {
    return new MyInheritedWidget(
      child: new Scaffold(
        appBar: new AppBar(
          title: new Text('Title'),
        ),
        body: new Column(
          children: [
            new WidgetA(),
            new Container(
              child: new Row(
                children: [
                  new Icon(Icons.shopping_cart),
                  new WidgetB(),
                  new WidgetC(),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class WidgetA extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final MyInheritedWidgetState state = MyInheritedWidget.of(context);
    return new Container(
      child: new RaisedButton(
        child: new Text('Add Item'),
        onPressed: () {
          state.addItem('new item');
        },
      ),
    );
  }
}

class WidgetB extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final MyInheritedWidgetState state = MyInheritedWidget.of(context);
    return new Text('${state.itemsCount}');
  }
}

class WidgetC extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Text('I am Widget C');
  }
}

Пояснения

В этом очень простом примере:

  • _MyInherited — это InheritedWidget, который воссоздается каждый раз, когда мы добавляем элемент посредством нажатия на кнопку «Виджет A».
  • MyInheritedWidget — это виджет с состоянием, который содержит список элементов. Это состояние доступно через  static MyInheritedWidgetState.of(BuildContext context)
  • MyInheritedWidgetState предоставляет один метод getter ( itemsCount ) и один метод ( addItem ), чтобы они могли использоваться виджетами, являющимися частью дерева дочерних виджетов.
  • Каждый раз, когда мы добавляем элемент в состояние, MyInheritedWidgetState перестраивает
  • Класс MyTree просто создает дерево виджетов, имея MyInheritedWidget вкачестве родителя дерева
  • WidgetA — это простой RaisedButton, который при нажатии вызывает метод addItem из ближайшего MyInheritedWidget
  • WidgetB — это простой текст, который отображает количество элементов, представленных на уровне ближайшего MyInheritedWidget

Как все это работает?

Регистрация виджета для последующих уведомлений

Когда дочерний виджет вызывает MyInheritedWidget.of ( context ) , он вызывает следующий метод MyInheritedWidget, передавая свой собственный контекст .

 
static MyInheritedWidgetState of(BuildContext context) {
	return (context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited).data;
  }

Внутренне, поверх простого возврата экземпляра MyInheritedWidgetState, он также подписывает потребительский( consumer ) виджет на уведомления об изменениях.

За кулисами простой вызов этого статического метода фактически делает 2 вещи:

  • потребитель ( consumer )  виджет автоматически добавляются в список абонентов , которые будут перестроены , когда модификация применяется к InheritedWidget (здесь _MyInherited )
  • что данные , указанные в _MyInherited виджета (ака MyInheritedWidgetState ) возвращается к потребителю

Поток

Поскольку и «Виджет А», и «Виджет Б» подписаны на InheritedWidget, так что, если модификация применяется к _MyInherited , поток операций будет следующим (упрощенная версия) при нажатии RaisedButton для Виджета А:

  1. Вызов производится с AddItem методом MyInheritedWidgetState
  2. Метод MyInheritedWidgetState.addItem добавляет новый элемент в список
  3. setState () вызывается для перестройки MyInheritedWidget
  4. Создается новый экземпляр _MyInherited с новым содержимым списка.
  5. _MyInherited записывает новое состояние, которое передается в аргументе ( данных )
  6. Как InheritedWidget , он проверяет , есть ли необходимость уведомить о потребителях (ответ верно)
  7. Он перебирает весь список потребителей (здесь виджеты A и W) и просит их перестроить
  8. Поскольку Wiget C не является потребителем , он не перестраивается.

Так что это работает!

Однако оба виджета A и W виджета B перестраиваются, а перестраивать Wiget A бесполезно, поскольку для него ничего не изменилось. Как предотвратить это?

Не позволяйте некоторым виджетам перестраиваться при доступе к унаследованному виджету

Причина, по которой Widget A также был перестроен, связана с тем, как он обращается к MyInheritedWidgetState .

Как мы видели ранее, факт вызова метода context.inheritFromWidgetOfExactType() автоматически подписывает виджет на список потребителей .

Решение для предотвращения этой автоматической подписки при одновременном предоставлении Widget A доступа к MyInheritedWidgetState заключается в изменении статического метода MyInheritedWidget следующим образом:

  
static MyInheritedWidgetState of([BuildContext context, bool rebuild = true]){
    return (rebuild ? context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited
                    : context.ancestorWidgetOfExactType(_MyInherited) as _MyInherited).data;
  }

Добавляя логический дополнительный параметр…

  • Если параметр rebuild равен true (по умолчанию), мы используем обычный подход (и виджет будет добавлен в список подписчиков)
  • Если восстановить параметр является ложным, мы до сих пор получить доступ к данным , но без использования внутренней реализации в InheritedWidget

Итак, чтобы завершить решение, нам также нужно немного обновить код виджета A следующим образом (мы добавили ложный дополнительный параметр):

class WidgetA extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final MyInheritedWidgetState state = MyInheritedWidget.of(context, false);
    return new Container(
      child: new RaisedButton(
        child: new Text('Add Item'),
        onPressed: () {
          state.addItem('new item');
        },
      ),
    );
  }
}

Вот и все, виджет А больше не перестраивается, когда мы нажимаем на него.

Специальное примечание для маршрутов, диалогов …

Маршруты, контексты диалогов привязаны к приложению. Это означает, что даже если внутри экрана A вы запрашиваете отображение другого экрана B (например, поверх текущего), на любом из двух экранов нет простого способа связать их собственный контекст. Единственный способ узнать на экране B что-либо о контексте экрана A — это получить его с экрана A в качестве параметра Navigator.of (context) .push (….)

Выводы

По этим темам еще так много можно сказать … особенно по InheritedWidget .

В следующей статье я представлю понятие уведомителями / радиослушателей, который также очень интересно использовать в контексте государства и пути передачи данных.

Итак, следите за обновлениями и счастливого кодирования.

Официальный источник данной статьи https://www.didierboelens.com/2018/06/widget—state—context—inheritedwidget/

Использование пакетов

Flutter поддерживает использование общих пакетов, предоставленных другими разработчиками для экосистем Flutter и Dart. Это позволяет быстро создать приложение без необходимости разрабатывать все с нуля.

Существующие пакеты включают множество вариантов использования, например, создание сетевых запросов (http), настраиваемая навигация / обработка маршрутов (fluro), интеграция с API-интерфейсами устройств (url_launcher и батарея) и использование SDK сторонних платформ, таких как Firebase (FlutterFire).

Чтобы разработать новый пакет, см. Разработку пакетов. Чтобы добавить ресурсы, изображения или шрифты, хранящиеся в файлах или пакетах, см. Добавление ресурсов и изображений.

Использование пакетов

В следующем разделе описано, как использовать существующие опубликованные пакеты.

Поиск пакетов

Пакеты публикуются на сайте Pub.

На целевой странице Flutter на сайте Pub отображаются основные пакеты, совместимые с Flutter (те, которые объявляют зависимости, обычно совместимые с Flutter), и поддерживается поиск среди всех опубликованных пакетов.

Добавление зависимости пакета в приложение

Чтобы добавить пакет css_colors в приложение:

  1. Указываем на новую зависимость:
    • Открываем файл pubspec.yaml, расположенный в папке приложения, и добавляем css_colors: в ​​зависимости, указав его в виде значения свойству dependencies.
  2. Устанавливаем его:
    • Из терминала набираем команду flutter pub get.
    • Или, из панели Android Studio/IntelliJ: жмем Packages get в списке действий в верхней части файла pubspec.yaml.
    • Или, из панели VS Code: жмем Get Packages расположен в правой части списка действий в верхней части pubspec.yaml.
  3. Импортируем пакет:
    • Добавляем соответствующий оператор импорта import в коде Dart.
  4. Останавливаем и перезапускаем приложение, если это необходимо
    • Если пакет содержит специфичный для платформы код (Java / Kotlin для Android, Swift / Objective-C для iOS), этот код должен быть встроен в ваше приложение. Горячая перезагрузка и горячий перезапуск обновляют только код Dart, поэтому может потребоваться полный перезапуск приложения, чтобы избежать ошибок, таких как MissingPluginException при использовании пакета.

Вкладка «Установка», доступная на любой странице пакета в Pub, является удобным справочником для этих шагов.

Для полного примера см. Пример css_colors ниже.

Решение конфликта

Предположим, вы хотите использовать в приложении some_package и another_package, и оба они зависят от url_launcher, но в разных версиях. Это вызывает потенциальный конфликт. Лучший способ избежать этого — для авторов пакетов использовать диапазоны версий, а не конкретные версии при указании зависимостей.

dependencies:
  url_launcher: ^0.4.2    # Хорошо, любая 0.4.x версия, где x >= 2 работает.
  image_picker: '0.1.1'   # Это не хорошо, работает только версия 0.1.1.

Если some_package объявляет вышеуказанные зависимости, а another_package объявляет совместимую зависимость url_launcher, такую как «0.4.5» или ^0.4.0, то Pub автоматически решает проблему. Зависимости от платформы для модулей Gradle и/или CocoaPods решаются аналогичным образом.

Анимируем виджет по экранам

Часто бывает полезно направлять пользователей через приложение при переходе с экрана на экран. Распространенная техника, позволяющая проводить пользователей через приложение, — анимировать виджет с одного экрана на другой. Это создает визуальную привязку, соединяющую два экрана.

Используйте виджет «Герой»(Hero), чтобы анимировать виджет с одного экрана на другой. Этот рецепт использует следующие шаги:

  1. Создайте два экрана, показывающие одно и то же изображение.
  2. Добавьте виджет Hero на первый экран.
  3. Добавьте виджет Hero на второй экран.

1. Создаем два экрана, показывающие одно и то же изображение

В этом примере отобразите одно и то же изображение на обоих экранах. Анимируйте изображение с первого экрана на второй, когда пользователь нажимает на изображение. Пока создайте визуальную структуру; обрабатывать анимацию на следующих шагах.

Примечание. Этот пример основан на Navigate to a new screen and back и рецептов Handle taps .
class MainScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Main Screen'),
      ),
      body: GestureDetector(
        onTap: () {
          Navigator.push(context, MaterialPageRoute(builder: (_) {
            return DetailScreen();
          }));
        },
        child: Image.network(
          'https://picsum.photos/250?image=9',
        ),
      ),
    );
  }
}

class DetailScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: GestureDetector(
        onTap: () {
          Navigator.pop(context);
        },
        child: Center(
          child: Image.network(
            'https://picsum.photos/250?image=9',
          ),
        ),
      ),
    );
  }
}

2. Добавляем виджет Hero на первый экран.

Чтобы соединить два экрана вместе с анимацией, оберните виджет Image на обоих экранах в виджет Hero. Виджет Hero требует двух аргументов:

  • tag — Объект, который идентифицирует Hero. Он должен быть одинаковым на обоих экранах.
  • child — Виджет для анимации по экранам.

Виджет для анимации по экранам.

Hero(
  tag: 'imageHero',
  child: Image.network(
    'https://picsum.photos/250?image=9',
  ),
);

3. Добавьте виджет Hero на второй экран

Чтобы завершить соединение с первым экраном, оберните Image на втором экране виджетом Hero, который имеет тот же tag, что и герой Hero на первом экране.

После применения виджета Hero ко второму экрану анимация между экранами просто работает.

Hero(
  tag: 'imageHero',
  child: Image.network(
    'https://picsum.photos/250?image=9',
  ),
);
Примечание: этот код идентичен тому, что у вас есть на первом экране. Рекомендуется создавать многократно используемый виджет вместо повторяющегося кода. Этот пример использует идентичный код для обоих виджетов, для простоты.

Полный пример

import 'package:flutter/material.dart';

void main() => runApp(HeroApp());

class HeroApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Transition Demo',
      home: MainScreen(),
    );
  }
}

class MainScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Main Screen'),
      ),
      body: GestureDetector(
        child: Hero(
          tag: 'imageHero',
          child: Image.network(
            'https://picsum.photos/250?image=9',
          ),
        ),
        onTap: () {
          Navigator.push(context, MaterialPageRoute(builder: (_) {
            return DetailScreen();
          }));
        },
      ),
    );
  }
}

class DetailScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: GestureDetector(
        child: Center(
          child: Hero(
            tag: 'imageHero',
            child: Image.network(
              'https://picsum.photos/250?image=9',
            ),
          ),
        ),
        onTap: () {
          Navigator.pop(context);
        },
      ),
    );
  }
}
Результат в визуальном виде

Данная статься была переведена с официального ресурса по ссылке Animating a widget across screens.

Возврат данных с экрана

В некоторых случаях вы можете захотеть вернуть данные с нового экрана. Например, скажем, вы нажимаете новый экран, который предоставляет пользователю два варианта. Когда пользователь нажимает на опцию, вы хотите сообщить первому экрану о выборе пользователя, чтобы он мог воздействовать на эту информацию.

Вы можете сделать это с помощью метода Navigator.pop(), выполнив следующие действия:

  1. Определить домашний экран
  2. Добавить кнопку, которая запускает экран выбора
  3. Показать экран выбора с двумя кнопками
  4. Когда кнопка нажата, закройте экран выбора
  5. Показать снэк-бар на главном экране с выбором

1. Определите домашний экран

На главном экране отображается кнопка. При нажатии запускается экран выбора.

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Returning Data Demo'),
      ),
      // Create the SelectionButton widget in the next step.
      body: Center(child: SelectionButton()),
    );
  }
}

2. Добавьте кнопку, которая запускает экран выбора

Теперь создайте SelectionButton, который делает следующее:

  1. Запускает SelectionScreen, когда он коснулся.
  2. Ожидает, что SelectionScreen вернет результат.
class SelectionButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RaisedButton(
      onPressed: () {
        _navigateAndDisplaySelection(context);
      },
      child: Text('Pick an option, any option!'),
    );
  }

  // A method that launches the SelectionScreen and awaits the
  // result from Navigator.pop.
  _navigateAndDisplaySelection(BuildContext context) async {
    // Navigator.push returns a Future that completes after calling
    // Navigator.pop on the Selection Screen.
    final result = await Navigator.push(
      context,
      // Create the SelectionScreen in the next step.
      MaterialPageRoute(builder: (context) => SelectionScreen()),
    );
  }
}

3. Показать экран выбора с помощью двух кнопок

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

Этот шаг определяет пользовательский интерфейс. Следующий шаг добавляет код для возврата данных.

class SelectionScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Pick an option'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: RaisedButton(
                onPressed: () {
                  // Pop here with "Yep"...
                },
                child: Text('Yep!'),
              ),
            ),
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: RaisedButton(
                onPressed: () {
                  // Pop here with "Nope"
                },
                child: Text('Nope.'),
              ),
            )
          ],
        ),
      ),
    );
  }
}

4. При нажатии кнопки закройте экран выбора.

Теперь обновите функцию обратного вызова onPressed() для обеих кнопок. Чтобы вернуть данные на первый экран, используйте метод Navigator.pop(), который принимает необязательный второй аргумент с именем result. Любой результат возвращается в Future в SelectionButton.

Есть кнопка

RaisedButton(
  onPressed: () {
    // The Yep button returns "Yep!" as the result.
    Navigator.pop(context, 'Yep!');
  },
  child: Text('Yep!'),
);

Нет кнопки

RaisedButton(
  onPressed: () {
    // The Nope button returns "Nope!" as the result.
    Navigator.pop(context, 'Nope!');
  },
  child: Text('Nope!'),
);

5. Показать снэк-бар на главном экране с выбором

Теперь, когда вы запускаете экран выбора и ожидаете результата, вы захотите что-то сделать с возвращенной информацией.

В этом случае покажите снэк-бар, отображающий результат, используя метод _navigateAndDisplaySelection() в SelectionButton:

_navigateAndDisplaySelection(BuildContext context) async {
  final result = await Navigator.push(
    context,
    MaterialPageRoute(builder: (context) => SelectionScreen()),
  );

  // After the Selection Screen returns a result, hide any previous snackbars
  // and show the new result.
  Scaffold.of(context)
    ..removeCurrentSnackBar()
    ..showSnackBar(SnackBar(content: Text("$result")));
}

Полный пример

import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(
    title: 'Returning Data',
    home: HomeScreen(),
  ));
}

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Returning Data Demo'),
      ),
      body: Center(child: SelectionButton()),
    );
  }
}

class SelectionButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RaisedButton(
      onPressed: () {
        _navigateAndDisplaySelection(context);
      },
      child: Text('Pick an option, any option!'),
    );
  }

  // A method that launches the SelectionScreen and awaits the result from
  // Navigator.pop.
  _navigateAndDisplaySelection(BuildContext context) async {
    // Navigator.push returns a Future that completes after calling
    // Navigator.pop on the Selection Screen.
    final result = await Navigator.push(
      context,
      MaterialPageRoute(builder: (context) => SelectionScreen()),
    );

    // After the Selection Screen returns a result, hide any previous snackbars
    // and show the new result.
    Scaffold.of(context)
      ..removeCurrentSnackBar()
      ..showSnackBar(SnackBar(content: Text("$result")));
  }
}

class SelectionScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Pick an option'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: RaisedButton(
                onPressed: () {
                  // Close the screen and return "Yep!" as the result.
                  Navigator.pop(context, 'Yep!');
                },
                child: Text('Yep!'),
              ),
            ),
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: RaisedButton(
                onPressed: () {
                  // Close the screen and return "Nope!" as the result.
                  Navigator.pop(context, 'Nope.');
                },
                child: Text('Nope.'),
              ),
            )
          ],
        ),
      ),
    );
  }
}
Результат в визуальном виде

Данная статься является переводом официальной статьи по ссылке Return data from a screen.

Отправка данных на новый экран

Часто вы хотите не только перейти на новый экран, но и передать данные на экран. Например, вы можете передать информацию об элементе, который был прослушан.

Помните: экраны — это просто виджеты. В этом примере создайте список задач. При касании задачи перейдите на новый экран (виджет), в котором отображается информация о задаче. Этот рецепт использует следующие шаги:

  1. Определить класс задач.
  2. Показать список задач.
  3. Создайте подробный экран, который может отображать информацию о задачах.
  4. Перейдите и передайте данные на подробный экран.

1. Определяем класс задач

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

class Todo {
  final String title;
  final String description;

  Todo(this.title, this.description);
}

2. Создаем список задач

Во-вторых, отобразить список задач. В этом примере сгенерируйте 20 задач и покажите их с помощью ListView. Для получения дополнительной информации о работе со списками, см. Рецепт Использовать списки.

Генерация списка задач

final todos = List.generate(
  20,
  (i) => Todo(
        'Todo $i',
        'A description of what needs to be done for Todo $i',
      ),
);

Отображаем список задач, используя ListView

ListView.builder(
  itemCount: todos.length,
  itemBuilder: (context, index) {
    return ListTile(
      title: Text(todos[index].title),
    );
  },
);

Все идет нормально. Это создает 20 задач и отображает их в ListView.

3. Создайте подробный экран для отображения информации о задачах.

Теперь создаем второй экран. Заголовок экрана содержит заголовок задачи, а тело экрана показывает описание.

Поскольку подробный экран является обычным StatelessWidget, требуется, чтобы пользователь ввел Todo в пользовательском интерфейсе. Затем создайте пользовательский интерфейс, используя заданную задачу.

class DetailScreen extends StatelessWidget {
  // Declare a field that holds the Todo.
  final Todo todo;

  // In the constructor, require a Todo.
  DetailScreen({Key key, @required this.todo}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // Use the Todo to create the UI.
    return Scaffold(
      appBar: AppBar(
        title: Text(todo.title),
      ),
      body: Padding(
        padding: EdgeInsets.all(16.0),
        child: Text(todo.description),
      ),
    );
  }
}

4. Перейдите и передайте данные на подробный экран

С подробным экраном DetailScreen вы готовы выполнить навигацию. В этом примере перейдите к DetailScreen, когда пользователь нажимает на задачу в списке. Передайте задачу на DetailScreen.

Чтобы захватить касание пользователя, напишите обратный вызов onTap() для виджета ListTile. В обратном вызове onTap() используйте метод Navigator.push().

ListView.builder(
  itemCount: todos.length,
  itemBuilder: (context, index) {
    return ListTile(
      title: Text(todos[index].title),
      // When a user taps the ListTile, navigate to the DetailScreen.
      // Notice that you're not only creating a DetailScreen, you're
      // also passing the current todo to it.
      onTap: () {
        Navigator.push(
          context,
          MaterialPageRoute(
            builder: (context) => DetailScreen(todo: todos[index]),
          ),
        );
      },
    );
  },
);

Полный пример

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

class Todo {
  final String title;
  final String description;

  Todo(this.title, this.description);
}

void main() {
  runApp(MaterialApp(
    title: 'Passing Data',
    home: TodosScreen(
      todos: List.generate(
        20,
        (i) => Todo(
              'Todo $i',
              'A description of what needs to be done for Todo $i',
            ),
      ),
    ),
  ));
}

class TodosScreen extends StatelessWidget {
  final List todos;

  TodosScreen({Key key, @required this.todos}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Todos'),
      ),
      body: ListView.builder(
        itemCount: todos.length,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text(todos[index].title),
            // When a user taps the ListTile, navigate to the DetailScreen.
            // Notice that you're not only creating a DetailScreen, you're
            // also passing the current todo through to it.
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => DetailScreen(todo: todos[index]),
                ),
              );
            },
          );
        },
      ),
    );
  }
}

class DetailScreen extends StatelessWidget {
  // Declare a field that holds the Todo.
  final Todo todo;

  // In the constructor, require a Todo.
  DetailScreen({Key key, @required this.todo}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // Use the Todo to create the UI.
    return Scaffold(
      appBar: AppBar(
        title: Text(todo.title),
      ),
      body: Padding(
        padding: EdgeInsets.all(16.0),
        child: Text(todo.description),
      ),
    );
  }
}
Конечный визуальный результат

Данная статья является переводом официальной стать по ссылке Send data to a new screen

Навигация по именованным маршрутам

В статье Навигации к новому экрану и в обратно мы узнали, как перейти к новому экрану, создав новый маршрут и передав его в навигатор Navigator.

Однако, если вам нужно перейти к одному и тому же экрану во многих частях вашего приложения, такой подход может привести к дублированию кода. Решение состоит в том, чтобы определить именованный маршрут и использовать именованный маршрут для навигации.

Для работы с именованными маршрутами используйте функцию Navigator.pushNamed(). В этом примере реплицируется функциональность из исходного рецепта, демонстрируя, как использовать именованные маршруты, выполнив следующие шаги:

  1. Создайте два экрана.
  2. Определите маршруты.
  3. Перейдите на второй экран, используя Navigator.pushNamed().
  4. Вернитесь к первому экрану с помощью Navigator.pop().

1. Создаем два экрана

Сначала создайте два экрана для работы. Первый экран содержит кнопку для перехода ко второму экрану. Второй экран содержит кнопку, которая возвращает вас к первому.

class FirstScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('First Screen'),
      ),
      body: Center(
        child: RaisedButton(
          child: Text('Launch screen'),
          onPressed: () {
            // Navigate to the second screen when tapped.
          },
        ),
      ),
    );
  }
}

class SecondScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Second Screen"),
      ),
      body: Center(
        child: RaisedButton(
          onPressed: () {
            // Navigate back to first screen when tapped.
          },
          child: Text('Go back!'),
        ),
      ),
    );
  }
}

2. Определение маршрутов

Затем определяем маршруты, предоставив дополнительные свойства для конструктора MaterialApp: initialRoute и сами маршруты routes.

Свойство initialRoute определяет, с какого маршрута должно начинаться приложение. Свойство routes определяет доступные именованные маршруты и виджеты, которые нужно построить при переходе к этим маршрутам.

MaterialApp(
  // Start the app with the "/" named route. In this case, the app starts
  // on the FirstScreen widget.
  initialRoute: '/',
  routes: {
    // When navigating to the "/" route, build the FirstScreen widget.
    '/': (context) => FirstScreen(),
    // When navigating to the "/second" route, build the SecondScreen widget.
    '/second': (context) => SecondScreen(),
  },
);
Предупреждение: при использовании initialRoute не определяйте свойство home.

3. Перейдем на второй экран

При наличии виджетов и маршрутов запустите навигацию с помощью метода Navigator.pushNamed(). Это говорит Flutter построить виджет, определенный в таблице маршрутов routes, и запустить экран.

В методе build() виджета FirstScreen обновите обратный вызов onPressed():

// Within the `FirstScreen` widget
onPressed: () {
  // Navigate to the second screen using a named route.
  Navigator.pushNamed(context, '/second');
}

4. Вернемся к первому экрану

Чтобы вернуться к первому экрану, используйте функцию Navigator.pop().

// Within the SecondScreen widget
onPressed: () {
  // Navigate back to the first screen by popping the current route
  // off the stack.
  Navigator.pop(context);
}

Полный пример

import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(
    title: 'Named Routes Demo',
    // Start the app with the "/" named route. In this case, the app starts
    // on the FirstScreen widget.
    initialRoute: '/',
    routes: {
      // When navigating to the "/" route, build the FirstScreen widget.
      '/': (context) => FirstScreen(),
      // When navigating to the "/second" route, build the SecondScreen widget.
      '/second': (context) => SecondScreen(),
    },
  ));
}

class FirstScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('First Screen'),
      ),
      body: Center(
        child: RaisedButton(
          child: Text('Launch screen'),
          onPressed: () {
            // Navigate to the second screen using a named route.
            Navigator.pushNamed(context, '/second');
          },
        ),
      ),
    );
  }
}

class SecondScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Second Screen"),
      ),
      body: Center(
        child: RaisedButton(
          onPressed: () {
            // Navigate back to the first screen by popping the current route
            // off the stack.
            Navigator.pop(context);
          },
          child: Text('Go back!'),
        ),
      ),
    );
  }
}
Конечный визуальный результат

Данная статься является переводом официальной статьи на официальной статьи по ссылке Navigate with named routes.

Переход на новый экран и обратно

Большинство приложений содержат несколько экранов для отображения различных типов информации. Например, приложение может иметь экран, на котором отображаются продукты. Когда пользователь нажимает на изображение продукта, на новом экране отображаются сведения о продукте.

Терминология: во Flutter экраны и страницы называются маршрутами. Остальная часть этой статьи относится к маршрутам.

В Android маршрут эквивалентен действию. В iOS маршрут эквивалентен ViewController. Во Flutter маршрут — это просто виджет.

Переход к новому маршруту осуществляется с помощью навигатора Navigator.

Следующая работа над кодом покажет нам, как перемещаться между двумя маршрутами, используя следующие шаги:

  1. Создадим два маршрута.
  2. Перейдем ко второму маршруту, используя Navigator.push().
  3. Вернемся к первому маршруту, используя Navigator.pop().

1. Создаем два маршрута

Сначала создайте два маршрута для работы. Поскольку это базовый пример, каждый маршрут содержит только одну кнопку. Нажатие кнопки на первом маршруте позволяет перейти ко второму маршруту. Нажатие кнопки на втором маршруте возвращает к первому маршруту.

Сначала настройте визуальную структуру:

class FirstRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('First Route'),
      ),
      body: Center(
        child: RaisedButton(
          child: Text('Open route'),
          onPressed: () {
            // Navigate to second route when tapped.
          },
        ),
      ),
    );
  }
}

class SecondRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Second Route"),
      ),
      body: Center(
        child: RaisedButton(
          onPressed: () {
            // Navigate back to first route when tapped.
          },
          child: Text('Go back!'),
        ),
      ),
    );
  }
}

2. Перейдем ко второму маршруту, используя Navigator.push().

Чтобы переключиться на новый маршрут, используйте метод Navigator.push(). Метод push() добавляет маршрут Route в стек маршрутов, управляемых навигатором. Откуда берется маршрут Route? Вы можете создать свой собственный или использовать MaterialPageRoute, который полезен, потому что он переходит на новый маршрут, используя анимацию для конкретной платформы.

В методе build() виджета FirstRoute обновите обратный вызов onPressed():

// Within the `FirstRoute` widget
onPressed: () {
  Navigator.push(
    context,
    MaterialPageRoute(builder: (context) => SecondRoute()),
  );
}

3. Возврат к первому маршруту, используя Navigator.pop ().

Как закрыть второй маршрут и вернуться на первый? Используя метод Navigator.pop(). Метод pop() удаляет текущий маршрут Route из стека маршрутов, управляемых навигатором.

Чтобы реализовать возврат к исходному маршруту, обновите обратный вызов onPressed() в виджете SecondRoute:

// Within the SecondRoute widget
onPressed: () {
  Navigator.pop(context);
}

Полный пример

import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(
    title: 'Navigation Basics',
    home: FirstRoute(),
  ));
}

class FirstRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('First Route'),
      ),
      body: Center(
        child: RaisedButton(
          child: Text('Open route'),
          onPressed: () {
            Navigator.push(
              context,
              MaterialPageRoute(builder: (context) => SecondRoute()),
            );
          },
        ),
      ),
    );
  }
}

class SecondRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Second Route"),
      ),
      body: Center(
        child: RaisedButton(
          onPressed: () {
            Navigator.pop(context);
          },
          child: Text('Go back!'),
        ),
      ),
    );
  }
}
Визуальный результат тестирования

Данная статься является переводом официальной статьи по ссылке Navigate to a new screen and back