Установка 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 (для эмулятора и SDK). Скачать: developer.android.com/studio. После установки: Tools → SDK Manager → установить Android SDK 34+.
Установите расширение 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 всегда запускайте flutter pub get.
Добавить пакет: flutter pub add dio (автоматически обновит pubspec и скачает).
Минимальный каркас приложения
Каждое Flutter-приложение следует паттерну: main() → runApp() → MaterialApp → Scaffold.
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— плавающая кнопка
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: 'Избранные')]),
),
)
Scaffold ≈ QMainWindow в Qt.
AppBar ≈ QToolBar + 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),
)
| Свойство | Тип | Описание |
|---|---|---|
fontSize | double | Размер шрифта |
fontWeight | FontWeight | Толщина: w100..w900, bold |
color | Color | Цвет текста |
fontStyle | FontStyle | italic / normal |
letterSpacing | double | Межбуквенное расстояние |
height | double | Высота строки (множитель) |
decoration | TextDecoration | underline, 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 (горизонтальная компоновка)
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|
Row: главная ось = горизонталь (→), поперечная = вертикаль (↓).
Column: главная ось = вертикаль (↓), поперечная = горизонталь (→).
mainAxisAlignment распределяет детей вдоль главной оси.
crossAxisAlignment выравнивает детей по поперечной оси.
Expanded и Flexible
Когда детей в Row/Column не хватает места, нужно явно сказать, кто как делит пространство.
Expanded и Flexible — инструменты для этого.
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
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(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 — будет ошибка «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')),
);
},
)
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(
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 — автодополнение покажет все варианты.
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('Войти'),
),
],
);
}
}
| Свойство | Описание |
|---|---|
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 — когда это явная кнопка с текстом
GestureDetector ≈ addEventListener в JavaScript.
InkWell ≈ <button> с CSS-эффектом :active.
onTap ≈ click, onLongPress ≈ mousedown + 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;
Именованные — удобно для глобальных экранов (home, settings, profile).
push() — удобно для локальных переходов с передачей сложных объектов.
В реальных проектах часто используют go_router — более мощную альтернативу.
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)),
),
);
SnackBar ≈ Toast в 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 отлично работает для локального состояния одного экрана: счётчики, тогглы, формы, индикаторы загрузки.
Для глобального состояния (авторизация, корзина, тема) → используйте 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
| FutureBuilder | StreamBuilder |
|---|---|
| Один результат | Много результатов (поток) |
| Загрузка данных один раз | Реалтайм обновления |
| HTTP GET, SharedPreferences | WebSocket, Firestore, таймеры |
| await future | await for (value in stream) |
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(); // назад
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++, но с проверкой диапазона |
double | IEEE 754, 64 бита | Аналог double в C++ |
num | Супертип для int и double | Нет прямого аналога |
String | UTF-16, неизменяемый (Immutable) | Как std::string в C++, но immutable |
bool | true / 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
}
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» означает, что компилятор на 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++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
| Режим | Компиляция | Назначение |
|---|---|---|
| Debug | JIT | Hot Reload, быстрая итерация |
| Release iOS | AOT | Production, native ARM код |
| Release Android | AOT / CoreJIT | Production |
Snapshot-ы
В debug-режиме Flutter формирует snapshots — снимки состояния, которые переиспользуются при Hot Reload:
- Script snapshot — снимок скомпилированного кода
- App snapshot — снимок состояния приложения
- Full snapshot — полный снимок с библиотеками
- Kernel Binary (
.dill) — промежуточное представление кода
AOT компилирует Dart в эффективный native код → Flutter быстрый. JIT позволяет Hot Reload → быстрая итерация. Это уникальная комбинация, которой нет у JavaScript (только JIT) или C++ (только AOT).
Как 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);
| Python | Dart |
|---|---|
threading.Thread | Нет аналога (нет threads) |
multiprocessing.Process | Isolate |
| GIL блокирует параллелизм | Нет GIL, каждый isolate = отдельный поток |
Queue для обмена | SendPort / ReceivePort |
pickle для сериализации | Сообщения должны быть сериализуемыми |
Тяжёлые вычисления (парсинг, шифрование, ML inference), обработка изображений/видео — всё, что не должно блокировать UI thread (main isolate).
Futures и async/await
Future<T> в Dart = Promise в JS = Task в C++ = Future в Python. Это обещание результата, который будет доступен в будущем.
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, // отмена при ошибке
);
| Концепция | Dart | Python |
|---|---|---|
| Одно значение | Future<T> | asyncio.Future |
| Несколько значений | Stream<T> | asyncio.Queue / async generator |
| Ожидание | await | await |
| Создание потока | StreamController | yield в async def |
| Event loop | Встроен | asyncio.run() |
Generics
Generics обеспечивают типобезопасность на этапе компиляции. В Dart 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++ 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 — 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 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
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: 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? → ОШИБКА КОМПИЛЯЦИИ!
};
}
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: 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);
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(),
);
}
}
Создаёт корневой 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() помечает 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 # Добавить пакет
Фиксирует точные версии всех транзитивных зависимостей. Коммитить в git.
Три дерева: 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детям, дети возвращают размеры
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.
Rendering Pipeline (Skia)
Flutter не использует нативные компоненты платформы. Всё рисуется через Skia (C++ библиотека). Это даёт консистентный вид на всех платформах, нативную производительность (AOT компиляция) и полный контроль над отрисовкой.
Процесс рендеринга:
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 компиляцию — обновляется только изменённый код.
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() | При удалении элемента из дерева |
Clean: виджет стабилен, build не нужен. Dirty: состояние изменилось, нужен вызов build(). State может пережить пересоздание StatefulWidget — это оптимизация. Если StatefulWidget удалён из дерева и вставлен обратно — создаётся новый объект State.
Element Lifecycle
createElement() → mount() → update() → deactivate() → unmount()
- Created — Element создан через
widget.createElement() - Active — после
mount(), элемент в дереве - Updated —
update()при изменении состояния - 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
}
}
}
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.
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), ...)
ValueKey ≈ хеш-ключ в std::unordered_map. GlobalKey ≈ глобальный идентификатор (UUID). UniqueKey ≈ id(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();
},
)
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
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();
},
)
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.
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() критичен для производительности. Возвращайте true только когда данные действительно изменились. Порядок отрисовки: painter → child → foregroundPainter.
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: ...),
],
)
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)],
),
)
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);
},
));
http / dio → requests / 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);
}
fromJson() factory → @dataclass / Pydantic. json_serializable → dataclasses-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();
}
Валидация формы: 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: Blueprint('users', __name__) + app.register_blueprint(). FastAPI: APIRouter() + app.include_router() — аналогично, но async.
Pydantic валидация
Pydantic — библиотека валидации данных, основанная на Python type hints.
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():
...
Переиспользование логики (подключение к БД, аутентификация). Тестируемость (легко подменить зависимости). Чистый код (убираем 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 # Войти в контейнер
Бэкенд (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 | Простые сообщения в обе стороны |
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)
- Прунинг — удаление неиспользуемых связей
- Кластеризация — группировка весов
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);
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 для ускорения на поддерживаемых устройствах
Inference на TFLite ≈ interpreter.invoke() в Python. Подготовка изображения ≈ np.array(image) / 255.0. Выбор между cloud и on-device ≈ выбор между REST API и локальной моделью.