🚀 Fullstack Инженерия: Flutter & FastAPI

Полный курс от нуля до продакшена. 77 секций, 13 модулей, практический проект.

⚙️Установка
🧩Виджеты
🧭Навигация
Состояние
🎯Проект
🔬Dart
🏗️Engine
🎨UI
🌐Сеть
🖥️FastAPI
🐳DevOps
🤖ML

Установка Flutter SDK

Flutter — кроссплатформенный фреймворк от Google. Один код — Android, iOS, Web, Desktop. Прежде чем писать код, нужно установить инструменты.

Шаг 1: Скачать Flutter

# Linux (рекомендуется через snap)
sudo snap install flutter --classic

# Или вручную:
cd ~/development
git clone https://github.com/flutter/flutter.git -b stable
export PATH="$PATH:`pwd`/flutter/bin"

# macOS (через brew)
brew install flutter

# Windows — скачать установщик с flutter.dev

Шаг 2: Настроить PATH

Чтобы команда flutter была доступна отовсюду:

# Добавить в ~/.bashrc или ~/.zshrc:
export PATH="$PATH:$HOME/flutter/bin"

# Применить:
source ~/.bashrc

Шаг 3: Проверить установку

flutter doctor

Команда покажет, что установлено, а что нет:

Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.24.0)
[✓] Android toolchain - develop for Android devices
[✓] Chrome - develop for the web
[✓] Android Studio
[✓] VS Code
[✓] Connected device (1 available)

• No issues found!
🛠 Android Studio

Для мобильной разработки нужна Android Studio (для эмулятора и SDK). Скачать: developer.android.com/studio. После установки: Tools → SDK Manager → установить Android SDK 34+.

💡 VS Code (рекомендуется)

Установите расширение Flutter в VS Code — оно автоматически ставит Dart. Плагины: Flutter, Dart, Flutter Widget Snippets.

Первый проект

Создание проекта

# Создать новый проект
flutter create hello_world

# Перейти в папку
cd hello_world

# Запустить (нужен запущенный эмулятор или подключённый телефон)
flutter run

Что создалось

hello_world/
├── android/          ← нативный код Android
├── ios/              ← нативный код iOS
├── lib/              ← ⭐ ВАШ КОД ЗДЕСЬ
│   └── main.dart     ← точка входа приложения
├── test/             ← тесты
├── web/              ← веб-версия
├── pubspec.yaml      ← зависимости и метаданные
├── pubspec.lock      ← зафиксированные версии (gitignore)
├── .gitignore
└── README.md

Запуск на эмуляторе

# Показать доступные устройства
flutter devices

# Запустить на конкретном устройстве
flutter run -d emulator-5554

# Запустить в Chrome
flutter run -d chrome

# Горячие клавиши при запуске:
# r  — Hot Reload (быстрое обновление)
# R  — Hot Restart (полный перезапуск)
# q  — Выход
🛠 Нет эмулятора?

Android Studio → Tools → Device Manager → Create Device → выберите Pixel 7 → скачайте системный образ (API 34) → Start. Или подключите реальный телефон по USB (включите «Разработка → Отладка по USB»).

Hot Reload и Hot Restart

Главная «магия» Flutter — мгновенное обновление приложения без перекомпиляции.

Hot Reload (горячая перезагрузка)

  • Нажмите r в терминале или сохраните файл в VS Code
  • Состояние приложения сохраняется
  • Обновляется только UI (~1 секунда)
  • Идеально для работы над интерфейсом

Hot Restart (горячий перезапуск)

  • Нажмите Shift+r в терминале
  • Состояние приложения сбрасывается
  • Полный перезапуск Dart VM (~3-5 секунд)
  • Нужно когда изменили initState, конструкторы, или глобальное состояние
💡 Когда что использовать

Hot Reload — изменили виджет, текст, цвет,布局 → нажали r → видите результат.
Hot Restart — изменили initState(), добавили новый Provider, поменяли конструктор → Shift+r.
flutter run (полный запуск) — изменили нативный код, добавили пакет, изменили pubspec.yaml → нужно перезапустить.

Структура Flutter-проекта

Понимание структуры проекта — ключ к продуктивной работе.

Ключевые файлы и папки

ПутьНазначение
lib/main.dartТочка входа. Здесь начинается ваше приложение
lib/Весь Dart-код приложения. Можно создавать подпапки
pubspec.yamlМанифест: название, версия, зависимости, assets
android/Нативный Android-код (обычно не трогаем)
ios/Нативный iOS-код (обычно не трогаем)
test/Unit и widget тесты
assets/Картинки, шрифты, JSON-файлы

Рекомендуемая структура lib/

lib/
├── main.dart                 ← точка входа
├── app.dart                  ← MaterialApp, роутинг
├── screens/                  ← экраны (по одному на файл)
│   ├── home_screen.dart
│   ├── login_screen.dart
│   └── settings_screen.dart
├── widgets/                  ← переиспользуемые виджеты
│   ├── custom_button.dart
│   └── user_avatar.dart
├── models/                   ← модели данных
│   └── user.dart
├── services/                 ← бизнес-логика, API, хранилище
│   └── api_service.dart
└── utils/                    ← утилиты, константы, темы
    └── constants.dart
🛠 pubspec.yaml — манифест проекта

После изменения pubspec.yaml всегда запускайте flutter pub get. Добавить пакет: flutter pub add dio (автоматически обновит pubspec и скачает).

Минимальный каркас приложения

Каждое Flutter-приложение следует паттерну: main()runApp()MaterialAppScaffold.

Hello World

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Моё первое приложение',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Привет, Flutter!'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: const Center(
        child: Text(
          'Hello World!',
          style: TextStyle(fontSize: 24),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          print('Нажали!');
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}

Что здесь происходит

  • main() — точка входа Dart. Вызывает runApp()
  • runApp() — «приклеивает» корневой виджет к экрану
  • MaterialApp — оболочка: тема, навигация, локализация
  • Scaffold — каркас экрана: шапка, тело, кнопка
  • AppBar — верхняя панель с заголовком
  • Center + Text — текст по центру
  • FloatingActionButton — плавающая кнопка
💡 Сравнение с C++/Python

main()int main() в C++ или if __name__ == '__main__': в Python.
MaterialApp ≈ корневой виджет в Qt или Tk() в tkinter.
Scaffold ≈ QMainWindow с toolbar и statusbar в Qt.

⚠️ Попробуйте сами

1. Создайте проект: flutter create my_app
2. Замените содержимое lib/main.dart на код выше
3. Запустите: flutter run
4. Измените текст «Hello World!» на что-то своё, сохраните (Hot Reload) → увидите результат

Scaffold и AppBar

Scaffold — это каркас экрана. Практически каждый экран в Flutter начинается с Scaffold. Он предоставляет: шапку (AppBar), тело (body), плавающую кнопку (FAB), боковое меню (drawer), нижнюю навигацию.

Полный Scaffold

Scaffold(
  appBar: AppBar(
    title: const Text('Мой экран'),
    actions: [
      IconButton(onPressed: () {}, icon: const Icon(Icons.search)),
      IconButton(onPressed: () {}, icon: const Icon(Icons.more_vert)),
    ],
  ),
  body: const Center(child: Text('Контент')),
  floatingActionButton: FloatingActionButton(
    onPressed: () {},
    child: const Icon(Icons.add),
  ),
  drawer: const Drawer(child: Text('Меню')),
  bottomNavigationBar: BottomNavigationBar(
    items: const [
      BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Главная'),
      BottomNavigationBarItem(icon: Icon(Icons.settings), label: 'Настройки'),
    ],
  ),
)

AppBar — подробнее

AppBar(
  title: const Text('Заголовок'),
  centerTitle: true,                    // центрировать заголовок
  backgroundColor: Colors.blue,         // цвет фона
  foregroundColor: Colors.white,        // цвет текста и иконок
  elevation: 4,                         // тень (0 = плоский)
  leading: IconButton(                  // кнопка слева (обычно ← или ☰)
    onPressed: () => Navigator.pop(context),
    icon: const Icon(Icons.arrow_back),
  ),
  actions: [                            // кнопки справа
    IconButton(onPressed: () {}, icon: const Icon(Icons.search)),
    IconButton(onPressed: () {}, icon: const Icon(Icons.share)),
  ],
  bottom: PreferredSize(                // вкладки под заголовком
    preferredSize: const Size.fromHeight(48),
    child: TabBar(tabs: [Tab(text: 'Все'), Tab(text: 'Избранные')]),
  ),
)
💡 Сравнение с Qt/Python

ScaffoldQMainWindow в Qt.
AppBarQToolBar + QMenuBar.
body ≈ центральный виджет (setCentralWidget).

Text и TextStyle

Базовый текст

Text('Привет, мир!')

// С стилем
Text(
  'Заголовок',
  style: TextStyle(
    fontSize: 24,
    fontWeight: FontWeight.bold,
    color: Colors.blue,
  ),
)

// С выравниванием и переносом
Text(
  'Длинный текст который не влезает в одну строку и будет перенесён',
  textAlign: TextAlign.center,
  maxLines: 2,
  overflow: TextOverflow.ellipsis,  // обрезать с "..."
  style: const TextStyle(fontSize: 16),
)

RichText — разные стили в одном абзаце

RichText(
  text: TextSpan(
    text: 'У вас ',
    style: const TextStyle(color: Colors.black, fontSize: 16),
    children: [
      TextSpan(
        text: '3 новых',
        style: const TextStyle(
          fontWeight: FontWeight.bold,
          color: Colors.red,
        ),
      ),
      const TextSpan(text: ' сообщения'),
    ],
  ),
)

Google Fonts (кастомные шрифты)

flutter pub add google_fonts
import 'package:google_fonts/google_fonts.dart';

Text(
  'Красивый шрифт',
  style: GoogleFonts.roboto(fontSize: 20, fontWeight: FontWeight.w300),
)
🛠 TextStyle —常用属性
СвойствоТипОписание
fontSizedoubleРазмер шрифта
fontWeightFontWeightТолщина: w100..w900, bold
colorColorЦвет текста
fontStyleFontStyleitalic / normal
letterSpacingdoubleМежбуквенное расстояние
heightdoubleВысота строки (множитель)
decorationTextDecorationunderline, lineThrough, overline

Container и BoxDecoration

Container — самый универсальный виджет-обёртка. Задаёт размер, отступы, фон, скругление, тень.

Простой Container

Container(
  width: 200,
  height: 100,
  color: Colors.blue,
  child: const Center(child: Text('Привет')),
)

С декорацией (скругление, тень, градиент)

Container(
  width: 300,
  padding: const EdgeInsets.all(16),        // внутренние отступы
  margin: const EdgeInsets.all(8),           // внешние отступы
  decoration: BoxDecoration(
    color: Colors.white,
    borderRadius: BorderRadius.circular(12), // скругление углов
    boxShadow: [                             // тень
      BoxShadow(
        color: Colors.black.withOpacity(0.1),
        blurRadius: 10,
        offset: const Offset(0, 4),
      ),
    ],
    border: Border.all(color: Colors.grey.shade300), // рамка
  ),
  child: const Text('Карточка с тенью'),
)

Градиент

Container(
  width: double.infinity,
  height: 200,
  decoration: const BoxDecoration(
    gradient: LinearGradient(
      begin: Alignment.topLeft,
      end: Alignment.bottomRight,
      colors: [Colors.blue, Colors.purple],
    ),
  ),
  child: const Center(
    child: Text('Градиент', style: TextStyle(color: Colors.white, fontSize: 24)),
  ),
)
⚠️ Важно

Нельзя одновременно задавать color в Container и color в BoxDecoration — будет ошибка. Используйте цвет только в BoxDecoration.

Padding, SizedBox и Spacer

Padding — отступы вокруг виджета

Padding(
  padding: const EdgeInsets.all(16),           // со всех сторон
  child: Text('Со всех сторон 16px'),
)

Padding(
  padding: const EdgeInsets.symmetric(
    horizontal: 24,  // слева и справа
    vertical: 12,    // сверху и снизу
  ),
  child: Text('Симметричные отступы'),
)

Padding(
  padding: const EdgeInsets.only(left: 16, top: 8), // только слева и сверху
  child: Text('Только с определённых сторон'),
)

SizedBox — фиксированный промежуток

// Как промежуток между элементами в Column
Column(
  children: [
    Text('Первый'),
    SizedBox(height: 16),  // ← промежуток 16px
    Text('Второй'),
    SizedBox(height: 16),
    Text('Третий'),
  ],
)

// Как ограничитель размера
SizedBox(
  width: 200,
  height: 50,
  child: ElevatedButton(onPressed: () {}, child: Text('Кнопка')),
)

Spacer — гибкий промежуток

Row(
  children: [
    Text('Лево'),
    Spacer(),              // ← забирает всё свободное пространство
    Text('Право'),
  ],
)
// Результат: "Лево" прижато влево, "Право" — вправо
💡 Когда что использовать

Padding — когда нужен отступ вокруг виджета.
SizedBox(height: N) — когда нужен точный промежуток N пикселей.
Spacer() — когда нужно «раздвинуть» элементы на всё доступное пространство.
Container(margin: ...) — тоже отступ, но можно комбинировать с декорацией.

Row и Column

Два главных layout-виджета. Row — горизонтально, Column — вертикально.

Column (вертикальная компоновка)

Column(
  mainAxisAlignment: MainAxisAlignment.center,    // по вертикали
  crossAxisAlignment: CrossAxisAlignment.start,   // по горизонтали
  children: [
    Text('Первый'),
    Text('Второй'),
    Text('Третий'),
  ],
)
📐 Row и Column — оси компоновки
Row (→ главная ось) mainAxisAlignment → cross↓ A B C Column (↓ главная ось) main↓ crossAxisAlignment → A B C

Row (горизонтальная компоновка)

Row(
  mainAxisAlignment: MainAxisAlignment.spaceEvenly, // равномерно
  children: [
    Icon(Icons.star, color: Colors.red),
    Icon(Icons.star, color: Colors.red),
    Icon(Icons.star, color: Colors.grey),
  ],
)

MainAxisAlignment — распределение по главной оси

MainAxisAlignment.start        ← [A][B][C]..........
MainAxisAlignment.center       .....[A][B][C].......
MainAxisAlignment.end          ..........[A][B][C]
MainAxisAlignment.spaceBetween .[A].....[B].....[C].
MainAxisAlignment.spaceAround  ..[A]...[B]...[C]...
MainAxisAlignment.spaceEvenly  ...[A]..[B]..[C]...

CrossAxisAlignment — выравнивание по поперечной оси

CrossAxisAlignment.start       |A|      |B|      |C|
                               |  longer  |
CrossAxisAlignment.center      |  longer  |
                               |A|      |B|      |C|
CrossAxisAlignment.end         |  longer  |
                               |A|      |B|      |C|
CrossAxisAlignment.stretch     |  longer  |
                               |AAAAA||BBBBB||CCCCC|
🛠 Главная vs Поперечная ось

Row: главная ось = горизонталь (→), поперечная = вертикаль (↓).
Column: главная ось = вертикаль (↓), поперечная = горизонталь (→).
mainAxisAlignment распределяет детей вдоль главной оси.
crossAxisAlignment выравнивает детей по поперечной оси.

Expanded и Flexible

Когда детей в Row/Column не хватает места, нужно явно сказать, кто как делит пространство. Expanded и Flexible — инструменты для этого.

📊 Expanded — распределение пространства (flex)
flex: 1 25% flex: 2 50% flex: 1 25% Expanded(flex:1) + Expanded(flex:2) + Expanded(flex:1) = 4 части Каждая 1/4 = 25%, 2/4 = 50%

Expanded — занимает всё доступное пространство

Row(
  children: [
    Expanded(
      flex: 1,  // 1 часть пространства
      child: Container(color: Colors.red, height: 50),
    ),
    Expanded(
      flex: 2,  // 2 части пространства (в 2 раза шире)
      child: Container(color: Colors.blue, height: 50),
    ),
    Expanded(
      flex: 1,  // 1 часть
      child: Container(color: Colors.green, height: 50),
    ),
  ],
)
// Итого: красная = 25%, синяя = 50%, зелёная = 25%

Flexible — может быть меньше

// Flexible НЕ принуждает занять всё пространство
Row(
  children: [
    Flexible(
      child: Text('Короткий'),  // займёт только столько, сколько нужно
    ),
    Flexible(
      child: Text('Тоже короткий'),
    ),
  ],
)

// Expanded ПРИНУЖДАЕТ занять всё
Row(
  children: [
    Expanded(child: Text('Растянется')),
    Expanded(child: Text('Тоже растянется')),
  ],
)
💡 Правило

Expanded = «забери всё свободное пространство» (flex-дитя).
Flexible = «можешь взять, но не обязан».
Если внутри Row/Column текст обрезается с ошибкой overflow — оберните в Expanded.

Stack и Positioned

Stack — наложение виджетов друг на друга (как z-index в CSS). Positioned — точное позиционирование внутри Stack.

📚 Stack — слои (z-index)
Container 1 задний план Container 2 поверх Positioned z-index ↑

Базовый Stack

Stack(
  children: [
    Container(width: 200, height: 200, color: Colors.blue),   // задний план
    Container(width: 150, height: 150, color: Colors.red),    // поверх
    const Positioned(                                          // точное место
      bottom: 10,
      right: 10,
      child: Icon(Icons.star, color: Colors.yellow, size: 30),
    ),
  ],
)

Практический пример: бейдж на аватаре

Stack(
  children: [
    const CircleAvatar(
      radius: 30,
      backgroundImage: NetworkImage('https://example.com/avatar.jpg'),
    ),
    Positioned(
      right: 0,
      bottom: 0,
      child: Container(
        padding: const EdgeInsets.all(2),
        decoration: const BoxDecoration(
          color: Colors.red,
          shape: BoxShape.circle,
        ),
        child: const Text('3', style: TextStyle(color: Colors.white, fontSize: 12)),
      ),
    ),
  ],
)
🛠 Positioned — позиционирование

Positioned(top: 10, left: 10) — 10px сверху и слева.
Positioned.fill() — растянуть на весь Stack.
Можно комбинировать: top + left, bottom + right, и т.д.

Card и ListTile

Card — карточка Material Design

Card(
  elevation: 4,                              // тень
  shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.circular(12), // скругление
  ),
  child: const Padding(
    padding: EdgeInsets.all(16),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text('Заголовок', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
        SizedBox(height: 8),
        Text('Описание карточки с каким-то текстом'),
      ],
    ),
  ),
)

