Chương 06 · State Management — Foundation

setState & InheritedWidget

90% beginner Flutter dùng setState cho mọi thứ — đến app vừa lớn là vỡ. Chương này build mental model: state ở đâu, ai quản lý, ai consume. Nắm chương 6 thì chương 7-8 (Bloc/Provider/Riverpod) dễ hơn nhiều — vì 3 package đó đều dựa trên InheritedWidget.

Độ dài: ~1020 dòng Bài tập: 5 Quiz: 8 Prerequisites: Ch 2 + Dart Ch 7 (Stream)
🎯 Mục tiêu chương
  • Hiểu spectrum state: ephemeral (widget) vs app state (across widget).
  • Pattern "lifting state up" — khi child cần share state với sibling.
  • Sử dụng setState đúng nơi đúng chỗ.
  • Hiểu InheritedWidget — primitive cho share state trong tree.
  • Tự viết InheritedWidget custom + helper static of(context).
  • InheritedNotifier, ChangeNotifier, ValueNotifier.
  • Hiểu khi nào setState không đủ + cần package.

1. State spectrum

ScopeToolUse case
Ephemeral (1 widget)setStateCounter, toggle, hover, input controller
Lifted (parent + siblings)Pass via constructor + callbackFilter + list, form parent
Inherited (subtree)InheritedWidgetTheme, locale, user — descendant nhiều
App-wideBloc / Provider / Riverpod / GetXAuth, cart, theme, complex domain
PersistentSharedPreferences / Hive / SQLiteSetting, cache, offline data (ch.11)
🧠 Đừng over-engineer

Counter cần Bloc? Không. setState đủ. Toggle dark mode? ValueNotifier đủ — không cần Riverpod. Lift state cao khi cần, không cao hơn.

2. Pattern 1 — Local setState

Khi state chỉ một widget care:

class _ToggleState extends State<Toggle> {
  bool _on = false;

  @override
  Widget build(BuildContext context) {
    return Switch(
      value: _on,
      onChanged: (v) => setState(() => _on = v),
    );
  }
}

Dùng cho: toggle, expand/collapse, input focus, animation playing.

3. Pattern 2 — Lifting state up

2 sibling widget cần share state → move state lên parent chung:

class _FilterableListState extends State<FilterableList> {
  String _query = '';

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        FilterBar(
          query: _query,
          onChanged: (q) => setState(() => _query = q),
        ),
        Expanded(
          child: ItemList(query: _query),
        ),
      ],
    );
  }
}

// FilterBar và ItemList đều là StatelessWidget — không có state riêng.
// State 'query' ở parent. FilterBar push update qua callback. ItemList nhận query qua constructor.

Khi nào lifting fail?

Tree càng sâu → pass props qua 5-6 cấp widget chỉ vì leaf cần. Boilerplate khổng lồ. Đây là lúc cần InheritedWidget hoặc state management package.

// ❌ Pass through 5 levels — prop drilling
App(user: u) → Page(user: u) → Section(user: u) → Card(user: u) → Avatar(user: u)

4. InheritedWidget — cứu cánh

Primitive Flutter cho share state tới mọi descendant. Lookup qua context.dependOnInheritedWidgetOfExactType<T>().

class UserInherited extends InheritedWidget {
  final User user;

  const UserInherited({
    super.key,
    required this.user,
    required super.child,
  });

  // Helper static — convenient access
  static User? maybeOf(BuildContext context) {
    return context
        .dependOnInheritedWidgetOfExactType<UserInherited>()
        ?.user;
  }

  static User of(BuildContext context) {
    final u = maybeOf(context);
    assert(u != null, 'No UserInherited found in context');
    return u!;
  }

  // Khi data đổi, descendant đang dependOn có rebuild không?
  @override
  bool updateShouldNotify(UserInherited old) => user != old.user;
}

// Apply
UserInherited(
  user: User(name: 'Việt', age: 25),
  child: MyApp(),
)

// Access từ bất kỳ descendant
class UserBadge extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final user = UserInherited.of(context);
    return Text(user.name);
  }
}

updateShouldNotify

Khi UserInherited instance mới thay cũ, framework gọi updateShouldNotify(old):

  • Trả true → mọi descendant dùng UserInherited.of rebuild.
  • Trả false → skip rebuild (vì data không đổi thật sự).
🧠 InheritedWidget immutable — phải re-create để update

InheritedWidget không mutate field trực tiếp. Wrap với StatefulWidget bên ngoài:

class UserScope extends StatefulWidget {
  final Widget child;
  const UserScope({super.key, required this.child});

