Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
SafeArea: Защита UI от системных элементов
SafeArea — это важнейший виджет в Flutter, который гарантирует, что содержимое вашего приложения не будет перекрыто системными элементами, такими как статус-бар, навигационный бар, выемка (notch) на экране и gesture-ограничения.
Зачем нужен SafeArea
Разные устройства имеют разные "опасные зоны" на экране:
iOS:
- Статус-бар в верхней части (время, сигнал, батарея)
- Home indicator внизу (свайп для возврата на главный экран)
- Notch на некоторых моделях (iPhone X, 11, 12, 13 и т.д.)
- Dynamic Island на iPhone 14+
Android:
- Статус-бар вверху
- Навигационный бар внизу (на некоторых устройствах)
- Notch на некоторых флагманах
Без SafeArea содержимое приложения может:
- Быть скрыто под статус-баром
- Перекрываться кнопками навигации
- Быть недоступным для взаимодействия в опасных зонах
- Выглядеть некорректно на разных экранах
Как работает SafeArea
// SafeArea добавляет padding вокруг содержимого
// чтобы избежать системных элементов
Scaffold(
body: SafeArea(
child: ListView(
children: [
Text('Это содержимое будет ниже статус-бара'),
// ...
],
),
),
)
// Эквивалентно:
Scaffold(
body: Padding(
padding: EdgeInsets.only(
top: mediaQuery.padding.top, // Статус-бар
bottom: mediaQuery.padding.bottom, // Навигационный бар
left: mediaQuery.padding.left,
right: mediaQuery.padding.right,
),
child: ListView(
children: [
Text('Это содержимое будет ниже статус-бара'),
],
),
),
)
Базовое использование
Пример 1: Простое использование
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Safe Area Demo')),
body: SafeArea(
child: Column(
children: [
const Text('Это внутри SafeArea'),
const Text('Не будет скрыто под статус-баром'),
],
),
),
),
);
}
}
Сейчас AppBar сам управляет padding'ом, но для custom layout нужен SafeArea.
Пример 2: Custom layout без AppBar
class CustomScreenWithoutAppBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
children: [
// Header кастомный
Container(
color: Colors.blue,
padding: const EdgeInsets.all(16),
child: const Text(
'Custom Header',
style: TextStyle(color: Colors.white, fontSize: 20),
),
),
// Контент
Expanded(
child: ListView.builder(
itemCount: 100,
itemBuilder: (context, index) {
return ListTile(
title: Text('Item $index'),
);
},
),
),
],
),
),
);
}
}
// БЕЗ SafeArea:
// - Header может быть скрыт под статус-баром
// - На некоторых устройствах контент будет перекрыт
// С SafeArea:
// - Header красиво размещен ниже статус-бара
// - Контент доступен полностью
Детальное управление SafeArea
Ты можешь контролировать, какие области должны быть защищены:
SafeArea(
// Защищать ли от статус-бара вверху
top: true,
// Защищать ли от навигационного бара внизу
bottom: true,
// Защищать ли от боков (notch, бортовые панели)
left: true,
right: true,
// Минимальный padding (если система не требует больше)
minimum: const EdgeInsets.all(0),
child: Column(
children: [
Text('Safe content'),
],
),
)
Пример: Фон во всю высоту, контент в SafeArea
class BackgroundFullScreenWithSafeArea extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
// Фон на весь экран (включая notch и статус-бар)
Container(
color: Colors.purple,
),
// Контент в SafeArea
SafeArea(
child: Container(
padding: const EdgeInsets.all(16),
child: Column(
children: [
const Text(
'Safe Content',
style: TextStyle(
color: Colors.white,
fontSize: 24,
),
),
],
),
),
),
],
),
);
}
}
Пример: Отключить защиту для одной стороны
class PartialSafeArea extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
// Защищать только сверху и снизу, игнорировать боки
left: false,
right: false,
child: Column(
children: [
Container(
color: Colors.blue,
height: 200,
// Этот контейнер будет от края до края (включая bokovye notch)
),
Expanded(
child: Container(
color: Colors.white,
// Это тоже от края до края
),
),
],
),
),
);
}
}
Практические примеры
Пример 1: Chat приложение
class ChatScreen extends StatefulWidget {
@override
State<ChatScreen> createState() => _ChatScreenState();
}
class _ChatScreenState extends State<ChatScreen> {
late TextEditingController _controller;
List<String> messages = [];
@override
void initState() {
super.initState();
_controller = TextEditingController();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
children: [
// Header
Container(
padding: const EdgeInsets.all(16),
color: Colors.blue,
child: const Text(
'Chat',
style: TextStyle(
color: Colors.white,
fontSize: 18,
),
),
),
// Список сообщений
Expanded(
child: ListView.builder(
itemCount: messages.length,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Align(
alignment: index % 2 == 0
? Alignment.centerLeft
: Alignment.centerRight,
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: index % 2 == 0 ? Colors.grey : Colors.blue,
borderRadius: BorderRadius.circular(12),
),
child: Text(messages[index]),
),
),
);
},
),
),
// Input field в SafeArea (выше home indicator на iPhone)
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: InputDecoration(
hintText: 'Type message...',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(24),
),
),
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () {
if (_controller.text.isNotEmpty) {
setState(() {
messages.add(_controller.text);
_controller.clear();
});
}
},
child: const Text('Send'),
),
],
),
),
],
),
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
Пример 2: Dashboard без AppBar
class DashboardScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: CustomScrollView(
slivers: [
// Header
SliverAppBar(
expandedHeight: 200,
pinned: false,
flexibleSpace: FlexibleSpaceBar(
background: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue.shade800, Colors.blue.shade400],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Text(
'Welcome',
style: TextStyle(
color: Colors.white,
fontSize: 28,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
),
// Контент
SliverList(
delegate: SliverChildListDelegate([
Padding(
padding: const EdgeInsets.all(16.0),
child: GridView.count(
crossAxisCount: 2,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
children: [
DashboardCard(title: 'Users', count: '1234'),
DashboardCard(title: 'Orders', count: '567'),
DashboardCard(title: 'Revenue', count: '\$12.5K'),
DashboardCard(title: 'Rating', count: '4.8'),
],
),
),
]),
),
],
),
),
);
}
}
class DashboardCard extends StatelessWidget {
final String title;
final String count;
const DashboardCard({
required this.title,
required this.count,
});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Colors.blue.shade100,
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
count,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(title),
],
),
);
}
}
Best Practices
✅ ИСПОЛЬЗУЙ SafeArea:
- Для кастомных layout'ов без AppBar
- При использовании Stack'ов с фоном на весь экран
- Для FloatingActionButton и других позиционированных элементов
- При работе с жестами в краевых областях
float Button(
onPressed: () {},
child: const Icon(Icons.add),
)
// FloatingActionButton автоматически избегает notch
SafeArea(
child: GestureDetector(
onTap: () {},
child: Container(),
),
)
// Гестура работает корректно в safe zones
❌ НЕ НУЖЕН SafeArea:
- Когда используешь AppBar (он сам управляет padding'ом)
- Для элементов, которые должны быть под статус-баром
- Когда Scaffold автоматически обрабатывает insets
MediaQuery альтернатива
Если нужен более тонкий контроль, используй MediaQuery:
final padding = MediaQuery.of(context).padding;
Padding(
padding: EdgeInsets.only(
top: padding.top,
bottom: padding.bottom,
),
child: Container(),
)
Итог
SafeArea — это:
- Гарантия, что контент не будет скрыт системными элементами
- Essential для cross-device совместимости
- Простой способ сделать приложение usable на iPhone с notch и Android с navigation bar
- Не нужен для AppBar, но обязателен для custom layout'ов
Используй SafeArea по умолчанию для любого custom layout без AppBar!