ListTile — строка списка

Card(
  child: ListTile(
    leading: const CircleAvatar(child: Icon(Icons.person)),  // слева
    title: const Text('Иван Иванов'),                       // заголовок
    subtitle: const Text('ivan@example.com'),                // подзаголовок
    trailing: const Icon(Icons.arrow_forward_ios),           // справа
    onTap: () {
      print('Нажали на элемент');
    },
  ),
)

Популярные паттерны с ListTile

// Переключатель
ListTile(
  leading: const Icon(Icons.notifications),
  title: const Text('Уведомления'),
  trailing: Switch(value: true, onChanged: (v) {}),
)

// Выбор из списка
ListTile(
  leading: const Icon(Icons.language),
  title: const Text('Язык'),
  subtitle: const Text('Русский'),
  trailing: const Icon(Icons.chevron_right),
  onTap: () {},
)

// Чекбокс
ListTile(
  leading: const Checkbox(value: false, onChanged: null),
  title: const Text('Принять условия'),
)

ListView и ListView.builder

ListView — прокручиваемый список. Самый используемый виджет во Flutter.

Статический ListView

ListView(
  children: const [
    ListTile(title: Text('Элемент 1')),
    ListTile(title: Text('Элемент 2')),
    ListTile(title: Text('Элемент 3')),
  ],
)

ListView.builder — ленивая загрузка (⭐ основной)

ListView.builder(
  itemCount: 1000,  // количество элементов
  itemBuilder: (context, index) {
    return ListTile(
      leading: CircleAvatar(child: Text('$index')),
      title: Text('Элемент #$index'),
      subtitle: Text('Описание элемента'),
    );
  },
)

ListView.separated — с разделителями

ListView.separated(
  itemCount: 50,
  itemBuilder: (context, index) {
    return ListTile(title: Text('Контакт #$index'));
  },
  separatorBuilder: (context, index) {
    return const Divider(height: 1);
  },
)
⚠️ ListView внутри Column

Нельзя просто так вложить ListView в Column — будет ошибка «unbounded height». Оберните ListView в Expanded:
Column(children: [Text('Шапка'), Expanded(child: ListView(...))])

GridView

GridView.count — фиксированное количество колонок

GridView.count(
  crossAxisCount: 2,       // 2 колонки
  mainAxisSpacing: 8,      // отступ по вертикали
  crossAxisSpacing: 8,     // отступ по горизонтали
  padding: const EdgeInsets.all(8),
  children: [
    Container(color: Colors.red, child: const Center(child: Text('1'))),
    Container(color: Colors.blue, child: const Center(child: Text('2'))),
    Container(color: Colors.green, child: const Center(child: Text('3'))),
    Container(color: Colors.orange, child: const Center(child: Text('4'))),
  ],
)

GridView.builder — ленивая загрузка

GridView.builder(
  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 2,
    mainAxisSpacing: 8,
    crossAxisSpacing: 8,
    childAspectRatio: 0.75,  // соотношение сторон
  ),
  itemCount: 100,
  itemBuilder: (context, index) {
    return Card(
      child: Center(child: Text('Фото #$index')),
    );
  },
)
💡 Когда GridView vs ListView

ListView — одноколоночный список (контакты, сообщения, статьи).
GridView — сетка (галерея фото, плитка товаров, иконки приложений).

Image

Изображения из assets

# pubspec.yaml
flutter:
  assets:
    - assets/images/
Image.asset(
  'assets/images/logo.png',
  width: 200,
  height: 100,
  fit: BoxFit.cover,  // как вписывать: cover, contain, fill, fitWidth, fitHeight
)

Изображения из интернета

Image.network(
  'https://picsum.photos/400/300',
  width: 400,
  height: 300,
  fit: BoxFit.cover,
  loadingBuilder: (context, child, loadingProgress) {
    if (loadingProgress == null) return child;
    return const Center(child: CircularProgressIndicator());
  },
  errorBuilder: (context, error, stackTrace) {
    return const Icon(Icons.error, size: 50);
  },
)

BoxFit — варианты вписывания

BoxFit.cover    — заполнить контейнер, обрезать лишнее (⭐ самый частый)
BoxFit.contain  — вписать целиком, могут быть поля
BoxFit.fill     — растянуть (может исказить пропорции)
BoxFit.fitWidth — вписать по ширине
BoxFit.fitHeight— вписать по высоте
BoxFit.none     — оригинальный размер, без масштабирования
🛠 CircleAvatar — круглое изображение
CircleAvatar(
  radius: 40,
  backgroundImage: NetworkImage('https://example.com/avatar.jpg'),
  child: Text('А'),  // fallback если нет картинки
)

Icon и IconButton

Встроенные иконки Material

Icon(Icons.home, size: 30, color: Colors.blue)
Icon(Icons.favorite, color: Colors.red)
Icon(Icons.search)
Icon(Icons.settings)
Icon(Icons.person)
Icon(Icons.add)
Icon(Icons.delete)
Icon(Icons.edit)
Icon(Icons.share)
Icon(Icons.star)

IconButton — иконка-кнопка

IconButton(
  icon: const Icon(Icons.favorite_border),
  iconSize: 30,
  color: Colors.red,
  onPressed: () {
    print('Нажали на сердечко');
  },
)

Полный список: flutter.dev/resources/cheatsheet

В Flutter 1400+ иконок Material. Поиск: начните писать Icons. в IDE — автодополнение покажет все варианты.

Кнопки

Типы кнопок Material 3

// ElevatedButton — залитая кнопка (основное действие)
ElevatedButton(
  onPressed: () {},
  child: const Text('Отправить'),
)

// TextButton — текстовая кнопка (вторичное действие)
TextButton(
  onPressed: () {},
  child: const Text('Отмена'),
)

// OutlinedButton — кнопка с рамкой
OutlinedButton(
  onPressed: () {},
  child: const Text('Подробнее'),
)

// IconButton — кнопка-иконка (см. выше)

// FloatingActionButton — плавающая кнопка
FloatingActionButton(
  onPressed: () {},
  child: const Icon(Icons.add),
)

Стилизация кнопки

ElevatedButton(
  onPressed: () {},
  style: ElevatedButton.styleFrom(
    backgroundColor: Colors.green,        // цвет фона
    foregroundColor: Colors.white,        // цвет текста
    padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(8),
    ),
    elevation: 4,
  ),
  child: const Text('Заказать', style: TextStyle(fontSize: 16)),
)

Кнопка с иконкой

ElevatedButton.icon(
  onPressed: () {},
  icon: const Icon(Icons.download),
  label: const Text('Скачать'),
)

TextButton.icon(
  onPressed: () {},
  icon: const Icon(Icons.share),
  label: const Text('Поделиться'),
)
💡 Когда какую кнопку

ElevatedButton — главная кнопка на экране («Отправить», «Купить»).
TextButton — вторичное действие («Отмена», «Позже»).
OutlinedButton — альтернативное действие («Подробнее»).
IconButton — в AppBar, тулбарах, компактных местах.

TextField и TextFormField

Простой TextField

TextField(
  decoration: InputDecoration(
    labelText: 'Имя',
    hintText: 'Введите ваше имя',
    border: OutlineInputBorder(
      borderRadius: BorderRadius.circular(8),
    ),
    prefixIcon: const Icon(Icons.person),
  ),
)

С контроллером (чтобы получить текст)

class _MyFormState extends State<MyForm> {
  final TextEditingController _emailController = TextEditingController();
  final TextEditingController _passwordController = TextEditingController();

  @override
  void dispose() {
    _emailController.dispose();  // ← обязательно освободить!
    _passwordController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextField(
          controller: _emailController,
          keyboardType: TextInputType.emailAddress,
          decoration: const InputDecoration(
            labelText: 'Email',
            prefixIcon: Icon(Icons.email),
            border: OutlineInputBorder(),
          ),
        ),
        const SizedBox(height: 16),
        TextField(
          controller: _passwordController,
          obscureText: true,  // скрыть текст (пароль)
          decoration: const InputDecoration(
            labelText: 'Пароль',
            prefixIcon: Icon(Icons.lock),
            border: OutlineInputBorder(),
          ),
        ),
        const SizedBox(height: 16),
        ElevatedButton(
          onPressed: () {
            String email = _emailController.text;
            String password = _passwordController.text;
            print('Email: $email, Password: $password');
          },
          child: const Text('Войти'),
        ),
      ],
    );
  }
}
🛠 InputDecoration —常用属性
СвойствоОписание
labelTextМетка (поднимается при фокусе)
hintTextПодсказка (исчезает при вводе)
prefixIconИконка слева
suffixIconИконка справа
borderСтиль рамки (OutlineInputBorder и т.д.)
errorTextТекст ошибки
filledЗаливка фона

GestureDetector и InkWell

GestureDetector — обработка жестов

