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

Как создать кастомный виджет во Flutter?

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

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

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

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

Создание кастомного виджета во Flutter

Основные типы виджетов

В Flutter есть два основных типа виджетов для создания собственных компонентов:

  1. StatelessWidget — для виджетов без состояния
  2. StatefulWidget — для виджетов с состоянием

1. StatelessWidget — Простой кастомный виджет

Используется для статических, не меняющихся UI элементов.

import 'package:flutter/material.dart';

// ✅ ПРАВИЛЬНО
class CustomButton extends StatelessWidget {
  final String label;
  final VoidCallback onPressed;
  final Color? backgroundColor;
  final Color? textColor;
  
  const CustomButton({
    Key? key,
    required this.label,
    required this.onPressed,
    this.backgroundColor,
    this.textColor,
  }) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    return Material(
      child: InkWell(
        onTap: onPressed,
        child: Container(
          padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
          decoration: BoxDecoration(
            color: backgroundColor ?? Colors.blue,
            borderRadius: BorderRadius.circular(8),
          ),
          child: Text(
            label,
            style: TextStyle(
              color: textColor ?? Colors.white,
              fontSize: 16,
              fontWeight: FontWeight.bold,
            ),
          ),
        ),
      ),
    );
  }
}

2. StatefulWidget — Виджет с состоянием

Используется когда виджет должен изменять свой UI в зависимости от состояния.

class CounterWidget extends StatefulWidget {
  final int initialValue;
  final Function(int) onCountChanged;
  
  const CounterWidget({
    Key? key,
    this.initialValue = 0,
    required this.onCountChanged,
  }) : super(key: key);
  
  @override
  State<CounterWidget> createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {
  late int _count;
  
  @override
  void initState() {
    super.initState();
    _count = widget.initialValue;
  }
  
  void _increment() {
    setState(() {
      _count++;
      widget.onCountChanged(_count);
    });
  }
  
  void _decrement() {
    setState(() {
      _count--;
      widget.onCountChanged(_count);
    });
  }
  
  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(16),
      decoration: BoxDecoration(
        border: Border.all(color: Colors.grey),
        borderRadius: BorderRadius.circular(8),
      ),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Text(
            'Counter: $_count',
            style: Theme.of(context).textTheme.headlineMedium,
          ),
          SizedBox(height: 16),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              ElevatedButton(
                onPressed: _decrement,
                child: Text('-'),
              ),
              ElevatedButton(
                onPressed: _increment,
                child: Text('+'),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

3. Кастомный PainterWidget (для рисования)

Для создания сложных визуальных эффектов используй CustomPainter.

class CircleProgressPainter extends CustomPainter {
  final double progress; // 0.0 - 1.0
  final Color color;
  
  CircleProgressPainter({
    required this.progress,
    this.color = Colors.blue,
  });
  
  @override
  void paint(Canvas canvas, Size size) {
    final center = Offset(size.width / 2, size.height / 2);
    final radius = size.width / 2;
    
    // Фон круга
    final bgPaint = Paint()
      ..color = Colors.grey[300]!
      ..style = PaintingStyle.stroke
      ..strokeWidth = 8;
    
    canvas.drawCircle(center, radius, bgPaint);
    
    // Прогресс
    final progressPaint = Paint()
      ..color = color
      ..style = PaintingStyle.stroke
      ..strokeWidth = 8
      ..strokeCap = StrokeCap.round;
    
    final sweepAngle = (progress * 360 * pi) / 180;
    
    canvas.drawArc(
      Rect.fromCircle(center: center, radius: radius),
      -pi / 2,
      sweepAngle,
      false,
      progressPaint,
    );
    
    // Текст прогресса в центре
    final textPainter = TextPainter(
      text: TextSpan(
        text: '${(progress * 100).toInt()}%',
        style: TextStyle(
          color: Colors.black,
          fontSize: 24,
          fontWeight: FontWeight.bold,
        ),
      ),
      textDirection: TextDirection.ltr,
    );
    
    textPainter.layout();
    textPainter.paint(
      canvas,
      center - Offset(textPainter.width / 2, textPainter.height / 2),
    );
  }
  
  @override
  bool shouldRepaint(CircleProgressPainter oldDelegate) {
    return oldDelegate.progress != progress || oldDelegate.color != color;
  }
}

4. Шаблон лучшей практики

import 'package:flutter/material.dart';

// ✅ Правильная структура кастомного виджета
@immutable
class CustomComponent extends StatelessWidget {
  final String text;
  final VoidCallback? onPressed;
  final Color? backgroundColor;
  
  const CustomComponent({
    Key? key,
    required this.text,
    this.onPressed,
    this.backgroundColor,
  }) : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: onPressed,
      child: Container(
        padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
        decoration: BoxDecoration(
          color: backgroundColor ?? Colors.blue,
          borderRadius: BorderRadius.circular(8),
        ),
        child: Text(
          text,
          style: Theme.of(context).textTheme.labelLarge?.copyWith(
            color: Colors.white,
          ),
        ),
      ),
    );
  }
}

Ключевые моменты

StatelessWidget vs StatefulWidget:

  • StatelessWidget: простой, неизменяемый, быстрый
  • StatefulWidget: требует больше памяти, но нужен для интерактивности

Правила при создании:

  • Используй const конструктор
  • Все параметры делай final
  • Предоставляй разумные дефолтные значения
  • Документируй параметры
  • Для StatefulWidget — правильно управляй ресурсами
  • Используй @immutable аннотацию

CustomPainter для сложной графики:

  • Переопредели методы paint() и shouldRepaint()
  • Используй Canvas API для рисования
  • Оптимизируй shouldRepaint() для производительности

Заключение

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

Как создать кастомный виджет во Flutter? | PrepBro