skills/flutter-dev/references/state_patterns.md

9.3 KiB

State Management Patterns

Comprehensive examples of state management approaches in Flutter.

Pattern 1: ValueNotifier (Simplest)

Best for: Simple local state with a single value

class CounterScreen extends StatefulWidget {
  const CounterScreen({super.key});

  @override
  State<CounterScreen> createState() => _CounterScreenState();
}

class _CounterScreenState extends State<CounterScreen> {
  final ValueNotifier<int> _counter = ValueNotifier<int>(0);

  @override
  void dispose() {
    _counter.dispose();
    super.dispose();
  }

  void _increment() {
    _counter.value++;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: ValueListenableBuilder<int>(
          valueListenable: _counter,
          builder: (context, value, child) {
            return Text('Count: $value');
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _increment,
        child: const Icon(Icons.add),
      ),
    );
  }
}

Pattern 2: ChangeNotifier (Intermediate)

Best for: Complex state or shared state across widgets

// Model
class CounterModel extends ChangeNotifier {
  int _count = 0;
  
  int get count => _count;
  
  void increment() {
    _count++;
    notifyListeners();
  }
  
  void decrement() {
    _count--;
    notifyListeners();
  }
  
  void reset() {
    _count = 0;
    notifyListeners();
  }
}

// Usage
class CounterScreen extends StatefulWidget {
  const CounterScreen({super.key});

  @override
  State<CounterScreen> createState() => _CounterScreenState();
}

class _CounterScreenState extends State<CounterScreen> {
  final CounterModel _model = CounterModel();

  @override
  void dispose() {
    _model.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: ListenableBuilder(
          listenable: _model,
          builder: (context, child) {
            return Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text('Count: ${_model.count}'),
                Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    ElevatedButton(
                      onPressed: _model.decrement,
                      child: const Icon(Icons.remove),
                    ),
                    const SizedBox(width: 16),
                    ElevatedButton(
                      onPressed: _model.increment,
                      child: const Icon(Icons.add),
                    ),
                  ],
                ),
              ],
            );
          },
        ),
      ),
    );
  }
}

Pattern 3: Streams + StreamBuilder

Best for: Sequences of asynchronous events

class TimerModel {
  final _controller = StreamController<int>();
  Stream<int> get stream => _controller.stream;
  
  int _count = 0;
  Timer? _timer;
  
  void start() {
    _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
      _count++;
      _controller.add(_count);
    });
  }
  
  void stop() {
    _timer?.cancel();
  }
  
  void dispose() {
    _timer?.cancel();
    _controller.close();
  }
}

class TimerScreen extends StatefulWidget {
  const TimerScreen({super.key});

  @override
  State<TimerScreen> createState() => _TimerScreenState();
}

class _TimerScreenState extends State<TimerScreen> {
  final TimerModel _model = TimerModel();

  @override
  void dispose() {
    _model.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: StreamBuilder<int>(
          stream: _model.stream,
          initialData: 0,
          builder: (context, snapshot) {
            return Text('Time: ${snapshot.data}s');
          },
        ),
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            onPressed: _model.start,
            child: const Icon(Icons.play_arrow),
          ),
          const SizedBox(height: 8),
          FloatingActionButton(
            onPressed: _model.stop,
            child: const Icon(Icons.stop),
          ),
        ],
      ),
    );
  }
}

Pattern 4: MVVM (Advanced)

Best for: Complex applications with business logic separation

// Model
class User {
  final String id;
  final String name;
  final String email;
  
  const User({
    required this.id,
    required this.name,
    required this.email,
  });
}

// ViewModel
class UserViewModel extends ChangeNotifier {
  final UserRepository _repository;
  
  UserViewModel(this._repository);
  
  User? _user;
  bool _isLoading = false;
  String? _error;
  
  User? get user => _user;
  bool get isLoading => _isLoading;
  String? get error => _error;
  
  Future<void> loadUser(String userId) async {
    _isLoading = true;
    _error = null;
    notifyListeners();
    
    try {
      _user = await _repository.getUser(userId);
    } catch (e) {
      _error = e.toString();
    } finally {
      _isLoading = false;
      notifyListeners();
    }
  }
  
  Future<void> updateUser(User user) async {
    _isLoading = true;
    notifyListeners();
    
    try {
      await _repository.updateUser(user);
      _user = user;
    } catch (e) {
      _error = e.toString();
    } finally {
      _isLoading = false;
      notifyListeners();
    }
  }
}

// View
class UserScreen extends StatefulWidget {
  final String userId;
  
  const UserScreen({super.key, required this.userId});

  @override
  State<UserScreen> createState() => _UserScreenState();
}

class _UserScreenState extends State<UserScreen> {
  late final UserViewModel _viewModel;

  @override
  void initState() {
    super.initState();
    _viewModel = UserViewModel(UserRepository());
    _viewModel.loadUser(widget.userId);
  }

  @override
  void dispose() {
    _viewModel.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('User Profile')),
      body: ListenableBuilder(
        listenable: _viewModel,
        builder: (context, child) {
          if (_viewModel.isLoading) {
            return const Center(child: CircularProgressIndicator());
          }
          
          if (_viewModel.error != null) {
            return Center(child: Text('Error: ${_viewModel.error}'));
          }
          
          final user = _viewModel.user;
          if (user == null) {
            return const Center(child: Text('No user found'));
          }
          
          return Padding(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text('Name: ${user.name}'),
                const SizedBox(height: 8),
                Text('Email: ${user.email}'),
              ],
            ),
          );
        },
      ),
    );
  }
}

Pattern 5: Manual Dependency Injection

Best for: Making dependencies explicit and testable

// Service
class AuthService {
  Future<bool> login(String email, String password) async {
    // Implementation
    return true;
  }
  
  bool isLoggedIn() {
    // Implementation
    return false;
  }
}

// Screen with dependency injection
class LoginScreen extends StatefulWidget {
  final AuthService authService;
  
  const LoginScreen({
    super.key,
    required this.authService,
  });

  @override
  State<LoginScreen> createState() => _LoginScreenState();
}

class _LoginScreenState extends State<LoginScreen> {
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  bool _isLoading = false;

  @override
  void dispose() {
    _emailController.dispose();
    _passwordController.dispose();
    super.dispose();
  }

  Future<void> _handleLogin() async {
    setState(() => _isLoading = true);
    
    try {
      final success = await widget.authService.login(
        _emailController.text,
        _passwordController.text,
      );
      
      if (success && mounted) {
        // Navigate to home
      }
    } finally {
      if (mounted) {
        setState(() => _isLoading = false);
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    // Build UI
    return Scaffold(
      body: Center(
        child: _isLoading
            ? const CircularProgressIndicator()
            : ElevatedButton(
                onPressed: _handleLogin,
                child: const Text('Login'),
              ),
      ),
    );
  }
}

// Usage
class MyApp extends StatelessWidget {
  final AuthService authService = AuthService();
  
  MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: LoginScreen(authService: authService),
    );
  }
}

State Management Decision Tree

  1. Single value, local to widget? → Use ValueNotifier
  2. Multiple values or shared across widgets? → Use ChangeNotifier
  3. Handling async event sequences? → Use Stream + StreamBuilder
  4. Single async operation? → Use Future + FutureBuilder
  5. Complex app with business logic separation? → Use MVVM pattern
  6. Need explicit dependencies for testing? → Use manual DI with constructor injection
  7. User explicitly requests Provider/Riverpod/Bloc? → Use requested solution