GestureDetector(
  onTap: () => print('Короткое нажатие'),
  onDoubleTap: () => print('Двойной тап'),
  onLongPress: () => print('Долгое нажатие'),
  onPanUpdate: (details) => print('Свайп: ${details.delta}'),
  child: Container(
    width: 200,
    height: 200,
    color: Colors.blue,
    child: const Center(child: Text('Нажми меня')),
  ),
)

InkWell — Material-ripple эффект

InkWell(
  onTap: () => print('Нажали'),
  onLongPress: () => print('Долгое нажатие'),
  borderRadius: BorderRadius.circular(12),
  child: Container(
    padding: const EdgeInsets.all(16),
    child: const Text('Нажми — увидишь ripple'),
  ),
)

Когда что использовать

  • GestureDetector — когда нужна невидимая обработка жестов (свайп, перетаскивание, pinch)
  • InkWell — когда нужен визуальный отклик (ripple-эффект, Material Design)
  • ElevatedButton — когда это явная кнопка с текстом
💡 Сравнение с вебом

GestureDetectoraddEventListener в JavaScript.
InkWell<button> с CSS-эффектом :active.
onTapclick, onLongPressmousedown + setTimeout.

Именованные маршруты

Определение маршрутов в MaterialApp

MaterialApp(
  initialRoute: '/',
  routes: {
    '/': (context) => const HomeScreen(),
    '/settings': (context) => const SettingsScreen(),
    '/profile': (context) => const ProfileScreen(),
  },
)

Навигация по имени

// Открыть по имени
Navigator.pushNamed(context, '/settings');

// Открыть с аргументами
Navigator.pushNamed(
  context,
  '/profile',
  arguments: {'userId': 42, 'name': 'Иван'},
);

// Получить аргументы на принимающем экране
final args = ModalRoute.of(context)!.settings.arguments as Map;
🛠 Когда именованные vs push()

Именованные — удобно для глобальных экранов (home, settings, profile).
push() — удобно для локальных переходов с передачей сложных объектов.
В реальных проектах часто используют go_router — более мощную альтернативу.

Drawer — боковое меню

Scaffold(
  appBar: AppBar(title: const Text('Моё приложение')),
  drawer: Drawer(
    child: ListView(
      padding: EdgeInsets.zero,
      children: [
        const DrawerHeader(
          decoration: BoxDecoration(color: Colors.blue),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              CircleAvatar(radius: 30, child: Icon(Icons.person, size: 30)),
              SizedBox(height: 10),
              Text('Иван Иванов', style: TextStyle(color: Colors.white, fontSize: 18)),
              Text('ivan@example.com', style: TextStyle(color: Colors.white70)),
            ],
          ),
        ),
        ListTile(
          leading: const Icon(Icons.home),
          title: const Text('Главная'),
          onTap: () {
            Navigator.pop(context);  // закрыть drawer
            // переход на главную
          },
        ),
        ListTile(
          leading: const Icon(Icons.settings),
          title: const Text('Настройки'),
          onTap: () {
            Navigator.pop(context);
            Navigator.pushNamed(context, '/settings');
          },
        ),
        const Divider(),
        ListTile(
          leading: const Icon(Icons.exit_to_app),
          title: const Text('Выход'),
          onTap: () {},
        ),
      ],
    ),
  ),
  body: const Center(child: Text('Основной контент')),
)

BottomNavigationBar

Нижняя навигация — стандарт Material для 3-5 основных разделов.

class MainScreen extends StatefulWidget {
  const MainScreen({super.key});

  @override
  State<MainScreen> createState() => _MainScreenState();
}

class _MainScreenState extends State<MainScreen> {
  int _currentIndex = 0;

  final List<Widget> _screens = [
    const HomeScreen(),
    const SearchScreen(),
    const ProfileScreen(),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: _screens[_currentIndex],
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        onTap: (index) => setState(() => _currentIndex = index),
        type: BottomNavigationBarType.fixed,  // если больше 3 элементов
        items: const [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Главная'),
          BottomNavigationBarItem(icon: Icon(Icons.search), label: 'Поиск'),
          BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Профиль'),
        ],
      ),
    );
  }
}

TabBar и TabBarView

DefaultTabController(
  length: 3,  // количество вкладок
  child: Scaffold(
    appBar: AppBar(
      title: const Text('Вкладки'),
      bottom: const TabBar(
        tabs: [
          Tab(icon: Icon(Icons.directions_car), text: 'Авто'),
          Tab(icon: Icon(Icons.directions_transit), text: 'Транзит'),
          Tab(icon: Icon(Icons.directions_bike), text: 'Вело'),
        ],
      ),
    ),
    body: const TabBarView(
      children: [
        Center(child: Text('Контент: Авто')),
        Center(child: Text('Контент: Транзит')),
        Center(child: Text('Контент: Вело')),
      ],
    ),
  ),
)

AlertDialog и BottomSheet

AlertDialog

showDialog(
  context: context,
  builder: (context) => AlertDialog(
    title: const Text('Удалить?'),
    content: const Text('Вы уверены, что хотите удалить этот элемент?'),
    actions: [
      TextButton(
        onPressed: () => Navigator.pop(context),
        child: const Text('Отмена'),
      ),
      TextButton(
        onPressed: () {
          // удалить
          Navigator.pop(context);
        },
        child: const Text('Удалить', style: TextStyle(color: Colors.red)),
      ),
    ],
  ),
);

SimpleDialog — выбор из вариантов

showDialog(
  context: context,
  builder: (context) => SimpleDialog(
    title: const Text('Выберите действие'),
    children: [
      SimpleDialogOption(
        onPressed: () => Navigator.pop(context, 'camera'),
        child: const ListTile(leading: Icon(Icons.camera), title: Text('Камера')),
      ),
      SimpleDialogOption(
        onPressed: () => Navigator.pop(context, 'gallery'),
        child: const ListTile(leading: Icon(Icons.photo), title: Text('Галерея')),
      ),
    ],
  ),
);

showModalBottomSheet — панель снизу

showModalBottomSheet(
  context: context,
  shape: const RoundedRectangleBorder(
    borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
  ),
  builder: (context) => Container(
    padding: const EdgeInsets.all(16),
    child: Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        const Text('Выберите', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
        const SizedBox(height: 16),
        ListTile(leading: const Icon(Icons.copy), title: const Text('Копировать'), onTap: () => Navigator.pop(context)),
        ListTile(leading: const Icon(Icons.share), title: const Text('Поделиться'), onTap: () => Navigator.pop(context)),
        ListTile(leading: const Icon(Icons.delete), title: const Text('Удалить'), onTap: () => Navigator.pop(context)),
      ],
    ),
  ),
);

SnackBar

// Простой SnackBar
ScaffoldMessenger.of(context).showSnackBar(
  const SnackBar(content: Text('Сохранено!')),
);

// С действием
ScaffoldMessenger.of(context).showSnackBar(
  SnackBar(
    content: const Text('Элемент удалён'),
    action: SnackBarAction(
      label: 'ОТМЕНИТЬ',
      onPressed: () {
        // восстановить элемент
      },
    ),
    duration: const Duration(seconds: 3),
    behavior: SnackBarBehavior.floating,  // плавающий (не прижат к низу)
    shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
  ),
);
💡 Сравнение с Android/Python

SnackBarToast в Android.
ScaffoldMessenger.of(context) — аналог получения контекста для показа уведомлений.

setState — когда и как

setState() — самый простой способ управления состоянием. Он говорит Flutter: «данные изменились, перерисуй UI».

Правильное использование

class CounterPage extends StatefulWidget {
  const CounterPage({super.key});

  @override
  State<CounterPage> createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  int _count = 0;

  void _increment() {
    setState(() {
      _count++;  // ← изменяем данные ВНУТРИ setState
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Text('$_count', style: const TextStyle(fontSize: 48)),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _increment,
        child: const Icon(Icons.add),
      ),
    );
  }
}

Антипаттерны

// ❌ ПЛОХО: setState с тяжёлой логикой
setState(() {
  _data = await fetchData();  // ❌ async внутри setState!
  _filtered = _data.where(...).toList();
  _count = _filtered.length;
});

// ✅ ХОРОШО: вычисления ДО setState
final data = await fetchData();
final filtered = data.where(...).toList();
setState(() {
  _data = data;
  _filtered = filtered;
  _count = filtered.length;
});

Подъём состояния (Lifting State Up)

// Родитель хранит общее состояние
class ParentWidget extends StatefulWidget {
  @override
  State<ParentWidget> createState() => _ParentWidgetState();
}

class _ParentWidgetState extends State<ParentWidget> {
  bool _isFavorite = false;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // Передаём данные и callback вниз
        FavoriteButton(
          isFavorite: _isFavorite,
          onToggle: () => setState(() => _isFavorite = !_isFavorite),
        ),
        Text(_isFavorite ? '❤️ В избранном' : '🤍 Не в избранном'),
      ],
    );
  }
}

// Дитя принимает данные и вызывает callback
class FavoriteButton extends StatelessWidget {
  final bool isFavorite;
  final VoidCallback onToggle;

  const FavoriteButton({required this.isFavorite, required this.onToggle});

  @override
  Widget build(BuildContext context) {
    return IconButton(
      icon: Icon(isFavorite ? Icons.favorite : Icons.favorite_border),
      onPressed: onToggle,
    );
  }
}
🛠 Когда setState достаточно

setState отлично работает для локального состояния одного экрана: счётчики, тогглы, формы, индикаторы загрузки. Для глобального состояния (авторизация, корзина, тема) → используйте Provider / Riverpod / BLoC.

FutureBuilder

FutureBuilder — виджет, который строит UI на основе асинхронных данных (Future). Идеален для загрузки данных из API, чтения файлов, SharedPreferences.

FutureBuilder<String>(
  future: fetchDataFromAPI(),  // Future-функция
  builder: (context, snapshot) {
    // Состояние 1: загрузка
    if (snapshot.connectionState == ConnectionState.waiting) {
      return const Center(child: CircularProgressIndicator());
    }
    // Состояние 2: ошибка
    if (snapshot.hasError) {
      return Center(child: Text('Ошибка: ${snapshot.error}'));
    }
    // Состояние 3: данные получены
    if (snapshot.hasData) {
      return Text(snapshot.data!);
    }
    // Состояние 4: нет данных
    return const Center(child: Text('Нет данных'));
  },
)

Практический пример: загрузка профиля

class ProfilePage extends StatelessWidget {
  const ProfilePage({super.key});

  Future<Map<String, dynamic>> _loadProfile() async {
    final response = await http.get(Uri.parse('https://api.example.com/profile'));
    return jsonDecode(response.body);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Профиль')),
      body: FutureBuilder<Map<String, dynamic>>(
        future: _loadProfile(),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          }
          if (snapshot.hasError) {
            return Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  const Icon(Icons.error, size: 48, color: Colors.red),
                  Text('Не удалось загрузить: ${snapshot.error}'),
                  ElevatedButton(onPressed: () {}, child: const Text('Повторить')),
                ],
              ),
            );
          }
          final profile = snapshot.data!;
          return ListTile(
            leading: CircleAvatar(child: Text(profile['name'][0])),
            title: Text(profile['name']),
            subtitle: Text(profile['email']),
          );
        },
      ),
    );
  }
}
⚠️ Важно

Не создавайте Future внутри build() — он будет вызываться при каждой перерисовке! Создайте Future в initState() или используйте переменную.

StreamBuilder

StreamBuilder — как FutureBuilder, но для потоков данных (стримов). Обновляет UI каждый раз, когда приходят новые данные.

StreamBuilder<List<Message>>(
  stream: getMessagesStream(),  // Stream-функция
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return const CircularProgressIndicator();
    }
    if (snapshot.hasError) {
      return Text('Ошибка: ${snapshot.error}');
    }
    final messages = snapshot.data ?? [];
    return ListView.builder(
      itemCount: messages.length,
      itemBuilder: (context, index) {
        return ListTile(title: Text(messages[index].text));
      },
    );
  },
)

FutureBuilder vs StreamBuilder

FutureBuilderStreamBuilder
Один результатМного результатов (поток)
Загрузка данных один разРеалтайм обновления
HTTP GET, SharedPreferencesWebSocket, Firestore, таймеры
await futureawait for (value in stream)

SharedPreferences

SharedPreferences — простое локальное хранилище для пар «ключ-значение». Подходит для настроек, токенов, флагов «первый запуск».

flutter pub add shared_preferences
import 'package:shared_preferences/shared_preferences.dart';

// Запись
final prefs = await SharedPreferences.getInstance();
await prefs.setString('username', 'ivan');
await prefs.setInt('age', 25);
await prefs.setBool('is_logged_in', true);
await prefs.setStringList('favorites', ['id1', 'id2']);

// Чтение
final username = prefs.getString('username') ?? 'Гость';
final age = prefs.getInt('age') ?? 0;
final isLoggedIn = prefs.getBool('is_logged_in') ?? false;
final favorites = prefs.getStringList('favorites') ?? [];

// Удаление
await prefs.remove('username');
await prefs.clear();  // удалить всё

Практический пример: «первый запуск»

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FutureBuilder<bool>(
      future: _isFirstLaunch(),
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return const CircularProgressIndicator();
        }
        final isFirst = snapshot.data ?? true;
        return isFirst ? const OnboardingScreen() : const HomeScreen();
      },
    );
  }

  Future<bool> _isFirstLaunch() async {
    final prefs = await SharedPreferences.getInstance();
    final launched = prefs.getBool('launched_before') ?? false;
    if (!launched) {
      await prefs.setBool('launched_before', true);
    }
    return !launched;
  }
}
🛠 SharedPreferences vs SQLite vs Hive

