ข้ามไปยังเนื้อหา

Shared State ด้วย Provider

React แก้ปัญหา shared state ด้วย Context API หรือ Redux — คุณสร้าง context, ครอบ tree ด้วย provider และ consume จาก descendant ใดก็ได้ Provider package ของ Flutter ทำงานแบบเดียวกัน: คุณกำหนด class ChangeNotifier (state ของคุณ), ครอบ tree ด้วย ChangeNotifierProvider และอ่านจาก descendant ใดก็ได้ผ่าน context.watch หรือ context.read ถ้าคุณเคยใช้ React Context กับ useReducer มาก่อน Provider จะรู้สึกคุ้นเคยทันที

React
// React: Context API + useReducer
const CounterContext = createContext(null);
function counterReducer(state, action) {
switch (action.type) {
case 'increment': return { count: state.count + 1 };
default: return state;
}
}
export function CounterProvider({ children }) {
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
return (
<CounterContext.Provider value={{ state, dispatch }}>
{children}
</CounterContext.Provider>
);
}
// Consuming component
function CounterDisplay() {
const { state } = useContext(CounterContext);
return <p>Count: {state.count}</p>;
}
function IncrementButton() {
const { dispatch } = useContext(CounterContext);
return (
<button onClick={() => dispatch({ type: 'increment' })}>
Increment
</button>
);
}
Flutter
// Flutter: Provider + ChangeNotifier
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners(); // เทียบเท่ากับ dispatch / setState
}
}
// ครอบ tree — เหมือนกับ Context.Provider
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => CounterModel(),
child: const MyApp(),
),
);
}
// Consuming widgets
class CounterDisplay extends StatelessWidget {
@override
Widget build(BuildContext context) {
// context.watch rebuild widget นี้เมื่อ notifyListeners() ถูกเรียก
final count = context.watch<CounterModel>().count;
return Text('Count: ${count}');
}
}
class IncrementButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
// context.read ไม่ subscribe — ปลอดภัยสำหรับ callbacks
return ElevatedButton(
onPressed: () => context.read<CounterModel>().increment(),
child: const Text('Increment'),
);
}
}
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) => CounterModel(),
      child: const MyApp(),
    ),
  );
}

class CounterModel extends ChangeNotifier {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: Scaffold(
        body: Center(
          child: CounterPage(),
        ),
      ),
    );
  }
}

class CounterPage extends StatelessWidget {
  const CounterPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: const [
        CounterDisplay(),
        SizedBox(height: 24),
        IncrementButton(),
      ],
    );
  }
}

class CounterDisplay extends StatelessWidget {
  const CounterDisplay({super.key});

  @override
  Widget build(BuildContext context) {
    final counter = context.watch<CounterModel>();
    return Card(
      elevation: 4,
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
      child: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 48, vertical: 32),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            const Text(
              'Count',
              style: TextStyle(fontSize: 16, color: Colors.grey),
            ),
            const SizedBox(height: 8),
            Text(
              '${counter.count}',
              style: const TextStyle(
                fontSize: 64,
                fontWeight: FontWeight.bold,
                color: Colors.indigo,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class IncrementButton extends StatelessWidget {
  const IncrementButton({super.key});

  @override
  Widget build(BuildContext context) {
    return ElevatedButton.icon(
      style: ElevatedButton.styleFrom(
        padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
        backgroundColor: Colors.indigo,
        foregroundColor: Colors.white,
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
      ),
      onPressed: () => context.read<CounterModel>().increment(),
      icon: const Icon(Icons.add),
      label: const Text('Increment', style: TextStyle(fontSize: 16)),
    );
  }
}

Riverpod และ Bloc เป็น alternatives ยอดนิยมที่มี features มากกว่า — Provider เป็นจุดเริ่มต้นที่แนะนำ

notifyListeners() ใน ChangeNotifier เทียบเท่ากับอะไรใน React?
ความแตกต่างระหว่าง context.watch() และ context.read() คืออะไร?
เราต้องเรียก notifyListeners() เมื่อใดใน ChangeNotifier?