Provider — Практика

Практика

Теперь, когда мы разобрались с проблемами передачи состояния через дерево виджетов и изучили, как Provider помогает их решить, пришло время применить полученные знания на практике!

Наша задача — создать небольшое приложение, используя Provider для управления состоянием.

Что нам предстоит сделать:

У нас есть готовое приложение, которое работает, но имеет серьёзные недостатки в архитектуре — состояние передаётся через множество уровней виджетов, что делает код сложным для понимания и поддержки.

Как настоящие разработчики, мы проведём рефакторинг этого кода:

Это отличная возможность попрактиковаться в одной из самых важных задач разработчика — улучшении существующего кода и его оптимизации!

Начинаем работу

Скачайте проект по этой ссылке

или создаём новый проект и копируй этот код в файл main

Файл main.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 MaterialApp(
      title: 'Character Customizer',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: const CharacterCustomizerPage(),
    );
  }
}

/// Родительский виджет, который хранит все состояние.
///
/// В этом виджете мы определяем цвета персонажа и методы для их изменения.
/// Это "единственный источник правды" для нашего приложения.
class CharacterCustomizerPage extends StatefulWidget {
  const CharacterCustomizerPage({super.key});

  @override
  State createState() =>
      _CharacterCustomizerPageState();
}

class _CharacterCustomizerPageState extends State {
  Color _headColor = Colors.yellow;
  Color _shirtColor = Colors.green;

  /// Метод для изменения цвета головы.
  /// Вызов этого метода из дочерних виджетов будет приводить к перерисовке.
  void _changeHeadColor(Color color) {
    setState(() {
      _headColor = color;
    });
  }

  /// Метод для изменения цвета одежды.
  void _changeShirtColor(Color color) {
    setState(() {
      _shirtColor = color;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Character Customizer'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text(
              'Настройте своего персонажа:',
              style: TextStyle(fontSize: 20),
            ),
            const SizedBox(height: 20),
            // 1. Виджет для отображения персонажа.
            // Мы передаем ему текущие цвета.
            CharacterView(headColor: _headColor, shirtColor: _shirtColor),
            const SizedBox(height: 30),
            // 2. "Промежуточный" виджет-обертка для контролов.
            // Мы "пробрасываем" через него методы для изменения состояния.
            // Это классический пример "prop drilling".
            ControlsWrapper(
              onHeadColorChanged: _changeHeadColor,
              onShirtColorChanged: _changeShirtColor,
            ),
          ],
        ),
      ),
    );
  }
}

/// Виджет, отвечающий за визуальное представление персонажа.
/// Он полностью зависит от данных, которые ему передали.
class CharacterView extends StatelessWidget {
  final Color headColor;
  final Color shirtColor;

  const CharacterView({
    super.key,
    required this.headColor,
    required this.shirtColor,
  });

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // Голова
        CircleAvatar(
          radius: 40,
          backgroundColor: headColor,
        ),
        // Тело
        Container(
          width: 80,
          height: 100,
          decoration: BoxDecoration(
            color: shirtColor,
            borderRadius: const BorderRadius.only(
              bottomLeft: Radius.circular(40),
              bottomRight: Radius.circular(40),
            ),
          ),
        ),
      ],
    );
  }
}

/// Виджет-обертка, который не имеет своего состояния, а лишь
/// передает полученные колбэки дальше по дереву.
class ControlsWrapper extends StatelessWidget {
  final ValueChanged onHeadColorChanged;
  final ValueChanged onShirtColorChanged;

  const ControlsWrapper({
    super.key,
    required this.onHeadColorChanged,
    required this.onShirtColorChanged,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(16.0),
      decoration: BoxDecoration(
        color: Colors.grey[200],
        borderRadius: BorderRadius.circular(10),
      ),
      child: Column(
        children: [
          // 3. Виджет для выбора цвета головы.
          // Мы передаем ему колбэк, полученный от родителя.
          ColorPicker(
            title: 'Цвет головы',
            onColorSelected: onHeadColorChanged,
          ),
          const SizedBox(height: 16),
          // 4. Виджет для выбора цвета одежды.
          // Аналогично передаем второй колбэк.
          ColorPicker(
            title: 'Цвет одежды',
            onColorSelected: onShirtColorChanged,
          ),
        ],
      ),
    );
  }
}

/// Конечный виджет в иерархии, который инициирует изменение состояния.
/// При нажатии на кнопку, он вызывает колбэк `onColorSelected`,
/// который был передан ему через все дерево виджетов.
class ColorPicker extends StatelessWidget {
  final String title;
  final ValueChanged onColorSelected;
  final List _colors = [Colors.red, Colors.green, Colors.blue, Colors.purple, Colors.orange];


  ColorPicker({
    super.key,
    required this.title,
    required this.onColorSelected,
  });

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(title, style: const TextStyle(fontWeight: FontWeight.bold)),
        const SizedBox(height: 8),
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: _colors.map((color) => GestureDetector(
            onTap: () => onColorSelected(color),
            child: Container(
              width: 40,
              height: 40,
              decoration: BoxDecoration(
                color: color,
                shape: BoxShape.circle,
                border: Border.all(color: Colors.black26, width: 2),
              ),
            ),
          )).toList(),
        ),
      ],
    );
  }
}
        
Первый скриншот