SharedPreferences — простые настройки (строки, числа, булевы).
SQLite / sqflite — структурированные данные, запросы, большие объёмы.
Hive / Isar — быстрая NoSQL-БД, работает с объектами напрямую.

go_router — современная навигация

go_router — пакет от Flutter team для декларативной навигации. Поддерживает deep links, redirects, guards, вложенные навигаторы.

flutter pub add go_router
import 'package:go_router/go_router.dart';

final router = GoRouter(
  initialLocation: '/',
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const HomeScreen(),
    ),
    GoRoute(
      path: '/profile/:id',
      builder: (context, state) {
        final id = state.pathParameters['id']!;
        return ProfileScreen(userId: id);
      },
    ),
    GoRoute(
      path: '/settings',
      builder: (context, state) => const SettingsScreen(),
    ),
  ],
  redirect: (context, state) {
    final isLoggedIn = AuthService.instance.isLoggedIn;
    if (!isLoggedIn && state.matchedLocation != '/login') {
      return '/login';
    }
    return null;
  },
);

// Использование в MaterialApp
MaterialApp.router(routerConfig: router)

// Навигация
context.go('/profile/42');           // абсолютный переход
context.push('/settings');           // открыть поверх
context.pop();                       // назад
💡 go_router vs Navigator

Navigator — императивный стек (push/pop). Простой, но ограниченный.
go_router — декларативный (описываешь routes, он сам навигирует). Deep links, guards, URL-based. Для продакшена → go_router.

Проект «Заметки»: Архитектура

Соберём полноценное приложение «Заметки» — от каркаса до рабочего APK. Это закрепит всё, что вы изучили в предыдущих блоках.

Что будет в приложении

  • 📋 Список заметок (ListView.builder)
  • ➕ Создание новой заметки (Form + TextFormField)
  • ✏️ Редактирование существующей
  • 🗑️ Удаление свайпом (Dismissible)
  • 💾 Сохранение в SharedPreferences
  • 🔍 Поиск по заметкам

Структура проекта

lib/
├── main.dart              ← точка входа
├── models/
│   └── note.dart          ← модель данных Note
├── services/
│   └── note_service.dart  ← сохранение/загрузка заметок
├── screens/
│   ├── home_screen.dart   ← список заметок
│   └── edit_screen.dart   ← создание/редактирование
└── widgets/
    └── note_card.dart     ← карточка заметки

Шаг 1: Создать проект

flutter create notes_app
cd notes_app
flutter pub add shared_preferences uuid

Экран списка заметок

Модель данных

// lib/models/note.dart
class Note {
  final String id;
  String title;
  String content;
  DateTime createdAt;

  Note({
    required this.id,
    required this.title,
    required this.content,
    DateTime? createdAt,
  }) : createdAt = createdAt ?? DateTime.now();

  Map<String, dynamic> toJson() => {
    'id': id,
    'title': title,
    'content': content,
    'createdAt': createdAt.toIso8601String(),
  };

  factory Note.fromJson(Map<String, dynamic> json) => Note(
    id: json['id'],
    title: json['title'],
    content: json['content'],
    createdAt: DateTime.parse(json['createdAt']),
  );
}

Экран списка

// lib/screens/home_screen.dart
class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  List<Note> _notes = [];

  @override
  void initState() {
    super.initState();
    _loadNotes();
  }

  Future<void> _loadNotes() async {
    final notes = await NoteService.loadNotes();
    setState(() => _notes = notes);
  }

  void _deleteNote(int index) {
    setState(() => _notes.removeAt(index));
    NoteService.saveNotes(_notes);
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('Заметка удалена')),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Мои заметки')),
      body: _notes.isEmpty
          ? const Center(child: Text('Нет заметок. Нажмите +'))
          : ListView.builder(
              itemCount: _notes.length,
              itemBuilder: (context, index) {
                final note = _notes[index];
                return Dismissible(
                  key: Key(note.id),
                  direction: DismissDirection.endToStart,
                  background: Container(
                    color: Colors.red,
                    alignment: Alignment.centerRight,
                    padding: const EdgeInsets.only(right: 16),
                    child: const Icon(Icons.delete, color: Colors.white),
                  ),
                  onDismissed: (_) => _deleteNote(index),
                  child: NoteCard(
                    note: note,
                    onTap: () => _editNote(note),
                  ),
                );
              },
            ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _createNote(),
        child: const Icon(Icons.add),
      ),
    );
  }

  void _createNote() async {
    final result = await Navigator.push(
      context,
      MaterialPageRoute(builder: (context) => const EditScreen()),
    );
    if (result != null) {
      setState(() => _notes.add(result));
      NoteService.saveNotes(_notes);
    }
  }

  void _editNote(Note note) async {
    final result = await Navigator.push(
      context,
      MaterialPageRoute(builder: (context) => EditScreen(note: note)),
    );
    if (result != null) {
      final index = _notes.indexWhere((n) => n.id == note.id);
      setState(() => _notes[index] = result);
      NoteService.saveNotes(_notes);
    }
  }
}

Экран создания/редактирования

// lib/screens/edit_screen.dart
class EditScreen extends StatefulWidget {
  final Note? note;  // null = создание, не null = редактирование

  const EditScreen({super.key, this.note});

  @override
  State<EditScreen> createState() => _EditScreenState();
}

class _EditScreenState extends State<EditScreen> {
  final _formKey = GlobalKey<FormState>();
  late TextEditingController _titleController;
  late TextEditingController _contentController;

  @override
  void initState() {
    super.initState();
    _titleController = TextEditingController(text: widget.note?.title ?? '');
    _contentController = TextEditingController(text: widget.note?.content ?? '');
  }

  @override
  void dispose() {
    _titleController.dispose();
    _contentController.dispose();
    super.dispose();
  }

  void _save() {
    if (_formKey.currentState!.validate()) {
      final note = Note(
        id: widget.note?.id ?? const Uuid().v4(),
        title: _titleController.text,
        content: _contentController.text,
        createdAt: widget.note?.createdAt,
      );
      Navigator.pop(context, note);
    }
  }

