- 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
| Scope | Tool | Use case |
|---|---|---|
| Ephemeral (1 widget) | setState | Counter, toggle, hover, input controller |
| Lifted (parent + siblings) | Pass via constructor + callback | Filter + list, form parent |
| Inherited (subtree) | InheritedWidget | Theme, locale, user — descendant nhiều |
| App-wide | Bloc / Provider / Riverpod / GetX | Auth, cart, theme, complex domain |
| Persistent | SharedPreferences / Hive / SQLite | Setting, cache, offline data (ch.11) |
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ùngUserInherited.ofrebuild. - Trả
false→ skip rebuild (vì data không đổi thật sự).
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(...) => ...;
}
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
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.
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.
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.
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.
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.
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.
mounted field trả gì sau dispose?
Xem đáp án
Đáp án: false. Dùng kiểm tra trước setState sau async.
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.
- ✅
setStatecho 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. - ✅
updateShouldNotifycontrol rebuild. - ✅ InheritedNotifier wrap Listenable, ChangeNotifier multiple value, ValueNotifier single value.
- ✅ Dispose Notifier/Controller ở
dispose(). - ✅
mountedcheck sau async. - ✅ Khi setState không đủ → chương 7-8 (Bloc / Provider / Riverpod).
15. Kết nối
- Chương 7 — Bloc: BlocProvider build trên InheritedWidget.
- Chương 8: Provider/Riverpod cũng dùng InheritedWidget primitive.