Flutter 3.x: кроссплатформенная разработка
Flutter — фреймворк от Google для кроссплатформенной разработки с нативной производительностью. Версия 3.x приносит Material 3, улучшения производительности и расширенную поддержку платформ. Из одного кода вы можете собрать приложение для iOS, Android, Web, macOS, Windows и Linux.
Установка
Flutter SDK устанавливается через пакетный менеджер или скачивается с официального сайта. После установки проверьте окружение командой doctor.
# macOS
brew install flutter
# Проверка
flutter doctor
Команда doctor проверит наличие всех зависимостей: Android SDK, Xcode, Chrome.
Основы
В Flutter всё есть виджет — от кнопок до приложения целиком. Виджеты бывают stateless (без состояния) и stateful (с состоянием).
Widgets
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: 'Flutter Demo',
home: const HomePage(),
);
}
}
Stateful vs Stateless
// Stateless - без состояния
class StatelessWidget extends StatelessWidget {
const StatelessWidget({super.key});
@override
Widget build(BuildContext context) {
return const Text('Static');
}
}
// Stateful - с состоянием
class StatefulWidget extends StatefulWidget {
const StatefulWidget({super.key});
@override
State<StatefulWidget> createState() => _StatefulWidgetState();
}
class _StatefulWidgetState extends State<StatefulWidget> {
int _counter = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('$_counter'),
ElevatedButton(
onPressed: () => setState(() => _counter++),
child: const Text('Increment'),
),
],
);
}
}
Layout
Flutter предоставляет несколько виджетов для компоновки элементов на экране:
// Column - вертикально
Column(
children: [
Text('Item 1'),
Text('Item 2'),
],
)
// Row - горизонтально
Row(
children: [
Icon(Icons.star),
Text('Text'),
],
)
// Stack - наложение
Stack(
children: [
Container(color: Colors.blue),
const Text('Overlay'),
],
)
// List
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) => ListTile(
title: Text(items[index]),
),
)
Navigation
// Переход на новый экран
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const DetailsScreen()),
);
// С именем
Navigator.pushNamed(context, '/details');
State Management
Provider
// ChangeNotifier
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
// Provider
ChangeNotifierProvider(
create: (_) => CounterModel(),
child: MyApp(),
)
// Consumer
Consumer<CounterModel>(
builder: (context, model, _) => Text('${model.count}'),
)
Riverpod
import 'package:flutter_riverpod/flutter_riverpod.dart';
final counterProvider = StateProvider<int>((ref) => 0);
class MyWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Text('$count');
}
}
HTTP
import 'package:http/http.dart' as http;
final response = await http.get(Uri.parse('https://api.example.com/data'));
final data = jsonDecode(response.body);
Material 3
Темы:
MaterialApp(
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
brightness: Brightness.light,
),
),
darkTheme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
brightness: Brightness.dark,
),
),
themeMode: ThemeMode.system,
)
Новые виджеты:
NavigationBar— замена BottomNavigationBarNavigationRail— для планшетовFilledButton— акцентные кнопкиSegmentedButton— выбор из нескольких опцийDatePickerDialog— обновлённый диалог дат
Адаптивный UI
LayoutBuilder:
LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth < 600) {
return MobileLayout();
} else if (constraints.maxWidth < 1200) {
return TabletLayout();
} else {
return DesktopLayout();
}
},
)
MediaQuery:
final size = MediaQuery.of(context).size;
final orientation = MediaQuery.of(context).orientation;
if (size.width > 1200) {
// Desktop
} else if (orientation == Orientation.landscape) {
// Tablet landscape
} else {
// Mobile
}
Анимации
Implicit animations:
AnimatedContainer(
duration: const Duration(milliseconds: 300),
width: _expanded ? 200 : 100,
height: _expanded ? 200 : 100,
color: _expanded ? Colors.blue : Colors.red,
)
AnimatedOpacity(
opacity: _visible ? 1.0 : 0.0,
duration: const Duration(milliseconds: 500),
child: Text('Fade in/out'),
)
Explicit animations:
class _AnimatedWidgetState extends State<AnimatedWidget>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
);
}
@override
Widget build(BuildContext context) {
return ScaleTransition(
scale: _animation,
child: const Text('Animated'),
);
}
}
Работа с API
Dio:
import 'package:dio/dio.dart';
final dio = Dio(BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: const Duration(seconds: 5),
receiveTimeout: const Duration(seconds: 3),
));
// Interceptors
dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
// Add auth token
options.headers['Authorization'] = 'Bearer $token';
return handler.next(options);
},
onError: (error, handler) {
if (error.response?.statusCode == 401) {
// Refresh token
}
return handler.next(error);
},
));
// Использование
final response = await dio.get('/users');
final users = response.data;
Локальное хранилище
Hive:
import 'package:hive/hive.dart';
// Инициализация
await Hive.initFlutter();
Hive.openBox('mybox');
// Запись
var box = Hive.box('mybox');
box.put('name', 'John');
box.put('age', 30);
// Чтение
var name = box.get('name');
var age = box.get('age');
// Типобезопасность
@HiveType(typeId: 0)
class User extends HiveObject {
@HiveField(0)
String name;
@HiveField(1)
int age;
}
Тестирование
Unit тесты:
import 'package:flutter_test/flutter_test.dart';
void main() {
test('Counter increments', () {
final counter = Counter();
expect(counter.value, 0);
counter.increment();
expect(counter.value, 1);
});
}
Widget тесты:
testWidgets('Counter increments smoke test', (tester) async {
await tester.pumpWidget(const MyApp());
expect(find.text('0'), findsOneWidget);
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
expect(find.text('1'), findsOneWidget);
});
Integration тесты:
import 'package:integration_test/integration_test.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('Full app test', (tester) async {
await tester.pumpWidget(MyApp());
// Тест на реальном устройстве
});
}
Производительность
DevTools:
- Widget inspector — анализ дерева виджетов
- Performance — профилирование FPS
- Memory — поиск утечек памяти
- Network — мониторинг запросов
Оптимизация:
constконструкторы для неизменяемых виджетовRepaintBoundaryдля изоляции перерисовкиListView.builderдля длинных списков- Lazy loading изображений
Публикация
# Версионирование
# pubspec.yaml: version: 1.0.0+1
# iOS
flutter build ipa --release
# Android
flutter build appbundle --release
# Web
flutter build web --release
# macOS
flutter build macos --release
# Windows
flutter build windows --release
# Linux
flutter build linux --release
Популярные пакеты
- provider / riverpod — state management
- dio — HTTP клиент
- hive / isar — локальная БД
- go_router — роутинг
- flutter_bloc — BLoC паттерн
- freezed — code generation
- json_serializable — JSON сериализация