  @override
  Widget build(BuildContext context) {
    final isEditing = widget.note != null;

    return Scaffold(
      appBar: AppBar(
        title: Text(isEditing ? 'Редактировать' : 'Новая заметка'),
        actions: [
          TextButton(onPressed: _save, child: const Text('СОХРАНИТЬ')),
        ],
      ),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Form(
          key: _formKey,
          child: Column(
            children: [
              TextFormField(
                controller: _titleController,
                decoration: const InputDecoration(
                  labelText: 'Заголовок',
                  border: OutlineInputBorder(),
                ),
                validator: (value) =>
                    value == null || value.isEmpty ? 'Введите заголовок' : null,
              ),
              const SizedBox(height: 16),
              Expanded(
                child: TextFormField(
                  controller: _contentController,
                  decoration: const InputDecoration(
                    labelText: 'Текст заметки',
                    border: OutlineInputBorder(),
                    alignLabelWithHint: true,
                  ),
                  maxLines: null,
                  expands: true,
                  textAlignVertical: TextAlignVertical.top,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Хранение данных

NoteService — сохранение в SharedPreferences

// lib/services/note_service.dart
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/note.dart';

class NoteService {
  static const String _key = 'notes';

  static Future<List<Note>> loadNotes() async {
    final prefs = await SharedPreferences.getInstance();
    final json = prefs.getString(_key);
    if (json == null) return [];
    final List list = jsonDecode(json);
    return list.map((e) => Note.fromJson(e)).toList();
  }

  static Future<void> saveNotes(List<Note> notes) async {
    final prefs = await SharedPreferences.getInstance();
    final json = jsonEncode(notes.map((n) => n.toJson()).toList());
    await prefs.setString(_key, json);
  }
}

Карточка заметки

// lib/widgets/note_card.dart
class NoteCard extends StatelessWidget {
  final Note note;
  final VoidCallback onTap;

  const NoteCard({required this.note, required this.onTap});

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
      child: ListTile(
        title: Text(note.title, style: const TextStyle(fontWeight: FontWeight.bold)),
        subtitle: Text(
          note.content,
          maxLines: 2,
          overflow: TextOverflow.ellipsis,
        ),
        trailing: Text(
          '${note.createdAt.day}.${note.createdAt.month}.${note.createdAt.year}',
          style: const TextStyle(color: Colors.grey, fontSize: 12),
        ),
        onTap: onTap,
      ),
    );
  }
}

main.dart

// lib/main.dart
import 'package:flutter/material.dart';

void main() {
  runApp(const NotesApp());
}

class NotesApp extends StatelessWidget {
  const NotesApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Заметки',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
        useMaterial3: true,
      ),
      home: const HomeScreen(),
    );
  }
}

Сборка и тестирование

Запуск

# На эмуляторе
flutter run

# В Chrome
flutter run -d chrome

Сборка APK (Android)

# Debug APK (для тестов)
flutter build apk --debug

# Release APK (для публикации)
flutter build apk --release

# App Bundle (для Google Play)
flutter build appbundle --release

# Файл будет в: build/app/outputs/flutter-apk/

Сборка iOS

# Только на macOS!
flutter build ios --release

# Или через Xcode:
open ios/Runner.xcworkspace

Проверка перед релизом

# Анализ кода
flutter analyze

# Тесты
flutter test

# Размер приложения
flutter build apk --analyze-size
🎉 Поздравляем!

Вы собрали полноценное Flutter-приложение: список, CRUD, навигация, формы, валидация, локальное хранение, сборка APK. Теперь вы можете перейти к продвинутым темам: state management (BLoC/Provider), networking (Dio/FastAPI), анимации.

💡 Что делать дальше

1. Добавьте поиск по заметкам (TextField + фильтрация списка)
2. Добавьте категории/теги для заметок
3. Подключите FastAPI бэкенд (см. модуль 6 курса)
4. Перепишите на Provider вместо setState (см. модуль 3)
5. Добавьте push-уведомления для напоминаний

Система типов и Null-safety

Dart — статически типизированный язык. В отличие от Python, где тип определяется в рантайме, в Dart типы известны во время компиляции. Это роднит его с C++. При этом, как и в Python, все значения — объекты (экземпляры классов). Даже числа, строки, функции и null — объекты.

🛠 Под капотом: Зачем статическая типизация?

Статическая типизация позволяет AOT-компилятору (Ahead-Of-Time) генерировать оптимизированный машинный код. Компилятор знает точные типы всех переменных, поэтому может выбирать оптимальные инструкции CPU, не вставляя runtime-проверки типов. Это одна из причин, почему Flutter быстрее React Native.

Основные встроенные типы

ТипОписаниеСравнение с C++ / Python
intЦелые числа, 64 бита (VM) или 53 бита (JS)Как long long в C++, но с проверкой диапазона
doubleIEEE 754, 64 битаАналог double в C++
numСупертип для int и doubleНет прямого аналога
StringUTF-16, неизменяемый (Immutable)Как std::string в C++, но immutable
booltrue / falseИдентично C++ / Python
List<T>Упорядоченная коллекцияstd::vector<T> в C++, list в Python
Set<T>Неупорядоченное множество без дубликатовstd::set<T> в C++, set в Python
Map<K, V>Пары ключ-значениеstd::unordered_map в C++, dict в Python

Ключевые отличия от C++ и Python

  • Нет массивов — вместо них List. Списки бывают с фиксированным (List.filled(5, 0)) и произвольным количеством элементов.
  • Строки неизменяемыs[0] = 'П' вызовет ошибку. Для модификации нужно создать новый объект.
  • Присваивание копирует ссылку, не значение. var newList = myList — обе переменные указывают на один объект. Для копии: List.from(myList) или [...myList].
  • dynamic — тип, позволяющий хранить значения любых типов (как Any в Python). Не рекомендуется к повсеместному использованию.
  • var позволяет компилятору вывести тип автоматически — аналог auto в C++ (начиная с C++11).
var b = 10;      // int
var d = 1.1;     // double
var s = 'text';  // String

// Вложенные коллекции — поверхностное копирование!
var myList = [[10, 3]];
var newList = List.from(myList);
newList[0].add(77);  // myList тоже изменится!

final vs const vs late

Хотя final и const означают неизменяемость, разница между ними фундаментальна в контексте выделения памяти:

  • final: Переменная вычисляется один раз во время выполнения (runtime).
  • const: Значение должно быть известно во время компиляции (compile-time). Константы канонизируются: если вы создадите два одинаковых const-объекта, в памяти будет лежать только один инстанс.
  • late: Отложенная инициализация. Переменная non-nullable, но инициализируется позже (при первом обращении).
void main() {
    // Вычисляется в рантайме. Идеально для ответов от API.
    final currentTime = DateTime.now();
    
    // Вычисляется при компиляции. Значение жёстко вшивается в бинарник.
    const double pi = 3.14159;
    
    // Ошибка компиляции! DateTime.now() неизвестно до запуска программы.
    // const time = DateTime.now();
    
    // late — отложенная инициализация
    late String name;
    // print(name);  // LateInitializationError!
    name = 'Михаил';
    print(name);     // OK
    
    // lazy initialization
    late final expensiveValue = computeExpensive();
    // computeExpensive() не вызовётся, пока не обратимся к expensiveValue
}
💡 Сравнение с C++

const в Dart близок к constexpr в C++. late аналогичен std::call_once в C++ для ленивой инициализации. В Python нет прямого аналога ни для final, ни для const.

Sound Null Safety (Dart 2.12+)

Начиная с Dart 2.12, все переменные по умолчанию не могут хранить null. Это compile-time проверка, а не runtime. В отличие от Python, где Optional — это просто аннотация типа (не запрещает null на этапе компиляции).

🛠 Под капотом: Sound Null Safety

«Sound» означает, что компилятор на 100% гарантирует: если переменная не помечена как nullable (через ?), она никогда не будет содержать null. Это позволяет AOT-компилятору генерировать более оптимизированный машинный код, так как ему не нужно вставлять проверки на null во время выполнения.

// Nullable типы — оператор ?
Cat? myCat = null;  // OK
int? a;
String? name = null;

// Операторы для работы с null
cat?.helloMaster();                  // ?. — условный вызов
final someCat = cat ?? Cat();        // ?? — значение по умолчанию
cat!.helloMaster();                  // ! — утверждение "не null" (runtime исключение если null)

// Присваивание nullable → non-nullable
Cat? cat = Cat();
// Cat newCat = cat;  // Ошибка! 'Cat?' не может быть 'Cat'
Cat newCat = cat;    // OK, если компилятор доказал что не null
💡 Аналоги в C++ / Python

В C++17: std::optional с .value_or(). В Python: if obj is not None: (но это runtime проверка). В Dart проверка происходит на этапе компиляции.

JIT vs AOT компиляция

Dart поддерживает два режима компиляции, что является ключевым аргументом выбора языка для Flutter. Эта уникальная комбинация даёт и скорость разработки, и скорость выполнения.

JIT (Just-In-Time)

  • Код компилируется непосредственно перед использованием
  • Аналог: JavaScript + V8 в Chromium
  • Преимущества: не зависит от архитектуры платформы, поддерживает Hot Reload
  • Недостатки: компиляция в runtime может замедлять быстродействие
  • Используется в debug-режиме Flutter

AOT (Ahead-Of-Time)

  • Весь код компилируется заранее в нативный бинарный файл
  • Аналог: C++, Java (JVM bytecode → native)
  • Преимущества: высокое быстродействие, native performance
  • Недостатки: бинарник под одну целевую платформу
  • Используется в release-режиме Flutter

Режимы Flutter

РежимКомпиляцияНазначение
DebugJITHot Reload, быстрая итерация
Release iOSAOTProduction, native ARM код
Release AndroidAOT / CoreJITProduction

Snapshot-ы

В debug-режиме Flutter формирует snapshots — снимки состояния, которые переиспользуются при Hot Reload:

  • Script snapshot — снимок скомпилированного кода
  • App snapshot — снимок состояния приложения
  • Full snapshot — полный снимок с библиотеками
  • Kernel Binary (.dill) — промежуточное представление кода
⚡ Ключевой аргумент для Flutter

AOT компилирует Dart в эффективный native код → Flutter быстрый. JIT позволяет Hot Reload → быстрая итерация. Это уникальная комбинация, которой нет у JavaScript (только JIT) или C++ (только AOT).

💡 Сравнение с C++ / Python

Как C++: AOT компиляция даёт аналогичную производительность. Как Python: JIT в debug-режиме позволяет интерактивную разработку. В отличие от Python, Dart статически типизирован и компилируется в native code.

Event Loop и Isolates

Event Loop — однопоточная модель

В Dart однопоточная модель с event loop. Это ключевое отличие от Python, где есть threading и GIL.

🛠 Под капотом: Две очереди

Event Loop в Dart работает с двумя очередями:
1. Microtask Queue — приоритетная. Сюда попадают Future.then(), scheduleMicrotask()
2. Event Queue — обычная. Сюда попадают I/O, таймеры, события UI, Future() constructor

Правило: Event Loop сначала полностью опустошает Microtask Queue, только потом берёт один Event из Event Queue, и повторяет.

void main() {
    print('1: start');
    
    Future(() => print('2: event queue'));
    
    Future.microtask(() => print('3: microtask'));
    
    scheduleMicrotask(() => print('4: microtask'));
    
    print('5: synchronous');
}
// Вывод: 1, 5, 3, 4, 2
// Сначала синхронный код, потом microtasks, потом events

Isolates — параллельные вычисления

Isolate — это отдельная единица выполнения со своей собственной памятью. В отличие от потоков (threads), isolates не разделяют память. Обмен данными происходит через message passing (как в Erlang/Akka). Это безопаснее, чем shared memory — нет race conditions.

// Создание isolate
final receivePort = ReceivePort();
await Isolate.spawn(isolateFunction, receivePort.sendPort);

// Обмен сообщениями (не общая память!)
receivePort.listen((message) {
    print('Получено: $message');
});

// Функция для isolate
void isolateFunction(SendPort sendPort) {
    sendPort.send('Привет из isolate!');
}

// Compute — упрощённый вариант для одноразовых тяжёлых вычислений
final result = await compute(heavyFunction, inputData);
PythonDart
threading.ThreadНет аналога (нет threads)
multiprocessing.ProcessIsolate
GIL блокирует параллелизмНет GIL, каждый isolate = отдельный поток
Queue для обменаSendPort / ReceivePort
pickle для сериализацииСообщения должны быть сериализуемыми
💡 Когда использовать Isolates

Тяжёлые вычисления (парсинг, шифрование, ML inference), обработка изображений/видео — всё, что не должно блокировать UI thread (main isolate).

Futures и async/await

Future<T> в Dart = Promise в JS = Task в C++ = Future в Python. Это обещание результата, который будет доступен в будущем.

🛠 Под капотом: suspension points

await не блокирует поток. Функция «замораживается» в точке await (suspension point), event loop продолжает обрабатывать другие события. Когда результат готов — выполнение возобновляется. Это аналогично тому, как работает await в Python asyncio.

// Базовый async/await
Future<String> fetchUser() async {
    final response = await http.get('/user');
    return response.body;
}

// Цепочки через .then()
fetchUser()
    .then((user) => parseUser(user))
    .then((parsed) => saveUser(parsed))
    .catchError((e) => handleError(e));

// async* — генератор потока (аналог async generator в Python)
Stream<int> countStream() async* {
    for (int i = 0; i < 10; i++) {
        await Future.delayed(Duration(seconds: 1));
        yield i;  // Как yield в Python generators
    }
}

// sync* — ленивый итератор (аналог generator в Python)
Iterable<int> fibonacci() sync* {
    int a = 0, b = 1;
    while (true) {
        yield a;
        var temp = a;
        a = b;
        b = temp + b;
    }
}

Streams — последовательности асинхронных событий

Streams — это последовательности асинхронных событий (от 0 до n). Если Future — одно значение, то Stream — несколько.

// StreamController — создание Stream вручную
final controller = StreamController<int>();

controller.sink.add(1);
controller.sink.add(2);

controller.stream.listen((data) => print(data));
controller.close();  // Обязательно закрыть!

// Подписка с обработкой ошибок
stream.listen(
    (data) => print(data),
    onError: (error) => print(error),
    onDone: () => print('done'),
    cancelOnError: true,  // отмена при ошибке
);
КонцепцияDartPython
Одно значениеFuture<T>asyncio.Future
Несколько значенийStream<T>asyncio.Queue / async generator
Ожиданиеawaitawait
Создание потокаStreamControlleryield в async def
Event loopВстроенasyncio.run()

Generics

Generics обеспечивают типобезопасность на этапе компиляции. В Dart generics реифицированные — информация о типе сохраняется в рантайме.

🛠 Под капотом: Reified Generics

В отличие от Java (type erasure) и Python (type hints стираются), в Dart generics реифицированы — тип T доступен в рантайме. Это позволяет делать if (<int>[] is List<int>) и получать true.

// Базовое использование
T getFirst<T>(List<T> items) => items.first;

// Констрейнты (аналог template constraints в C++)
T max<T extends Comparable<T>>(T a, T b) => a.compareTo(b) > 0 ? a : b;

// Covariance — List<int> НЕ является List<num>
List<int> ints = [1, 2, 3];
// List<num> nums = ints; // Ошибка!
💡 Сравнение с C++

В C++ templates — это макросы, которые генерируют код для каждого типа (monomorphization). В Dart generics — единый код с runtime type checks (как Java generics, но без type erasure).

Records и Pattern Matching

Records (Dart 3)

Records — это анонимные, иммутабельные, структурированные типы данных. Впервые в Dart 3.

// Positional
var record = (1, 'hello', 3.14);
print(record.$1); // 1
print(record.$2); // 'hello'

// Named
var user = (name: 'John', age: 30);
print(user.name); // John

// Mixed
var mixed = (1, name: 'John', 3.14);

// Деструктуризация
(String, int) getUser() => ('John', 30);
var (name, age) = getUser();
💡 Сравнение с Python / C++

В Python — namedtuple (аналог). В C++ — struct (похоже, но именованное). Records в Dart типобезопасны и работают с pattern matching.

Pattern Matching (Dart 3)

Мощный инструмент для деструктуризации и проверки данных. Аналог: match в Rust, switch с pattern в C++ (C++20 proposals).

// Constant Pattern
switch (command) {
    case 'quit': exit(0);
    case 'help': showHelp();
    default: unknown();
}

// Variable Pattern (деструктуризация)
switch (user) {
    case (var name, var age): print('$name, $age');
}

// Wildcard Pattern (_)
switch (point) {
    case (0, 0): print('origin');
    case (_, 0): print('on x-axis');
    case (0, _): print('on y-axis');
}

// Guard clause (when)
switch (shape) {
    case Circle(radius: var r) when r > 10: print('Big circle');
    case Circle(): print('Small circle');
}

// Null-check
switch (maybeString) {
    case var s?: print('not null: $s');
    case null: print('is null');
}
💡 Сравнение с Python

Python 3.10+ имеет match/case, но Dart 3 pattern matching более мощный: есть guard clauses, exhaustive checking, и работает с Records.

Mixins и Extension Methods

Mixins

Mixin — это способ добавить поведение в класс без наследования. В Dart mixin — это полноценная фича языка, не декоратор как в Python.

// Определение mixin
mixin JsonMixin {
    Map<String, dynamic> toJson() => {'runtimeType': runtimeType};
}

// Использование
class User extends BaseModel with JsonMixin {
    String name;
}

// mixin с ограничением on
mixin Swimmer on Animal {
    void swim() => print('$name is swimming'); // name от Animal
}

class Animal { String name; }
class Duck extends Animal with Swimmer {} // OK
// class Car with Swimmer {} // Ошибка! Car не наследует Animal
💡 Ключевые отличия от Python

1. mixin — отдельное ключевое слово (не класс). 2. on — ограничение: можно применить только к классам, наследующим определённый тип. 3. Нет проблем с MRO (Method Resolution Order). 4. mixin не может иметь конструктор.

Extension Methods

Extension methods позволяют добавить методы к существующим классам без модификации их кода. Придумано в C# (LINQ), адаптировано в Dart.

// Определение
extension StringExtensions on String {
    String get capitalizeFirst =>
        '${this[0].toUpperCase()}${substring(1)}';
}

// Использование — как обычный метод
print('hello'.capitalizeFirst); // Hello

// Популярный паттерн для Flutter
extension BuildContextExtensions on BuildContext {
    ThemeData get theme => Theme.of(this);
    MediaQueryData get mediaQuery => MediaQuery.of(this);
    NavigatorState get navigator => Navigator.of(this);
}

// Вместо Theme.of(context).textTheme.headline6
// context.theme.textTheme.headline6
🛠 Под капотом

Extension methods — статический диспатч, разрешается в compile time. Не модифицирует оригинальный класс (синтаксический сахар). Не работает с dynamic — нужен известный тип.

Абстрактные классы, Enum и Sealed Classes

Абстрактные классы

Абстрактный класс — класс, который нельзя инстанцировать напрямую. Он определяет контракт для наследников. Может содержать как абстрактные методы (без реализации), так и обычные (с реализацией).

abstract class Animal {
  // Абстрактный метод — без реализации
  void speak();

  // Обычный метод — наследники получат
  void breathe() => print('breathing...');
}

class Dog extends Animal {
  @override
  void speak() => print('Woof!');
}

// Animal a = Animal(); // Ошибка! Нельзя создать экземпляр
Animal a = Dog(); // OK — переменная типа Animal, объект Dog
💡 Сравнение с Python / C++

Python: ABC + @abstractmethod. C++: чисто виртуальные методы virtual void f() = 0;. В Dart абстрактный класс может иметь конструктор и state — это больше, чем просто интерфейс.

Enum — перечисления

Enum в Dart — это полноценные классы (начиная с Dart 2.17). Могут иметь поля, методы, конструкторы.

// Базовый enum
enum Color { red, green, blue }
print(Color.red.name);   // 'red'
print(Color.red.index);  // 0

// Enhanced enum (Dart 2.17+) — с полями и методами
enum Planet {
  mercury(3.303e+23, 2.4397e6),
  venus(4.869e+24, 6.0518e6),
  earth(5.976e+24, 6.37814e6);

  final double mass;
  final double radius;
  const Planet(this.mass, this.radius);

  double get surfaceGravity => 6.67300E-11 * mass / (radius * radius);
}

print(Planet.earth.surfaceGravity); // ~9.8
💡 Сравнение

Python: enum.Enum. C++: enum class. Kotlin: enum class с properties. Dart enhanced enums мощнее — это полноценные классы с конструкторами.

Sealed Classes (Dart 3)

sealed class — абстрактный класс, подтипы которого известны компилятору (все в одном файле). Ключевое преимущество — exhaustive pattern matching: компилятор проверяет, что обработали все варианты.

sealed class Shape {}

class Circle extends Shape {
  final double radius;
  Circle(this.radius);
}

class Square extends Shape {
  final double side;
  Square(this.side);
}

// Exhaustive switch — компилятор проверяет все варианты
double area(Shape shape) {
  return switch (shape) {
    Circle c => 3.14 * c.radius * c.radius,
    Square s => s.side * s.side,
    // Убрали Triangle? → ОШИБКА КОМПИЛЯЦИИ!
  };
}
🛠 Под капотом: Sealed vs Abstract

abstract: подтипы могут быть где угодно, компилятор не знает полный список.
sealed: подтипы только в одном файле → компилятор знает все варианты → exhaustive checking.
Идеально для состояний (Success/Error/Loading), событий BLoC, алгебраических типов данных.

Typedef и типы функций

typedef создаёт псевдоним для типа. Чаще всего — для типов функций. В Dart функции — объекты первого класса (first-class citizens), их можно передавать, возвращать, хранить в переменных.

// Без typedef — тип функции писать каждый раз
void processItems(List<String> items, void Function(String) callback) {
  for (var item in items) callback(item);
}

// С typedef — читаемо и переиспользуемо
typedef StringCallback = void Function(String);
typedef Predicate<T> = bool Function(T);

void processItems(List<String> items, StringCallback callback) {
  for (var item in items) callback(item);
}

// Функции как объекты
int Function(int) doubler = (x) => x * 2;
print(doubler(5)); // 10
💡 Сравнение с Python / C++

Python: Callable[[str], None] из typing. C++: using F = void(*)(const string&);.
В Flutter typedef активно используется: WidgetBuilder = Widget Function(BuildContext), типы для BLoC events/states.

Named и Optional параметры

// Named parameters (в фигурных скобках)
void greet({required String name, int age = 0}) {
  print('$name, $age');
}
greet(name: 'Дима', age: 25);

// Optional positional (в квадратных скобках)
void log(String message, [String? prefix]) {
  print('${prefix ?? ''} $message');
}
log('error', '!!!');

// Cascade notation (..)
var list = [1, 2, 3]
  ..add(4)
  ..add(5)
  ..sort();
// list = [1, 2, 3, 4, 5]

Коллекции

List

const list = [1, 2, 3];           // Неизменяемый (compile-time)
var typed = <String>['a', 'b'];   // Типизированный

// Spread operator (как *list в Python)
var combined = [...list1, ...list2];

// Collection if/for — аналог list comprehension в Python!
var filtered = [
    for (var item in items)
        if (item.isActive) item.name,
];

Set

var set = {1, 2, 3};           // Set<int>, не Map!
var union = set1.union(set2);
var intersection = set1.intersection(set2);

Map

var map = {'key': 'value'};
map['newKey'] = 'newValue';
map.putIfAbsent('key', () => 'default'); // setdefault в Python

// Spread
var merged = {...map1, ...map2};

Итерация и трансформации

// map, where, reduce — как в Python
items.where((i) => i.active).map((i) => i.name).toList();

// fold (аналог reduce)
var sum = numbers.fold(0, (acc, n) => acc + n);
💡 Сравнение с Python

Collection for/if в Dart — аналог list comprehension в Python: [item.name for item in items if item.is_active].

main(), runApp и StatelessWidget/StatefulWidget

Точка входа Flutter приложения

Каждое Flutter-приложение начинается с main(), которая вызывает runApp().

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'My App',
      theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), useMaterial3: true),
      home: const HomePage(),
    );
  }
}
🛠 Под капотом: runApp()

