Flutter — кроссплатформенная разработка приложений

Flutter — это тулкит Google, который позволяет писать мобильное приложение один раз на языке Dart, а запускать на Android и iOS. Собственный движок отрисовки даёт быстрый, плавный интерфейс, единый на всех платформах. Подходит и для веба, и для десктопа.

Уровень: начальный 19 разделов 2–3 часа

1. Что такое Flutter

Flutter — это не язык, а фреймворк: интерфейс собирается из виджетов, а язык программирования — Dart (создан в Google, синтаксис похож на Java и JavaScript). В отличие от React Native, Flutter не использует «нативные» элементы системы — он всё рисует сам через движок Skia/Impeller.

Один код → несколько платформ: Android, iOS, а дополнительно — веб и настольные системы. Горячая перезагрузка (Hot Reload) показывает изменения за доли секунды прямо на запущенном устройстве.

// Первая программа на Dart
void main() {
  print("Привет, Flutter!");
  final sum = add(2, 3);   // 5
}

int add(int a, int b) => a + b;
Всё, что видит пользователь, — это дерево виджетов. Даже отступы и обработка касаний — виджеты. Эта идея «всё — виджет» — ключ к Flutter.

2. Установка и настройка

Скачай SDK с официального сайта flutter.dev, распакуй в удобную папку и добавь flutter/bin в переменную окружения PATH. Проверка окружения — командой flutter doctor.

# Windows
git clone https://github.com/flutter/flutter.git -b stable
set PATH=%PATH%;C:\src\flutter\bin

# Проверка окружения — она подскажет, чего не хватает
flutter doctor
flutter --version

Для Android дополнительно ставят Android Studio (для SDK и эмулятора), для iOS на Mac — Xcode. Проверка покажет недостающие компоненты в виде таблицы статусов.

flutter doctor — первая команда, которую стоит запускать после установки: она диагностирует Android Toolchain, Xcode, Chrome и лицензии SDK.
Не устанавливай Flutter в системные папки с ограниченными правами (например, Program Files). Лучшее место — корень диска: C:\flutter.

3. Первый проект и структура

Проект создаётся из шаблона, затем запускается на эмуляторе или устройстве:

flutter create my_app
cd my_app
flutter run

Ключевые папки проекта:

ПутьЧто внутри
lib/main.dartвесь код приложения (обычно пишут здесь)
pubspec.yamlзависимости и ресурсы проекта
android/обёртка платформы Android
ios/обёртка платформы iOS
test/автотесты на языке Dart
import 'package:flutter/material.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(home: HomePage());
  }
}
Файл pubspec.yaml — аналог package.json из Node.js: пакеты добавляются в секцию dependencies, после чего выполняется flutter pub get.

4. Основы языка Dart

Dart — объектно-ориентированный язык со строгой типизацией и возможностью вывода типов. Из ключевых моментов: константы final и const, неизменяемые переменные, безопасность от null.

void main() {
  var name = "Анна";        // var — тип выводится автоматически
  final city = "Москва";    // final — значение нельзя изменить
  const grav = 9.8;         // const — константа времени компиляции

  var nums = [1, 2, 3];
  var map = {'key': 'value'};

  String? maybe;            // nullable-переменная
  int length = maybe?.length ?? 0;   // ?. безопасный доступ, ?? — запасное значение
}

// Класс с конструктором
class User {
  final String name;
  User(this.name);
}
Функции — это объекты: их можно класть в переменные, передавать и возвращать. Лямбда с телом в одну строку записывается через =>.

5. Виджеты и композиция

Виджеты бывают двух видов: StatelessWidget (неизменяемый, без состояния) и StatefulWidget (хранит состояние и перерисовывается при его изменении).

class Counter extends StatefulWidget {
  @override
  State<Counter> createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text("Нажатий: $_count", style: const TextStyle(fontSize: 24)),
            ElevatedButton(
              onPressed: () => setState(() => _count++),
              child: const Text("Плюс"),
            ),
          ],
        ),
      ),
    );
  }
}

Базовые виджеты, которые нужны почти всегда:

ВиджетДля чего
Textвывод текста
Containerконтейнер с отступами, цветом, рамкой
Columnвертикальное расположение детей
Rowгоризонтальное расположение детей
ListViewпрокручиваемый список
Stackналожение виджетов слоями
Вызов setState пересобирает весь build текущего виджета. Держи тяжёлые вычисления вне build, чтобы не тормозить интерфейс.

