← Назад к вопросам

Для чего нужен Safe Area?

1.0 Junior🔥 171 комментариев
#Flutter виджеты

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

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:

  1. Для кастомных layout'ов без AppBar
  2. При использовании Stack'ов с фоном на весь экран
  3. Для FloatingActionButton и других позиционированных элементов
  4. При работе с жестами в краевых областях
float Button(
  onPressed: () {},
  child: const Icon(Icons.add),
)
// FloatingActionButton автоматически избегает notch

SafeArea(
  child: GestureDetector(
    onTap: () {},
    child: Container(),
  ),
)
// Гестура работает корректно в safe zones

❌ НЕ НУЖЕН SafeArea:

  1. Когда используешь AppBar (он сам управляет padding'ом)
  2. Для элементов, которые должны быть под статус-баром
  3. Когда 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!

Для чего нужен Safe Area? | PrepBro