Создаёт корневой RenderObject, прикрепляет виджет к элементу дерева (inflate), запускает rendering pipeline, подключает WidgetsFlutterBinding.

StatelessWidget

Immutable. build() один раз.

class Greeting extends StatelessWidget {
  final String name;
  const Greeting({super.key, required this.name});
  @override
  Widget build(BuildContext context) => Text('Привет, $name!');
}

StatefulWidget

Mutable состояние. Виджет (immutable) + State (mutable).

class Counter extends StatefulWidget {
  const Counter({super.key});
  @override
  State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
  int count = 0;
  @override
  Widget build(BuildContext context) {
    return Column(children: [
      Text('$count'),
      ElevatedButton(onPressed: () => setState(() => count++), child: const Text('+1')),
    ]);
  }
}
🛠 Под капотом: setState()

setState() помечает State как dirty, запускает build-цикл. Flutter сравнивает деревья (reconciliation) и обновляет только изменившиеся элементы. Как Virtual DOM в React, но через Element tree.

pubspec.yaml и зависимости

pubspec.yaml — манифест проекта (как package.json, Cargo.toml).

name: my_app
version: 1.0.0+1
environment:
  sdk: '>=3.0.0 <4.0.0'
dependencies:
  flutter:
    sdk: flutter
  http: ^1.1.0
  dio: ^5.3.2
dev_dependencies:
  flutter_test:
    sdk: flutter
flutter pub get       # Установить
flutter pub upgrade   # Обновить
flutter pub add dio   # Добавить пакет
🛠 pubspec.lock

Фиксирует точные версии всех транзитивных зависимостей. Коммитить в git.

🌳 Три дерева Flutter
Widget Tree MaterialApp Scaffold AppBar Element Tree StatefulEl RenderEl CompEl RenderObject RenderView RenderBox RenderPar Widget = конфигурация (immutable) Element = инстанс в дереве (lifecycle) RenderObject = layout + paint Widget может быть создан 0..N раз → каждый раз = новый Element

Три дерева: Widget, Element, RenderObject

Flutter работает с тремя параллельными деревьями. Понимание их различий — ключ к пониманию фреймворка «под капотом».

Widget

  • Неизменяемая (immutable) конфигурация
  • Описывает, как должен выглядеть UI
  • Все поля final, устанавливаются в конструкторе
  • Конструкторы используют только именованные параметры
  • Создаётся быстро и дёшево

Element

  • Конкретный инстанс Widget в определённом месте дерева
  • Widget может быть инстанцирован 0 или N раз
  • Процесс создания: inflation (через Widget.createElement())
  • BuildContext — это на самом деле интерфейс класса Element

RenderObject

  • Вычисляет размеры и позиции элементов
  • Составляет render tree, корень — RenderView
  • Определяет базовый layout-протокол: layout(Constraints)
  • Родитель передаёт Constraints детям, дети возвращают размеры
🛠 Под капотом: Аналогия для C++/Python разработчика

Widget ≈ конфигурационный файл (декларативное описание).
Element ≈ экземпляр класса, хранящий состояние и жизненный цикл.
RenderObject ≈ объект, отвечающий за физическую отрисовку (аналог render pass в graphics pipeline).

class MyWidget extends StatelessWidget {
    const MyWidget({Key? key}) : super(key: key);
    @override
    Widget build(BuildContext context) {
        return Text('Hello');
    }
}
  • Если parentUsesSize == true в методе layout(), то при изменении layout ребёнка потребуется перерасчёт и родителя
  • Информация layout хранится в свойстве parentData
  • Виджеты для layout делятся на: SingleChildRenderObjectWidget (один ребёнок) и MultiChildRenderObjectWidget (несколько детей)

В Android Studio можно видеть оба дерева: View → Tool Windows → Flutter Inspector. Верхняя панель — widgets tree, нижняя — render tree.

🏗️ Архитектура Flutter
Ваш Dart-код (Widgets, UI) Flutter Framework (Material, Cupertino) Flutter Engine (C++ / Skia / Impeller) Android (JNI/NDK) iOS (Objective-C)

Rendering Pipeline (Skia)

Flutter не использует нативные компоненты платформы. Всё рисуется через Skia (C++ библиотека). Это даёт консистентный вид на всех платформах, нативную производительность (AOT компиляция) и полный контроль над отрисовкой.

🛠 Под капотом: Pipeline

Процесс рендеринга:
1. Build — создание/обновление дерева виджетов
2. Layout — вычисление размеров и позиций (RenderObject)
3. Paint — отрисовка через Skia на GPU-backed canvas
4. Compositing — объединение слоёв
5. Rasterization — финальный рендер в пиксели

Hot Reload vs Hot Restart

ОперацияЧто делаетСкорость
Hot ReloadПрименяет изменения без перезапуска. Сохраняет состояние< 1 сек
Hot RestartПерезапускает приложение. Сбрасывает состояниеНесколько секунд

Hot Reload работает через JIT компиляцию — обновляется только изменённый код.

🔄 StatefulWidget Lifecycle
createState State() initState 1 раз didChangeDeps после initState build() каждый раз dispose 1 раз setState() → build()

Widget Lifecycle

StatelessWidget Lifecycle

StatelessWidget имеет простой жизненный цикл: Constructor → build()

Метод build() вызывается в трёх случаях: при первом создании виджета, когда родитель виджета изменился, когда изменился InheritedWidget, от которого виджет зависит.

StatefulWidget Lifecycle

StatefulWidget состоит из двух классов: StatefulWidget (пересоздаётся при изменении конфигурации) и State (сохраняется, что повышает производительность).

МетодКогда вызывается
createState()Создаёт объект State
initState()Один раз при вставке объекта в дерево
didChangeDependencies()После initState и при изменении зависимых InheritedWidget
build()Каждый раз при необходимости перерисовки UI
didUpdateWidget(oldWidget)Когда родитель пересоздаёт виджет с новой конфигурацией
setState()Уведомляет фреймворк об изменении состояния
dispose()При удалении элемента из дерева
🛠 Под капотом: Dirty vs Clean

Clean: виджет стабилен, build не нужен. Dirty: состояние изменилось, нужен вызов build(). State может пережить пересоздание StatefulWidget — это оптимизация. Если StatefulWidget удалён из дерева и вставлен обратно — создаётся новый объект State.

Element Lifecycle

createElement() → mount() → update() → deactivate() → unmount()
  • Created — Element создан через widget.createElement()
  • Active — после mount(), элемент в дереве
  • Updatedupdate() при изменении состояния
  • Inactive — после deactivateChild()
  • Defunct — после unmount()

App Lifecycle

class AppLifecycleObserver extends WidgetsBindingObserver {
    @override
    void didChangeAppLifecycleState(AppLifecycleState state) {
        switch (state) {
            case AppLifecycleState.resumed:  break; // На переднем плане
            case AppLifecycleState.inactive: break; // Неактивно (звонок)
            case AppLifecycleState.paused:   break; // Свёрнуто
            case AppLifecycleState.detached: break; // Отсоединено от engine
        }
    }
}
💡 Сравнение с C++ / Python

initState() ≈ конструктор с отложенной инициализацией. dispose() ≈ деструктор (~Class() в C++). setState() ≈ паттерн Observer/Signal — уведомление системы об изменении данных.

BuildContext

BuildContext — это интерфейс класса Element. Когда вы видите BuildContext context в методе build(), на самом деле это ссылка на Element в дереве, представляющий местоположение текущего виджета.

class WithBuildContext extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
        Column column = context.ancestorWidgetOfExactType(Column);
        return Text(column.children.length.toString());
    }
}
🛠 Под капотом: Механизм зависимостей

Когда виджет вызывает inheritFromWidgetOfExactType, он регистрирует зависимость от InheritedWidget. При изменении InheritedWidget все зарегистрированные виджеты будут перестроены. Именно на этом механизме построены подходы к управлению состоянием вроде Provider.

💡 Аналогия для C++/Python

BuildContext похож на указатель/ссылку на ноду в дереве (как DOM-узел в web), через который можно «проброситься» вверх или вниз по дереву для доступа к данным предков или потомков.

Keys

Ключи позволяют Flutter идентифицировать виджеты при обновлении дерева. Когда фреймворк сравнивает старое и новое дерево виджетов, он использует runtimeType и key: если совпадают — виджет обновляется на месте, если нет — старый элемент удаляется, создаётся новый.

Типы ключей

  • ValueKey<T> — ключ на основе значения
  • ObjectKey — ключ на основе identity объекта
  • UniqueKey — уникальный ключ, каждый раз новый
  • GlobalKey — уникален во всём приложении. Позволяет перемещать виджет по дереву без потери состояния
// GlobalKey для доступа к состоянию формы
GlobalKey<FormState> _formKey = GlobalKey<FormState>();

Form(key: _formKey, child: ...)

if (_formKey.currentState!.validate()) {
    _formKey.currentState!.save();
}

// LocalKey для идентификации элементов списка
Dismissible(key: ValueKey(item.id), ...)
💡 Сравнение с C++ / Python

ValueKey ≈ хеш-ключ в std::unordered_map. GlobalKey ≈ глобальный идентификатор (UUID). UniqueKeyid(obj) в Python.

setState и InheritedWidget

setState() — базовый подход

Самый простой подход к управлению состоянием. Вызов setState() помечает виджет как dirty и вызывает перестроение. Подходит для локального состояния (эфемерного) в пределах одного виджета.

setState(() {
    _counter++;
});

Проблема: prop drilling — передача данных через множество уровней вложенности. Не подходит для сложных приложений.

InheritedWidget — фундамент state management

Специальный виджет для передачи данных вниз по дереву виджетов без явного прокидывания через конструкторы. На его основе построены Provider, BLoC и другие решения.

class AppState extends InheritedWidget {
    final int counter;
    final VoidCallback increment;
    
    const AppState({
        required this.counter,
        required this.increment,
        required Widget child,
    }) : super(child: child);
    
    static AppState of(BuildContext context) {
        return context.dependOnInheritedWidgetOfExactType<AppState>()!;
    }
    
    @override
    bool updateShouldNotify(AppState oldWidget) {
        return counter != oldWidget.counter;
    }
}
🛠 Под капотом: Механизм уведомлений

InheritedWidget содержит список ссылок на все зависящие от него виджеты. При пересоздании: 1. InheritedElement вызывает notifyDependent(). 2. notifyDependent() вызывает на виджете didChangeDependencies(). 3. Виджет перестраивается.

Сравнение подходов

ПодходПростотаПроизводительностьТестируемость
setState★★★★★★★★
InheritedWidget★★★★★★★★★★
Provider★★★★★★★★★★★★★
BLoC★★★★★★★★★★★★★

BLoC Pattern