  @override
  State<UserScope> createState() => _UserScopeState();
}

class _UserScopeState extends State<UserScope> {
  User _user = const User(name: 'guest');

  void updateUser(User u) => setState(() => _user = u);

  @override
  Widget build(BuildContext context) => UserInherited(
    user: _user,
    child: widget.child,
  );
}

setState ngoài → tái tạo InheritedWidget → descendant rebuild.

5. InheritedNotifier<T extends Listenable>

Variant: InheritedWidget tự subscribe vào Listenable. Auto rebuild descendant khi notifier notifyListeners().

class CounterScope extends InheritedNotifier<ValueNotifier<int>> {
  const CounterScope({
    super.key,
    required ValueNotifier<int> super.notifier,
    required super.child,
  });

  static ValueNotifier<int> of(BuildContext context) {
    return context
        .dependOnInheritedWidgetOfExactType<CounterScope>()!
        .notifier!;
  }
}

// Apply
final counter = ValueNotifier(0);
CounterScope(
  notifier: counter,
  child: MyApp(),
)

// Increment từ bất kỳ widget — descendant rebuild
CounterScope.of(context).value++;

6. ValueNotifier<T> + ValueListenableBuilder

Đơn giản nhất cho single value. Widget rebuild khi value change.

final _count = ValueNotifier<int>(0);

ValueListenableBuilder<int>(
  valueListenable: _count,
  builder: (context, value, child) => Text('Count: $value'),
)

// Update từ button
ElevatedButton(
  onPressed: () => _count.value++,
  child: const Text('+1'),
)

ValueNotifier vs setState — khi nào dùng cái nào?

  • setState: state ở chính State class. Local widget.
  • ValueNotifier: state ngoài widget — share với widget khác, sống độc lập, không bị reset khi widget unmount (nếu dispose đúng).

7. ChangeNotifier

Multiple value, custom notify timing:

class CartModel extends ChangeNotifier {
  final List<Item> _items = [];

  List<Item> get items => List.unmodifiable(_items);
  int get totalItems => _items.fold(0, (a, i) => a + i.qty);
  int get totalPrice => _items.fold(0, (a, i) => a + i.price * i.qty);

  void add(Item item) {
    _items.add(item);
    notifyListeners();        // trigger rebuild của listener
  }

  void remove(Item item) {
    _items.remove(item);
    notifyListeners();
  }

  void clear() {
    _items.clear();
    notifyListeners();
  }
}

// Subscribe
final cart = CartModel();
AnimatedBuilder(
  animation: cart,
  builder: (context, child) => Text('Total: ${cart.totalItems}'),
)

8. Dispose — tránh leak

class _MyWidgetState extends State<MyWidget> {
  final _counter = ValueNotifier(0);
  final _cart = CartModel();

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

  @override
  Widget build(...) => ...;
}
⚠️ Không dispose = memory leak

ChangeNotifier / ValueNotifier giữ list listener. Nếu không dispose, listener cũ vẫn được call → setState sau dispose → crash hoặc leak.

9. Pitfall: setState sau dispose

class _MyState extends State<MyWidget> {
  String? _data;

  Future<void> _load() async {
    final r = await fetchData();
    // User có thể navigate away khi đang await — widget dispose
    if (!mounted) return;        // ✅ check
    setState(() => _data = r);
  }
}

10. Pattern: ThemeInherited custom

class ThemeScope extends StatefulWidget {
  final Widget child;
  const ThemeScope({super.key, required this.child});

  static _ThemeScopeState of(BuildContext context) =>
      context.findAncestorStateOfType<_ThemeScopeState>()!;

  @override
  State<ThemeScope> createState() => _ThemeScopeState();
}

class _ThemeScopeState extends State<ThemeScope> {
  ThemeMode _mode = ThemeMode.system;
  ThemeMode get mode => _mode;

  void toggle() {
    setState(() {
      _mode = _mode == ThemeMode.dark ? ThemeMode.light : ThemeMode.dark;
    });
  }

  @override
  Widget build(BuildContext context) => _ThemeInherited(
    mode: _mode,
    state: this,
    child: widget.child,
  );
}

class _ThemeInherited extends InheritedWidget {
  final ThemeMode mode;
  final _ThemeScopeState state;
  const _ThemeInherited({required this.mode, required this.state, required super.child});
  @override
  bool updateShouldNotify(_ThemeInherited old) => mode != old.mode;
}