6. Layout и расположение

Вёрстка — это вложенные контейнеры. Основные инструменты компоновки: Center (центрирование), Padding (отступы), Expanded (растягивание), SizedBox (фиксированный размер и пустой зазор).

Container(
  width: 200,
  height: 100,
  padding: const EdgeInsets.all(8),
  decoration: BoxDecoration(
    color: Colors.blue,
    borderRadius: BorderRadius.circular(8),
  ),
  child: const Center(child: Text("Контейнер")),
)

Row(
  children: [
    Expanded(child: Container(color: Colors.red, height: 50)),
    Expanded(child: Container(color: Colors.yellow, height: 50)),
  ],
)

Гибкость управляется свойствами mainAxisAlignment (главная ось) и crossAxisAlignment (поперечная ось). Для точного попиксельного контроля применяй Alignment(x, y): х и у в диапазоне от −1 до 1.

Используй SizedBox(width: 16) для пустого места между элементами — это идиоматичный способ сделать отступы без отдельных виджетов.

7. Навигация и маршруты

Переходы между экранами выполняет Navigator: стек экранов, куда можно положить новый маршрут или вытащить текущий.

// Переход на новый экран с ожиданием результата
Navigator.push(
  context,
  MaterialPageRoute(builder: (context) => const DetailPage()),
);

// Возврат назад
Navigator.pop(context);

// Маршруты, описанные заранее (подойдёт для больших приложений)
MaterialApp(
  routes: {
    '/detail': (context) => const DetailPage(),
  },
)

Именованные маршруты задаются в MaterialApp через routes, переход — Navigator.pushNamed(context, '/detail'). Экрану можно передать параметры через конструктор виджета.

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

8. Управление состоянием

Для одного экрана достаточно setState. Когда состояние нужно разделить между экранами, подключи пакет provider — самый простой из распространённых подходов.

class Cart extends ChangeNotifier {
  final List<String> _items = [];

  List<String> get items => List.unmodifiable(_items);

  void add(String item) {
    _items.add(item);
    notifyListeners();   // сообщаем подписчикам об изменении
  }
}

// Оборачиваем приложение провайдером
ChangeNotifierProvider(
  create: (context) => Cart(),
  child: const MyApp(),
)

Любой виджет читает состояние через хук:

final cart = context.watch<Cart>();
Text("В корзине: ${cart.items.length}")
Не клади в ChangeNotifier логику сети — только состояние и простые методы. Сетевые вызовы выноси в сервисы, а управление эффектами — в отдельный слой.
Когда проект вырастет, посмотри на riverpod и bloc — они дают больше гибкости и тестируемости. Начинать же с provider достаточно.

9. Работа с сетью

Зависимость http добавляется в pubspec.yaml. Запрос выполняется асинхронно, ответ разбирается через jsonDecode.

import 'dart:convert';
import 'package:http/http.dart' as http;

Future<List<String>> fetchTitles() async {
  final response = await http.get(
    Uri.parse('https://jsonplaceholder.typicode.com/posts'),
  );
  if (response.statusCode != 200) {
    throw Exception('Ошибка запроса: ${response.statusCode}');
  }
  final data = jsonDecode(response.body) as List;
  return data.map((post) => post['title'] as String).toList();
}

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

FutureBuilder<List<String>>(
  future: fetchTitles(),
  builder: (context, snapshot) {
    if (snapshot.hasData) {
      return ListView(children: [
        for (final title in snapshot.data!) Text(title),
      ]);
    }
    return const CircularProgressIndicator();
  },
)
Для продакшена используй dio — он добавляет перехватчики, таймауты, отмену запросов и работу с файлами.

10. Локальное хранение данных

Произвольные пары «ключ-значение» хранятся через пакет shared_preferences, структурированные данные — в SQLite через sqflite.

// shared_preferences: настройки и мелкие данные
final prefs = await SharedPreferences.getInstance();
await prefs.setInt('counter', 5);
await prefs.setString('token', 'abc123');

final counter = prefs.getInt('counter') ?? 0;
// sqflite: полноценная база
final db = await openDatabase('notes.db', version: 1);
await db.execute(
  'CREATE TABLE notes (id INTEGER PRIMARY KEY, text TEXT)',
);
await db.insert('notes', {'text': 'Купить продукты'});
final rows = await db.query('notes');