BLoC (Business Logic Component) — это компонент бизнес-логики, инкапсулирующий слой бизнес-логики приложения. BLoC — это аналог ViewModel в MVVM.

Архитектура

Events (Input) → BLoC (Logic) → State (Output)
                       ↑                ↓
                  Repository        Widgets

Реализация на чистых Streams

class CounterBloc {
    int _count = 0;
    final _counterController = StreamController<int>();
    final _counterEventController = StreamController<CounterEvent>();
    
    Stream<int> get counterStream => _counterController.stream;
    Sink<CounterEvent> get counterEventSink => _counterEventController.sink;
    
    CounterBloc() {
        _counterEventController.stream.listen(_mapEventToState);
    }
    
    void _mapEventToState(CounterEvent event) {
        if (event is Increment) _count++;
        if (event is Decrement) _count--;
        _counterController.sink.add(_count);
    }
    
    void dispose() {
        _counterController.close();
        _counterEventController.close();
    }
}

BLoC Library (flutter_bloc)

// BlocProvider — передаёт BLoC дереву виджетов через InheritedWidget
BlocProvider(
    create: (context) => WeatherBloc(repository),
    child: WeatherPage(),
)

// BlocBuilder — подписывается на изменения состояния
BlocBuilder<WeatherBloc, WeatherState>(
    builder: (context, state) {
        if (state is WeatherLoading) return CircularProgressIndicator();
        if (state is WeatherLoaded) return WeatherList(state.weather);
        return ErrorWidget();
    },
)
💡 Сравнение с Python

BLoC похож на паттерн Observer из Python (но с типизацией). StreamController аналогичен asyncio.Queue но с broadcast возможностями.

Provider и Riverpod

Provider

Современный подход, основанный на InheritedWidget. Проще BLoC и Redux. Рекомендуется сообществом Flutter.

// Создание
ChangeNotifierProvider(
    create: (_) => CounterModel(),
    child: MyApp(),
)

// Использование через Consumer
Consumer<CounterModel>(
    builder: (context, model, child) => Text('${model.count}'),
)

// Чтение без перестройки
final model = Provider.of<CounterModel>(context, listen: false);

Рекомендации по выбору

  • Простые виджетыsetState()
  • Локальное состояние нескольких виджетов → InheritedWidget / Provider
  • Сложная бизнес-логика → BLoC
  • Строгий контроль → Redux
  • Современный подход → Riverpod
💡 Сравнение с C++ / Python

Provider ≈ DI-контейнер с реактивными зависимостями. В C++/Qt: Signal/Slot. В Python: Dependency injection.

Reactive Programming

Flutter — декларативный фреймворк. UI строится как функция от состояния: UI = f(state). В отличие от императивного подхода (Android XML + setText()), во Flutter вы описываете что должно быть, а не как это сделать.

StreamBuilder — реактивный виджет

StreamBuilder<List<Todo>>(
    stream: todoBloc.todosStream,
    builder: (context, AsyncSnapshot<List<Todo>> snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
            return CircularProgressIndicator();
        }
        if (snapshot.hasError) {
            return Text('Error: ${snapshot.error}');
        }
        return ListView.builder(
            itemCount: snapshot.data!.length,
            itemBuilder: (ctx, i) => TodoTile(snapshot.data![i]),
        );
    },
)

FutureBuilder — для одноразовых данных

FutureBuilder<User>(
    future: api.getUser(id),
    builder: (context, snapshot) {
        if (snapshot.hasData) return Text(snapshot.data!.name);
        if (snapshot.hasError) return Text('Error');
        return CircularProgressIndicator();
    },
)
🛠 Под капотом: InheritedWidget как фундамент

Theme.of(context).textTheme и MediaQuery.of(context).size — это уже использование InheritedWidget. of(context) вызывает BuildContext.inheritFromWidgetOfExactType().

Animations

Flutter предоставляет богатую поддержку анимаций. В основе лежат несколько ключевых классов.

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

  • Ticker — посылает сигнал 60 раз в секунду (60 fps)
  • Animation — генерирует число на каждом тике
  • AnimationController — управляет анимацией: запуск, остановка, сброс, повтор
  • Tween — определяет переход из одного состояния в другое
  • Curve — определяет временную функцию (easing)
class GameScreenState extends State<GameScreen>
    with TickerProviderStateMixin {
    
    AnimationController gameLoopController;
    Animation gameLoopAnimation;
    
    @override
    void initState() {
        super.initState();
        gameLoopController = AnimationController(
            vsync: this,
            duration: Duration(milliseconds: 1000)
        );
        gameLoopAnimation = Tween(begin: 0, end: 17).animate(
            CurvedAnimation(parent: gameLoopController, curve: Curves.linear)
        );
        gameLoopAnimation.addStatusListener((status) {
            if (status == AnimationStatus.completed) {
                gameLoopController.reset();
                gameLoopController.forward();
            }
        });
        gameLoopAnimation.addListener(gameLoop);
        gameLoopController.forward();
    }
}

Неявные vs Явные анимации

Неявные (Implicit) — анимируют автоматически: AnimatedContainer, AnimatedCrossFade, AnimatedOpacity, AnimatedPositioned.

Явные (Explicit) — требуют AnimationController: SlideTransition, ScaleTransition, RotationTransition.

💡 Сравнение с C++

Ticker ≈ игровой цикл (game loop). AnimationController ≈ Timer + state machine. Tween ≈ линейная интерполяция: lerp(start, end, t). Curve ≈ easing function (как в CSS transition-timing-function).

CustomPainter

Если стандартных виджетов недостаточно, можно использовать CustomPaint — виджет, предоставляющий холст (canvas) для рисования произвольных элементов.

class Shapes extends CustomPainter {
    @override
    void paint(Canvas canvas, Size size) {
        // Прямоугоник
        Rect rect = Offset(5, 5) & (size - Offset(5, 5));
        canvas.drawRect(
            rect,
            Paint()
                ..color = Colors.red
                ..strokeWidth = 2
                ..style = PaintingStyle.stroke,
        );
        
        // Круг
        canvas.drawCircle(
            Offset(size.width / 2, size.height / 2),
            50,
            Paint()..color = Colors.blue,
        );
    }

    @override
    bool shouldRepaint(CustomPainter oldDelegate) => false;
}
🛠 Под капотом: shouldRepaint

Метод shouldRepaint() критичен для производительности. Возвращайте true только когда данные действительно изменились. Порядок отрисовки: painterchildforegroundPainter.

💡 Сравнение с C++

Canvas ≈ Graphics объект в Qt. Paint ≈ QPen/QBrush. CustomPainter ≈ callback отрисовки в paintEvent() (Qt).

Responsive Layouts

Основные layout-виджеты

// Row и Column — основа всего layout
Row(
    mainAxisAlignment: MainAxisAlignment.spaceBetween,
    crossAxisAlignment: CrossAxisAlignment.center,
    children: [
        Expanded(flex: 2, child: Text('Left')),
        Expanded(flex: 1, child: Text('Right')),
    ],
)

// Stack — позиционирование элементов друг поверх друга
Stack(children: [
    Positioned(top: 10, left: 10, child: Text('Hello')),
])

MediaQuery

MediaQuery.of(context).size.width
MediaQuery.of(context).size.height
MediaQuery.of(context).orientation

LayoutBuilder — адаптивный layout

LayoutBuilder(
    builder: (context, constraints) {
        if (constraints.maxWidth > 600) {
            return WideLayout();
        } else {
            return NarrowLayout();
        }
    },
)

Breakpoints

double mobileBreakpoint = 600;
double tabletBreakpoint = 1024;

Widget build(BuildContext context) {
    double width = MediaQuery.of(context).size.width;
    if (width < mobileBreakpoint) return MobileLayout();
    if (width < tabletBreakpoint) return TabletLayout();
    return DesktopLayout();
}

CustomScrollView и Slivers

CustomScrollView(
    slivers: [
        SliverAppBar(expandedHeight: 200, floating: true),
        SliverList(delegate: SliverChildListDelegate([...items...])),
        SliverGrid(delegate: ..., gridDelegate: ...),
    ],
)
💡 Сравнение с Qt (C++)

Row/Column → QHBoxLayout/QVBoxLayout. Stack → QStackedLayout. Expanded → stretch factor. MediaQuery → QDesktopWidget.screenGeometry().

Material Design

MaterialApp и Scaffold

MaterialApp(
    title: 'My App',
    theme: ThemeData(
        primarySwatch: Colors.blue,
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
    ),
    darkTheme: ThemeData.dark(),
    home: MyHomePage(),
)

Scaffold(
    appBar: AppBar(title: Text('Title')),
    body: Center(child: Text('Content')),
    floatingActionButton: FloatingActionButton(onPressed: () {}, child: Icon(Icons.add)),
    drawer: Drawer(child: ListView(...)),
    bottomNavigationBar: BottomNavigationBar(items: [...]),
)

ThemeData

ThemeData(
    primarySwatch: Colors.blue,
    fontFamily: 'Roboto',
    textTheme: TextTheme(
        headline1: TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
        bodyText1: TextStyle(fontSize: 16),
    ),
)

// Использование темы
Theme.of(context).textTheme.headline1
Theme.of(context).primaryColor

Стилизация

Container(
    decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(8),
        boxShadow: [BoxShadow(color: Colors.grey, blurRadius: 4)],
    ),
)
💡 Сравнение с Qt (C++)

MaterialApp → QApplication. Scaffold → QMainWindow. AppBar → QToolBar. Card → QFrame + shadow. AlertDialog → QMessageBox.

HTTP и Dio

Для связи с FastAPI сервером мы будем использовать REST запросы. В Dart есть встроенная библиотека http, но стандартом индустрии является пакет dio благодаря встроенной системе интерсепторов.

Пакет http

import 'package:http/http.dart';

// Простой GET
var response = await http.get(Uri.parse('https://api.example.com/data'));

// С параметрами
var uri = Uri.https('api.openweathermap.org', '/data/2.5/forecast', {
    'lat': '55.75',
    'lon': '37.62',
    'appid': API_KEY,
});
var response = await http.get(uri);

Dio (продвинутый HTTP-клиент)

import 'package:dio/dio.dart';

class ApiClient {
    final Dio _dio = Dio(BaseOptions(
        baseUrl: 'http://localhost:8000',
        connectTimeout: const Duration(seconds: 5),
    ));

    Future<Map<String, dynamic>> fetchUserData(int userId) async {
        try {
            final response = await _dio.get('/users/$userId');
            return response.data;
        } on DioException catch (e) {
            if (e.response?.statusCode == 404) {
                throw Exception('Пользователь не найден (404)');
            }
            throw Exception('Ошибка сети');
        }
    }
}

// Interceptor — перехватчик запросов
dio.interceptors.add(InterceptorsWrapper(
    onRequest: (options, handler) {
        options.headers['Authorization'] = 'Bearer $token';
        handler.next(options);
    },
    onError: (error, handler) {
        handler.next(error);
    },
));
💡 Сравнение с Python

http / diorequests / httpx. Dio interceptors → middleware. jsonDecode()json.loads().

JSON Serialization

Ручная сериализация

import 'dart:convert';

class User {
    final String name;
    final String email;
    
    User({required this.name, required this.email});
    
    factory User.fromJson(Map<String, dynamic> json) {
        return User(
            name: json['name'] as String,
            email: json['email'] as String,
        );
    }
    
    Map<String, dynamic> toJson() => {
        'name': name,
        'email': email,
    };
}

// Использование
Map<String, dynamic> json = jsonDecode(responseString);
var user = User.fromJson(json);
String jsonString = jsonEncode(user);

json_serializable (автоматическая сериализация)

import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart';

@JsonSerializable()
class User {
    final String name;
    @JsonKey(name: 'email_address')
    final String email;
    
    User({required this.name, required this.email});
    
    factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
    Map<String, dynamic> toJson() => _$UserToJson(this);
}

// Генерация: flutter pub run build_runner build

Freezed (immutable data classes)

@freezed
class User with _$User {
    const factory User({
        required String name,
        required String email,
    }) = _User;
    
    factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}
💡 Сравнение с Python

fromJson() factory → @dataclass / Pydantic. json_serializabledataclasses-json / Pydantic. Freezed → dataclasses с frozen=True.

Forms и Validation

final _formKey = GlobalKey<FormState>();

Form(
    key: _formKey,
    child: Column(
        children: [
            TextFormField(
                decoration: InputDecoration(
                    labelText: 'Email',
                    hintText: 'user@example.com',
                    prefixIcon: Icon(Icons.email),
                    border: OutlineInputBorder(),
                ),
                validator: (value) {
                    if (value == null || value.isEmpty) {
                        return 'Введите текст';
                    }
                    return null;
                },
            ),
            ElevatedButton(
                onPressed: () {
                    if (_formKey.currentState!.validate()) {
                        _formKey.currentState!.save();
                    }
                },
                child: Text('Отправить'),
            ),
        ],
    ),
)

FocusNodes — управление фокусом

final _focusNode1 = FocusNode();
final _focusNode2 = FocusNode();

TextFormField(
    focusNode: _focusNode1,
    textInputAction: TextInputAction.next,
    onFieldSubmitted: (_) {
        FocusScope.of(context).requestFocus(_focusNode2);
    },
)

// Не забыть dispose!
@override
void dispose() {
    _focusNode1.dispose();
    _focusNode2.dispose();
    super.dispose();
}
💡 Сравнение с Python / C++

Валидация формы: Flutter → validator callback, Python → WTForms / Pydantic, C++ → QValidator (Qt). FocusNode → setFocus() в Qt.

Архитектура FastAPI

🛠 Под капотом: Стек технологий

FastAPI построен на Starlette (ASGI framework) и Pydantic (валидация данных).