11. Khi nào setState không đủ?

  • App state global: user, theme, locale, cart cross route.
  • Cross route persistence: cart không reset khi pop, login info giữ qua nhiều page.
  • State phức tạp transaction: multiple update atomic, async flow.
  • Cần testability: tách logic ra khỏi widget để test pure Dart.
  • Cần dependency injection: inject repository vào state.

Lúc đó → Chương 7 (Bloc) hoặc Chương 8 (Provider/Riverpod/GetX).

12. Bài tập

Counter với lifting state up

Counter app với 2 sibling button (+1, -1) và 1 sibling Text hiển thị value. State ở parent.

💡 Gợi ý đáp án
class CounterScreen extends StatefulWidget {
  @override
  State createState() => _CounterScreenState();
}

class _CounterScreenState extends State<CounterScreen> {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        _CountDisplay(count: _count),
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            _Button(label: '-1', onTap: () => setState(() => _count--)),
            _Button(label: '+1', onTap: () => setState(() => _count++)),
          ],
        ),
      ],
    );
  }
}

ThemeScope custom

Implement ThemeManager qua InheritedNotifier + ValueNotifier<ThemeMode>. Toggle button ở 1 screen, theme đổi toàn app.

💡 Gợi ý đáp án

Tham khảo section 10 (ThemeScope) hoặc section 5 (InheritedNotifier).

CartModel ChangeNotifier

Build CartModel (extends ChangeNotifier) với add/remove/total. UI 2 screen: ProductList (add to cart), Cart (show items + total). Cart đổi → UI cập nhật real-time.

💡 Gợi ý đáp án

Tham khảo section 7. Subscribe qua AnimatedBuilder(animation: cart, builder: ...).

Fix mounted check

Refactor widget có async fetch + setState gây "setState after dispose" → fix bằng mounted check.

💡 Gợi ý đáp án
Future<void> _load() async {
  final data = await fetch();
  if (!mounted) return;     // ← critical
  setState(() => _data = data);
}

LocaleProvider InheritedWidget

Tự viết InheritedWidget LocaleProvider cho i18n manual (không dùng package). Đổi locale, mọi descendant Text rebuild.

💡 Gợi ý đáp án

Tương tự ThemeScope. Hold Locale + map translation. Method tr('key') lookup.

13. Quiz

Q1

setState gọi ngoài State class — chuyện gì?

Xem đáp án

Đáp án: Compile error — setState là method protected của State<T>, không gọi từ ngoài được.

Q2

InheritedWidget descendant không gọi .of(context) — có rebuild khi data đổi?

Xem đáp án

Đáp án: Không. .of(context) = dependOnInheritedWidgetOfExactType register dependency. Không gọi = không subscribe.

Q3

updateShouldNotify trả false — chuyện gì?

Xem đáp án

Đáp án: Descendant không rebuild dù data có thể đã đổi (vì InheritedWidget bảo "không có gì mới"). Dùng khi data thực sự == nhau.

Q4

ChangeNotifier cần dispose không?

Xem đáp án

Đáp án: Có. Listener bị giữ đến khi dispose. Quên dispose → memory leak + setState sau dispose.

Q5

ValueListenableBuilder rebuild mấy lần khi value đổi?

Xem đáp án

Đáp án: 1 lần. Frame builder. Multiple sequential .value = ... trong cùng frame chỉ rebuild 1 lần cuối.

Q6

State bị reset khi navigate đi rồi back — vì sao?

Xem đáp án

Đáp án: StatefulWidget bị remove khỏi tree → state dispose. Pop về → tree mount mới, state reset. Lift state lên parent giữ qua navigation, hoặc state mgmt global.

Q7

mounted field trả gì sau dispose?

Xem đáp án

Đáp án: false. Dùng kiểm tra trước setState sau async.

Q8

InheritedWidget mutate field thẳng được không?

Xem đáp án

Đáp án: Không, immutable. Phải re-create instance qua StatefulWidget wrapper. updateShouldNotify compare old vs new.

14. Tổng kết

  • ✅ State spectrum: ephemeral / lifted / inherited / app-wide / persistent.
  • setState cho local widget state.
  • ✅ Lifting state up — pass props + callback. Limit: prop drilling sâu.
  • ✅ InheritedWidget — primitive cho share state subtree.
  • .of(context) = register dependency, auto rebuild khi data đổi.
  • updateShouldNotify control rebuild.
  • ✅ InheritedNotifier wrap Listenable, ChangeNotifier multiple value, ValueNotifier single value.
  • ✅ Dispose Notifier/Controller ở dispose().
  • mounted check sau async.
  • ✅ Khi setState không đủ → chương 7-8 (Bloc / Provider / Riverpod).

15. Kết nối