Изоморфный пакет hive — быстрая база-коробка без SQL: данные записываются как объекты Dart напрямую.

Секреты (токены, пароли) не храни в shared_preferences — используй flutter_secure_storage, который обращается к системным хранилищам (Keychain на iOS, Keystore на Android).

11. Сборка APK для Android

Для релиза собери продакшен-сборку:

# Обычный APK
flutter build apk --release

# Готовый файл лежит здесь
# build/app/outputs/flutter-apk/app-release.apk

# App Bundle для Google Play (рекомендуется)
flutter build appbundle --release

Debug-сборка (с горячей перезагрузкой) собирается командой flutter run. Для подписи релиза в android/app/build.gradle.kts настраивают signingConfig с файлом ключа .jks.

Перед публикацией прогони flutter analyze — статический анализатор найдёт ошибки стиля и потенциальные баги.
Не забудь увеличить версию приложения (versionCode) в pubspec.yaml перед каждой новой сборкой — иначе Play Store отклонит загрузку старой версии.

12. Тесты Flutter-приложения

Тесты бывают трёх уровней: unit-тесты логики, widget-тесты интерфейса и интеграционные на устройстве. Базовые запускаются командой flutter test.

// test/widget_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/main.dart';

void main() {
  testWidgets('счётчик работает', (tester) async {
    await tester.pumpWidget(const MyApp());
    expect(find.text('Нажатий: 0'), findsOneWidget);

    await tester.tap(find.text('Плюс'));
    await tester.pump();
    expect(find.text('Нажатий: 1'), findsOneWidget);
  });
}

tester.pump() перерисовывает дерево после события, а find.text ищет виджет по тексту. Так проверяется реальное поведение интерфейса.

Разделяй логику и представление: чистые функции тестируются юнит-тестами быстро, представление — widget-тестами. Для настроек в CI запускай и то, и другое.
В GitHub Actions типичный набор: actions/checkout, setup-flutter, flutter pub get и flutter test.

13. Анимации

Простые анимации Flutter даёт через встроенные виджеты: AnimatedContainer, AnimatedOpacity, TweenAnimationBuilder. Они плавно изменяют свойства без ручного управления кадрами:

AnimatedContainer(
  duration: const Duration(milliseconds: 300),
  curve: Curves.easeInOut,
  width: isOpen ? 200 : 60,
  height: 60,
  color: isOpen ? Colors.blue : Colors.grey,
)

Переход между экранами анимируют через Hero — виджет с одним тегом «перелетает» с одного экрана на другой:

Hero(tag: 'avatar', child: CircleAvatar(...))
Встроенных виджетов хватает для 90% случаев. Плавные контейнеры, появление, смена состояния — без единого AnimationController.
Не анимируй layout-свойства по пикселю на каждый кадр — это нагрузка на layout. Для движения используй Transform: он работает на GPU без пересборки дерева.

14. Плагины и платформенные возможности

Доступ к камере, геолокации, уведомлениям и другим функциям ОС дают pub-пакеты. Типичный набор:

ВозможностьПакет
камера и галереяimage_picker
уведомленияflutter_local_notifications
геолокацияgeolocator
http-запросыdio / http
защищённое хранениеflutter_secure_storage

Подключение: добавь пакет в pubspec.yaml и выполни flutter pub get. Пример использования:

final picked = await ImagePicker().pickImage(
  source: ImageSource.gallery,
);
Пакеты — это мост к нативному коду. Если нужного плагина нет, используй MethodChannel для вызова собственного кода Android/iOS.
Некоторые плагины требуют прав в манифестах Android и Info.plist iOS (камера, хранилище). Без них приложение упадёт на устройстве, но будет работать в эмуляторе.

15. Производительность и профилирование

Если интерфейс «тормозит», сначала измерь: профилировщик DevTools и вкладка Performance покажут, какие виджеты перестраиваются зря. Частые причины:

ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, i) => ListTile(title: Text(items[i])),
)
Профиль, а не угадывай: замерь FPS при прокрутке и посмотри во вкладке Build, какие поддеревья перестраиваются на лишних. Узлы — по факту, а не по ощущениям.
Начинай с работающей версии, потом измерения, потом точечная оптимизация. Гонка за «красивым кодом» без метрик — трата времени.

16. Окружения сборки: --dart-define и flavor

Разработка и продакшен часто требуют разных настроек: базовые URL API, ключи аналитики, имена пакетов. Flutter позволяет передавать значения на этапе запуска:

flutter run --dart-define=API_URL=https://dev.api.example.com
flutter build apk --dart-define=API_URL=https://api.example.com

// main.dart
const apiUrl = String.fromEnvironment(
  'API_URL',
  defaultValue: 'http://localhost:3000',
);

Ключи компилируются в сборку статически и не меняются после неё. Для различия типов приложения (dev/stage/prod с разными иконками и именами пакетов) используют flavor: конфигурация в build.gradle.kts, запуск — flutter run --flavor prod.

Не храни настоящие секреты через --dart-define: они попадают в клиент и видны в APK. Секреты живут только на сервере.

Проследи, чтобы dev-конфиг не ушёл в релиз: отличай окружения по значению переменной, а не по комментарию в коде. Логи с dev-эндпоинтом в проде — путь к утечке данных.

17. Локализация приложения

Двух языков достаточно почти любому приложению. Пакет flutter_localizations и файлы ARB (Archival Resource Bundle) организуют переводы:

flutter pub add flutter_localizations --sdk=flutter intl

// pubspec.yaml
flutter:
  generate: true

В app_ru.arb лежат переводы в JSON-формате: {"hello": "Привет, мир!"}, в app_en.arb — английский. Генерация классов: flutter gen-l10n, после чего строки доступны как context.l10n.hello. Язык определяется системой через MaterialApp(locale: ...).

Помимо текста переводи плюральные формы («одна заметка / две заметки / пять заметок») и форматы дат — intl с DateFormat избавляет от ручной работы с региональными стандартами.

Не склеивай строки конкатенацией: «У вас {n} заметок» в шаблонах ARB корректно склоняется, а собранные вручную фразы на других языках ломаются.
Проверяй кавычки в ARB-строках, а файлы держи в UTF-8 — иначе кириллица может сломаться на iOS.

18. Темизация: тёмная тема и адаптив

MaterialApp принимает палитру через ThemeData: один объект меняет цвета всего приложения, а виджеты берут цвета через Theme.of(context).

MaterialApp(
  theme: ThemeData(
    colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
    brightness: Brightness.light,
  ),
  darkTheme: ThemeData(
    colorScheme: ColorScheme.fromSeed(
      seedColor: Colors.blue,
      brightness: Brightness.dark,
    ),
  ),
  themeMode: ThemeMode.system,
)

ThemeMode.system подстраивается под настройки ОС: тёмная тема включается без единой строки кода. В виджетах используй не жёсткие цвета, а colorScheme.primary, surface, onSurface — тогда обе темы работают согласованно.

Проверяй контраст текста на фоне: для обычного текста WCAG AA требует минимум 4.5:1. Инструменты вроде плагина contrast_aa показывают контраст прямо в редакторе.

Жёсткие цвета в сотне виджетов делают тему неюзабельной. Любое значение цвета бери из colorScheme или выноси в ThemeExtension.

19. CI/CD, подпись и публикация

Стабильный релиз-пайплайн: анализ → тесты → сборка → подпись → доставка в стор. Типовой шаг в GitHub Actions:

- uses: subosito/flutter-action@v2
  with: { channel: stable }
- run: flutter pub get
- run: flutter analyze
- run: flutter test

Android-подпись: файл ключа .jks и пароли клади в секреты репозитория, подставляй в build.gradle.kts только на этапе релиз-сборки. iOS — сертификат .p12 и provisioning profile тоже из секретов; экспорт IPA делают через xcodebuild или Fastlane.

Fastlane автоматизирует отгрузку: lane для Play Store (fastlane supply) и App Store (fastlane deliver). После выпуска следи за метриками падений — Crashlytics или Sentry подключённые через плагин.

Сторы требуют уникальный номер версии при каждой загрузке: автоподнимай его из git-тега в pubspec.yaml, чтобы не забыть вручную.
Проверяй секреты в логах CI: логи публичных репозиториев доступны всем. Утекший ключ подписи позволяет подписать чужие APK вашим именем.

Что дальше

Освоил основы — расширь проект: работа с камерой и картой (пакеты camera, flutter_map), анимации через AnimatedContainer и Hero. Управление версиями — курс Git, интеграции с API — REST API, синхронизация с сервером — Django или Node.js. Для сравнения с другими подходами смотри Android (Kotlin), iOS (Swift) и React Native.

← Вернуться к курсам