FastAPI (API layer) → Starlette (ASGI framework, routing, middleware) → Uvicorn (ASGI server, uvloop + httptools) → Python asyncio (event loop)

# Flask — синхронный, WSGI
from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello():
    return {'message': 'Hello'}  # Блокирует поток

# FastAPI — асинхронный, ASGI
from fastapi import FastAPI
app = FastAPI()

@app.get('/')
async def hello():
    return {'message': 'Hello'}  # Не блокирует event loop

Ключевые отличия от Flask

  • ASGI vs WSGI: FastAPI работает на asyncio, может обрабатывать тысячи concurrent connections
  • Автодокументация: Swagger UI (OpenAPI) генерируется автоматически на /docs
  • Type hints = валидация: Python type hints используются для валидации запросов/ответов
  • Dependency Injection: встроенная система DI
# Запуск
uvicorn main:app --port 8000 --reload
# uvloop + httptools = быстрее, чем gunicorn для async

Routing и APIRouter

Базовый роутинг

from fastapi import FastAPI
app = FastAPI()

@app.get("/items/{item_id}")
async def read_item(item_id: int, q: str = None):
    # item_id — path parameter (int благодаря type hint)
    # q — query parameter (опциональный)
    return {"item_id": item_id, "q": q}

APIRouter — модульная организация

# routes/users.py
from fastapi import APIRouter
user_router = APIRouter()

@user_router.get("/users")
async def get_users(): ...

# main.py
from routes.users import user_router
app = FastAPI()
app.include_router(user_router, prefix="/api/v1")

Path vs Query parameters

# Path — обязательный, часть URL
@app.get("/users/{user_id}")
async def get_user(user_id: int): ...

# Query — опциональный, после ?
@app.get("/users/")
async def list_users(skip: int = 0, limit: int = 100): ...
# GET /users/?skip=0&limit=10

# Request body — через Pydantic модель
@app.post("/users/")
async def create_user(user: UserCreate): ...

Автоматическая документация

  • Swagger UI на /docs
  • ReDoc на /redoc
  • OpenAPI JSON на /openapi.json
💡 Сравнение с Flask Blueprints

Flask: Blueprint('users', __name__) + app.register_blueprint(). FastAPI: APIRouter() + app.include_router() — аналогично, но async.

Pydantic валидация

Pydantic — библиотека валидации данных, основанная на Python type hints.

🛠 Под капотом: pydantic-core на Rust

Pydantic v2 использует Rust (pydantic-core) для валидации. Скорость: ~5-50x быстрее чистого Python.

# Python dataclass — просто контейнер, нет валидации!
from dataclasses import dataclass
@dataclass
class User:
    name: str
    age: int
    # user = User(name=123, age="old") пройдёт без ошибок

# Pydantic — валидация + сериализация
from pydantic import BaseModel, Field

class User(BaseModel):
    name: str = Field(..., min_length=2, max_length=50)
    age: int = Field(..., ge=0, le=150)
    email: str

# user = User(name="A", age=-1, email="bad") → ValidationError!

Модель ответа (response_model)

class UserResponse(BaseModel):
    id: int
    name: str
    # password НЕ включён — не уйдёт в ответ

@app.post("/users/", response_model=UserResponse)
async def create_user(user: UserCreate):
    # Автоматическая фильтрация: только поля из UserResponse
    return created_user

Nested models

class Address(BaseModel):
    city: str
    zip_code: str

class User(BaseModel):
    name: str
    address: Address  # Вложенная модель
    tags: list[str]   # Список строк

Dependency Injection в FastAPI

FastAPI имеет мощную систему Dependency Injection — одну из его killer features.

from fastapi import Depends

# Зависимость — обычная функция
async def get_db():
    db = Database()
    try:
        yield db  # yield = generator-based dependency
    finally:
        await db.close()

# Использование
@app.get("/items/")
async def read_items(db: Database = Depends(get_db)):
    return await db.get_items()

Класс как зависимость

class CommonQueryParams:
    def __init__(self, q: str = None, skip: int = 0, limit: int = 100):
        self.q = q
        self.skip = skip
        self.limit = limit

@app.get("/items/")
async def read_items(params: CommonQueryParams = Depends()):
    # FastAPI создаст экземпляр CommonQueryParams из query params
    ...

Sub-dependencies

async def get_token_header(x_token: str = Header()):
    if x_token != "fake-token":
        raise HTTPException(status_code=400)

async def get_current_user(token: str = Depends(get_token_header)):
    return decode_user(token)

@app.get("/users/me", dependencies=[Depends(get_current_user)])
async def read_users_me():
    ...
💡 Зачем DI

Переиспользование логики (подключение к БД, аутентификация). Тестируемость (легко подменить зависимости). Чистый код (убираем boilerplate из handlers).

OAuth2 и JWT

Схема аутентификации

Client → POST /token (username + password)
       ← Access Token (JWT)
       
Client → GET /protected (Authorization: Bearer ***)
       ← Protected data

OAuth2 Password Flow

from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

@app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
    user = authenticate_user(form_data.username, form_data.password)
    if not user:
        raise HTTPException(status_code=401)
    access_token = create_access_token(data={"sub": user.username})
    return {"access_token": access_token, "token_type": "bearer"}

@app.get("/users/me")
async def read_users_me(token: str = Depends(oauth2_scheme)):
    payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
    return get_user(payload["sub"])

JWT (JSON Web Token)

Header.Payload.Signature
eyJhbG...MSJ9.подпись

Header: алгоритм (HS256)
Payload: данные (user_id, exp)
Signature: HMAC-SHA256(header + payload, secret)

Хеширование паролей

from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

hashed = pwd_context.hash("password123")
pwd_context.verify("password123", hashed)  # True

CORS

from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3000"],  # Flutter web
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

Docker контейнеризация

Контейнер — изолированное окружение с приложением и всеми зависимостями. В отличие от виртуальной машины, контейнер делит ядро ОС с хостом.

Dockerfile для FastAPI

FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

Docker Compose — мультиконтейнер

version: '3.8'
services:
    api:
        build: .
        ports:
            - "8000:8000"
        depends_on:
            - db
    db:
        image: postgres:15
        environment:
            POSTGRES_PASSWORD: secret

Основные команды

docker build -t myapp .        # Собрать образ
docker run -p 8000:8000 myapp  # Запустить контейнер
docker compose up -d            # Запустить мультиконтейнер
docker compose down             # Остановить
docker logs <container>         # Логи
docker exec -it <container> sh # Войти в контейнер
💡 Зачем Flutter-разработчику Docker

Бэкенд (FastAPI) в контейнере = одинаково работает везде. БД (PostgreSQL/MongoDB) в контейнере = не надо ставить локально. CI/CD: тесты в чистом окружении.

Platform Channels

Platform channels позволяют Flutter-приложению вызывать нативный код платформы (iOS/Android). Это необходимо для доступа к функциям, не имеющим Flutter-аналогов: камера, GPS, Bluetooth, датчики.

Flutter стора (Dart)

class PlatformService {
    static const platform = MethodChannel('com.example/my_channel');
    
    static Future<String> getPlatformVersion() async {
        try {
            final String version = await platform.invokeMethod('getPlatformVersion');
            return version;
        } on PlatformException catch (e) {
            return 'Failed: ${e.message}';
        }
    }
}

iOS стора (Swift)

let channel = FlutterMethodChannel(
    name: "com.example/my_channel",
    binaryMessenger: controller.binaryMessenger
)

channel.setMethodCallHandler { (call, result) in
    if call.method == "getPlatformVersion" {
        result("iOS " + UIDevice.current.systemVersion)
    } else {
        result(FlutterMethodNotImplemented)
    }
}

Android стора (Kotlin)

MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "com.example/my_channel")
    .setMethodCallHandler { call, result ->
        if (call.method == "getPlatformVersion") {
            result.success("Android ${android.os.Build.VERSION.RELEASE}")
        } else {
            result.notImplemented()
        }
    }

Типы каналов

КаналОписание
MethodChannelАсинхронные вызовы методов (самый частый)
EventChannelПоток событий от нативной стороны (как Stream)
BasicMessageChannelПростые сообщения в обе стороны
💡 Сравнение с C++ / Python

Platform Channels ≈ IPC (Inter-Process Communication) в C++. MethodChannel ≈ RPC. Сериализация ≈ как pickle в Python или protobuf в C++.

Testing

Три уровня тестирования

1. Unit-тесты

Тестируют отдельные функции, методы, классы. Не требуют запуска приложения. Аналогия: pytest в Python, Google Test в C++.

2. Widget-тесты

import 'package:flutter_test/flutter_test.dart';

testWidgets('Counter increments', (WidgetTester tester) async {
    await tester.pumpWidget(MyApp());
    expect(find.text('0'), findsOneWidget);
    await tester.tap(find.byIcon(Icons.add));
    await tester.pump();
    expect(find.text('1'), findsOneWidget);
});

3. Integration-тесты

# Запуск интеграционных тестов
flutter drive --target=test_driver/main.dart

# Подключение к запущенному приложению
flutter drive --use-existing-app=http://localhost:50124

Инструменты

КомандаОписание
flutter testЗапуск unit и widget тестов
flutter driveЗапуск интеграционных тестов
flutter analyzeСтатический анализ кода

DI и тестирование

// В тестах заменяем зависимости на моки
class MockWeatherRepository extends Mock implements WeatherRepository {}

test('WeatherBloc emits loaded state', () {
    final mockRepo = MockWeatherRepository();
    when(mockRepo.getWeather(any)).thenAnswer((_) async => testWeather);
    
    final bloc = WeatherBloc(repository: mockRepo);
    bloc.add(FetchWeather('Moscow'));
    
    expectLater(bloc, emitsInOrder([
        WeatherLoading(),
        WeatherLoaded(testWeather),
    ]));
});

TensorFlow Lite

TensorFlow Lite — облегчённая версия TensorFlow для запуска ML-моделей на мобильных устройствах. Обеспечивает низкую задержку, оптимизацию для мобильного железа, работу без интернета и приватность данных.

Архитектура

TensorFlow Model (.pb/.h5)
  → TF Lite Converter
    → TensorFlow Lite Model (.tflite)
      → Mobile App (Android/iOS/Flutter)

Конвертация моделей (Python)

# Из SavedModel
converter = tf.lite.TFLiteConverter.from_saved_model('/tf_model')
tflite_model = converter.convert()

# Из Keras
converter = tf.lite.TFLiteConverter.from_keras_model(model)
tflite_model = converter.convert()

# CLI
# tflite_convert --saved_model_dir=/tf_model --output_file=/tflite_model.tflite

Интеграция с Flutter

// Зависимость
// dependencies:
//   tflite_flutter: ^0.9.0

// Загрузка и классификация
var result = await Tflite.runModelOnImage(
    path: imagePath,
    numResults: 5,
    threshold: 0.1,
);

Оптимизация моделей

  • Квантизация — уменьшение точности весов (float32 → int8)
  • Прунинг — удаление неиспользуемых связей
  • Кластеризация — группировка весов
💡 Сравнение с Python

TFLite ≈ оптимизированная версия TensorFlow для edge-устройств. Конвертация из .h5/.pb в .tflite — аналогично экспорту ONNX.

ML Kit

Firebase ML Kit — SDK для мобильных разработчиков, упрощающий интеграцию ML в приложения.

Доступные API

  • Face Detection — обнаружение лиц
  • Text Recognition — распознавание текста (OCR)
  • Barcode Scanning — сканирование штрих-кодов
  • Image Labeling — классификация изображений
  • Landmark Recognition — распознавание достопримечательностей

Два режима работы

РежимОписание
On-deviceРаботает без интернета, быстрее, меньше точность
Cloud-basedХостится на Google Cloud, выше точность

Face Detection с Flutter

final FirebaseVisionImage visionImage =
    FirebaseVisionImage.fromFile(imageFile);
final FaceDetector faceDetector = FirebaseVision.instance.faceDetector(
    FaceDetectorOptions(
        enableContours: true,
        enableClassification: true,
    )
);
List<Face> faces = await faceDetector.processImage(visionImage);

Text Recognition

final TextRecognizer textRecognizer =
    FirebaseVision.instance.textRecognizer();
RecognizedText text = await textRecognizer.processImage(visionImage);
💡 Сравнение с Python

ML Kit ≈ transformers.pipeline() в Python. Face Detection API ≈ cv2.CascadeClassifier + dlib. TFLite в Flutter ≈ tflite_runtime в Python.

Image Classification на устройстве

Два подхода

ПодходПлюсыМинусы
Cloud Vision APIВысокая точностьТребует интернет, платный
On-device TFLiteБез интернета, низкая задержка, бесплатноМеньше точность

Создание кастомной TFLite модели

Датасет → Обучение модели (Colab) → Конвертация в .tflite → Flutter App

Интеграция с Flutter

// Загрузка и классификация
var recognitions = await Tflite.runModelOnImage(
    path: imagePath,
    imageMean: 127.5,
    imageStd: 127.5,
    numResults: 5,
    threshold: 0.1,
);

setState(() {
    _results = recognitions;
});

Камера в Flutter

CameraController _controller;

void initCamera() async {
    final cameras = await availableCameras();
    _controller = CameraController(cameras[0], ResolutionPreset.medium);
    await _controller.initialize();
}

Оптимизации

  • Используйте imageMean и imageStd для нормализации входных данных
  • Уменьшите разрешение изображения перед inference
  • Используйте GPU delegate для ускорения на поддерживаемых устройствах
💡 Сравнение с Python

Inference на TFLite ≈ interpreter.invoke() в Python. Подготовка изображения ≈ np.array(image) / 255.0. Выбор между cloud и on-device ≈ выбор между REST API и локальной моделью.