- Hiểu mental model "everything is a Widget" — kể cả padding, margin, alignment.
- Phân biệt StatelessWidget vs StatefulWidget — khi nào dùng cái nào.
- Hiểu vòng đời
build()method và rebuild trigger. - Sử dụng
MaterialApp,Scaffold,AppBar,Text,Container,Centercơ bản. - Hiểu Widget Tree, Element Tree, RenderObject Tree (3 cây).
- Sử dụng
setStatecho local state. - Biết widget thường dùng: Image, Icon, ElevatedButton, Card, ListTile.
1. Mental model — Everything is a Widget
Câu mantra của Flutter. Nó nghĩa là gì trong thực tế?
Padding(
padding: const EdgeInsets.all(16), // padding là widget?
child: Center( // căn giữa là widget
child: Container( // box decoration là widget
width: 100,
height: 100,
color: Colors.blue,
child: const Text('Hello'), // text là widget
),
),
)
So với HTML/CSS, mỗi thuộc tính style là một attribute. Flutter thì style là widget:
<!-- HTML/CSS: style là attribute -->
<div style="padding: 16px; display: flex; align-items: center;">
<div style="width: 100px; height: 100px; background: blue;">Hello</div>
</div>
// SwiftUI: style là modifier
Text("Hello")
.frame(width: 100, height: 100)
.background(Color.blue)
.padding(16)
// Flutter: style là widget wrap quanh child
Padding(
padding: EdgeInsets.all(16),
child: Container(
width: 100, height: 100, color: Colors.blue,
child: const Text('Hello'),
),
)
- Composition mạnh — combine widget tự do, không bị giới hạn modifier có sẵn.
- Uniform API — không có phân biệt "container element" vs "style attribute". Mọi thứ build qua constructor.
- Tree là everything — debug bằng Flutter Inspector cực dễ vì tree đầy đủ.
Tradeoff: code dài hơn (nesting nhiều). Có shortcut với extension (Dart chương 8) hoặc helper widget custom.
2. Widget Tree
UI là một function của state: UI = f(state). runApp(root) đẩy widget root vào engine.
Engine render widget tree mỗi frame (60-120 FPS).
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: 'Demo',
home: Scaffold(
appBar: AppBar(title: const Text('IT Basic Demo')),
body: const Center(child: Text('Hello Flutter')),
),
);
}
}
3. Ba cây của Flutter
Khi runApp chạy, Flutter tạo và quản lý 3 cây song song:
| Cây | Vai trò | Mutable? | Tương ứng |
|---|---|---|---|
| Widget Tree | Configuration / blueprint | Immutable — tái tạo mỗi build | Class bạn viết |
| Element Tree | Instance trong tree, giữ state, lifecycle | Mutable | BuildContext = Element |
| RenderObject Tree | Layout + paint thực sự lên canvas | Mutable | Chương 13 deep dive |
- Widget immutable → cheap to create/recreate mỗi build. Hot reload work được nhờ vậy.
- Element giữ state + identity → state không mất khi parent rebuild (nếu type widget giữ nguyên).
- RenderObject heavy (layout, paint, hit test) → reuse giữa builds để performance.
Hiểu sơ thôi ở chương này. Chương 13 sẽ đào sâu khi cần debug performance hoặc custom RenderObject.
4. StatelessWidget
Widget không có state nội bộ — UI chỉ phụ thuộc constructor parameter (immutable config).
class Greeting extends StatelessWidget {
final String name;
final int? age;
const Greeting({super.key, required this.name, this.age});
@override
Widget build(BuildContext context) {
return Text('Xin chào $name, ${age ?? 0} tuổi');
}
}
// Use
const Greeting(name: 'Việt', age: 25);
Khi nào dùng StatelessWidget:
- UI thuần — chỉ render dựa input từ constructor.
- Reusable component không có local state.
- Wrap component có state khác (lift state up).
const constructor là performance hint
Stateless widget nên có const constructor khi mọi field là final + giá trị compile-time.
Caller gọi const Greeting(name: 'Việt') → Flutter biết widget không đổi, skip rebuild luôn.
5. StatefulWidget
Widget có state nội bộ thay đổi theo thời gian (counter, toggle, input controller, animation).
class Counter extends StatefulWidget {
const Counter({super.key, this.initialValue = 0});
final int initialValue;
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
late int _count;
@override
void initState() {
super.initState();
_count = widget.initialValue;
}
void _increment() {
setState(() {
_count++;
});
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Count: $_count', style: Theme.of(context).textTheme.headlineMedium),
ElevatedButton(onPressed: _increment, child: const Text('+1')),
],
);
}
}
Tại sao StatefulWidget cần 2 class?
StatefulWidget là config immutable (có thể const). State là instance mutable, giữ field, có lifecycle.
Tách 2 class → framework swap state khi widget rebuild mà state vẫn nguyên.
State lifecycle
State: tạo → init → build lặp → dispose khi unmount.Method chính:
createState()— gọi 1 lần khi widget được mount.initState()— gọi 1 lần sau create. Subscribe stream, fetch initial data ở đây.build()— gọi nhiều lần. Phải pure — không side effect.didUpdateWidget()— gọi khi widget parent rebuild với config mới.dispose()— cleanup: cancel stream, dispose controller, etc.
6. setState()
Khi state thay đổi, gọi setState(callback) để báo Flutter "rebuild đi". Callback đổi field state.
// ✅ Đúng
setState(() {
_count++;
_label = 'Updated';
});
// ❌ Sai — đổi field nhưng không gọi setState
_count++; // UI không refresh
// ❌ Sai — gọi async logic trong setState callback
setState(() {
final data = await fetchData(); // callback không được async
});
// ✅ Đúng — async ngoài, setState sau khi có data
final data = await fetchData();
if (!mounted) return; // kiểm tra widget còn alive
setState(() {
_data = data;
});
mounted check sau async
Nếu user nav away khi đang await, widget dispose. Gọi setState sau đó → exception
"setState() called after dispose()".
Pattern: if (!mounted) return; trước setState async. Đặc biệt sau await.
7. MaterialApp & Scaffold
MaterialApp
Wrap root app với Material Design (Google's design system). Provide theme, navigator, localization.
MaterialApp(
title: 'My App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
),
home: const HomePage(),
routes: {
'/details': (ctx) => const DetailsPage(),
},
debugShowCheckedModeBanner: false, // ẩn banner DEBUG góc phải
)
Scaffold
Skeleton của 1 page với các slot: appBar, body, FAB, drawer, bottomNavigationBar, snackBar.
Scaffold(
appBar: AppBar(
title: const Text('My Page'),
actions: [
IconButton(icon: const Icon(Icons.search), onPressed: () {}),
],
),
body: const Center(child: Text('Body content')),
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: const Icon(Icons.add),
),
drawer: const Drawer(/* ... */),
bottomNavigationBar: BottomNavigationBar(items: [/*...*/]),
)
8. Widget cơ bản — hiển thị
// Text
const Text('Hello')
Text('Hello', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold))
// Icon
const Icon(Icons.favorite, color: Colors.red, size: 32)
// Image
Image.network('https://example.com/cat.jpg')
Image.asset('assets/images/logo.png')
// Container — kitchen sink widget
Container(
width: 200,
height: 100,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.amber,
borderRadius: BorderRadius.circular(12),
boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 8)],
),
child: const Text('Card'),
)
// SizedBox — kích thước cố định, hay dùng cho spacing
const SizedBox(height: 16), // space dọc 16px
const SizedBox(width: 8), // space ngang 8px
// Padding
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text('Padded text'),
)
// Center — wrap child căn giữa parent
const Center(child: Text('Centered'))
9. Widget interactive cơ bản
// ElevatedButton — primary action
ElevatedButton(
onPressed: () => print('tap'),
child: const Text('Submit'),
)
// onPressed: null → button disabled (mờ + không tap)
const ElevatedButton(
onPressed: null,
child: Text('Disabled'),
)
// TextButton — secondary, không elevation
TextButton(onPressed: () {}, child: const Text('Cancel'))
// OutlinedButton — viền
OutlinedButton(onPressed: () {}, child: const Text('Learn more'))
// IconButton
IconButton(
icon: const Icon(Icons.favorite),
onPressed: () {},
)
// Card — Material card với elevation, rounded corner
Card(
child: ListTile(
leading: const Icon(Icons.person),
title: const Text('Việt'),
subtitle: const Text('Software engineer'),
trailing: const Icon(Icons.arrow_forward_ios),
onTap: () {},
),
)
10. BuildContext
BuildContext là handle vào Element Tree. Mỗi widget có một BuildContext riêng tại vị trí của nó.
Dùng để:
- Lookup ancestor —
Theme.of(context),MediaQuery.of(context). - Navigation —
Navigator.of(context).push(...). - Show overlay —
ScaffoldMessenger.of(context).showSnackBar(...). - Read state —
context.read<Bloc>()(chương 7).
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final screenWidth = MediaQuery.of(context).size.width;
return Text(
'Width: $screenWidth',
style: theme.textTheme.bodyLarge,
);
}
await
Nếu lưu context vào biến rồi await, sau đó dùng — context có thể không còn hợp lệ (widget unmount).
Cần check mounted trước khi dùng context async:
onPressed: () async {
final r = await _loadData();
if (!context.mounted) return;
Navigator.of(context).push(MaterialPageRoute(...));
}
11. const widget — performance hint chính
// Không const — tạo widget mới mỗi build
Text('Hello')
// Const — canonical, share instance, không rebuild
const Text('Hello')
// Tận dụng const cho mọi widget có thể
return const Padding(
padding: EdgeInsets.all(16),
child: Text('Hello'),
);
// Chỉ const được khi mọi nested widget là const + mọi field final + const constructor
Lint prefer_const_constructors + prefer_const_literals_to_create_immutables highlight chỗ có thể const.
Sử dụng tối đa cho perf.
12. Counter app — từ flutter create
Mở lib/main.dart sau khi flutter create:
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: 'Counter',
theme: ThemeData(useMaterial3: true),
home: const CounterPage(),
);
}
}
class CounterPage extends StatefulWidget {
const CounterPage({super.key});
@override
State<CounterPage> createState() => _CounterPageState();
}
class _CounterPageState extends State<CounterPage> {
int _count = 0;
void _increment() => setState(() => _count++);
void _decrement() => setState(() => _count--);
void _reset() => setState(() => _count = 0);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Counter')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Số đếm:'),
Text('$_count', style: Theme.of(context).textTheme.displayMedium),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(onPressed: _decrement, child: const Text('-1')),
OutlinedButton(onPressed: _reset, child: const Text('Reset')),
ElevatedButton(onPressed: _increment, child: const Text('+1')),
],
),
],
),
),
);
}
}
13. Bài tập
Profile screen Stateless
Recreate màn hình profile đơn giản: avatar tròn + name + email + 3 stat (posts/followers/following).
Chỉ widget cơ bản, không cần state. Tận dụng const tối đa.
💡 Gợi ý đáp án
class ProfilePage extends StatelessWidget {
const ProfilePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Profile')),
body: Column(
children: [
const SizedBox(height: 24),
const CircleAvatar(
radius: 50,
backgroundImage: NetworkImage('https://i.pravatar.cc/200'),
),
const SizedBox(height: 16),
const Text('Việt Nguyễn', style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold)),
const Text('viet@example.com'),
const SizedBox(height: 32),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: const [
_Stat(label: 'Posts', value: '42'),
_Stat(label: 'Followers', value: '1.2k'),
_Stat(label: 'Following', value: '200'),
],
),
],
),
);
}
}
class _Stat extends StatelessWidget {
final String label;
final String value;
const _Stat({required this.label, required this.value});
@override
Widget build(BuildContext context) => Column(
children: [
Text(value, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
Text(label),
],
);
}
Counter với 3 button
Viết Counter widget với 3 button: +1, -1, reset. Hiển thị value với Text lớn.
Style theo Material 3 default.
💡 Gợi ý đáp án
Tham khảo section 12 ("Counter app"). Hoặc thử variation: thêm step param cho phép +N/-N.
Const audit
Mở counter app sau flutter create. Audit toàn file lib/main.dart: bao nhiêu widget có thể convert sang const?
Bao nhiêu không thể (vì có biến runtime)?
💡 Gợi ý đáp án
const Text('Counter') được — string literal. Text('$_count') không — vì _count runtime.
const Icon(Icons.add) được. ElevatedButton(onPressed: _increment, child: ...) không — vì _increment là tear-off của instance method.
Lint prefer_const_constructors highlight chỗ thiếu — bật trong analysis_options.yaml.
Clock với Timer
Viết Stateful Clock hiển thị thời gian hiện tại (giờ:phút:giây). Dùng Timer.periodic trong initState,
cancel ở dispose. Cập nhật mỗi giây.
💡 Gợi ý đáp án
import 'dart:async';
class Clock extends StatefulWidget {
const Clock({super.key});
@override
State<Clock> createState() => _ClockState();
}
class _ClockState extends State<Clock> {
late Timer _timer;
DateTime _now = DateTime.now();
@override
void initState() {
super.initState();
_timer = Timer.periodic(const Duration(seconds: 1), (_) {
setState(() => _now = DateTime.now());
});
}
@override
void dispose() {
_timer.cancel(); // CRITICAL: nếu không, leak + setState sau dispose
super.dispose();
}
@override
Widget build(BuildContext context) {
final t = _now;
return Text(
'${t.hour.toString().padLeft(2, '0')}:'
'${t.minute.toString().padLeft(2, '0')}:'
'${t.second.toString().padLeft(2, '0')}',
style: Theme.of(context).textTheme.displayLarge,
);
}
}
WelcomeCard Stateless
Tạo widget WelcomeCard(name, role) Stateless với Card chứa avatar tròn + 2 text.
Sử dụng BoxDecoration cho avatar tròn nếu cần. Demo với 3 instance khác nhau.
💡 Gợi ý đáp án
class WelcomeCard extends StatelessWidget {
final String name;
final String role;
const WelcomeCard({super.key, required this.name, required this.role});
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
CircleAvatar(child: Text(name[0])),
const SizedBox(width: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(name, style: const TextStyle(fontWeight: FontWeight.bold)),
Text(role),
],
),
],
),
),
);
}
}
14. Quiz
Stateful widget cần 2 class — vì sao?
Xem đáp án
Đáp án: Để widget config immutable (có thể const), state mutable riêng. Framework swap state khi widget tree thay đổi, instance state vẫn giữ field nguyên.
build() gọi nhiều lần — có ổn không?
Xem đáp án
Đáp án: Có, build PHẢI pure — không side effect (không fetch HTTP, không setState, không Future.delayed). Framework tự optimize qua const + Element diff.
setState(() { fetch(); }) — bug ở đâu?
Xem đáp án
Đáp án: setState callback chỉ để đổi state field, không phải gọi async. Fetch nên gọi ngoài, sau đó setState(() => _data = result) với kết quả.
const SizedBox(height: 16) vs SizedBox(height: 16) — runtime khác gì?
Xem đáp án
Đáp án: const canonicalize — 2 lần dùng cùng instance memory. Không trigger rebuild khi parent rebuild. const > không const luôn nếu có thể.
BuildContext truyền vào async function rồi dùng sau await — vấn đề?
Xem đáp án
Đáp án: Context có thể invalid (widget unmount). Check if (!context.mounted) return; trước khi dùng. Hoặc lint use_build_context_synchronously.
Widget có Key — vai trò chính là gì?
Xem đáp án
Đáp án: Giúp Flutter reconcile (match widget mới với element cũ) khi tree đổi shape — đặc biệt list reorder. 99% case không cần dùng explicit Key.
Scaffold cần thiết cho mọi page không?
Xem đáp án
Đáp án: Không bắt buộc. Scaffold cung cấp skeleton (AppBar, body, FAB, drawer, SnackBar host). Bỏ qua được — dùng Container thuần hoặc SafeArea.
Khác biệt Image.network và Image.asset?
Xem đáp án
Đáp án: Network = fetch từ URL runtime (cần internet permission, có placeholder/error builder). Asset = file local declare trong pubspec.yaml assets:, bundle vào app.
15. Tổng kết
- ✅ Mental model "everything is a Widget" — padding, alignment, decoration đều là widget.
- ✅
UI = f(state)declarative model. - ✅ 3 cây: Widget (immutable spec) / Element (instance + state) / RenderObject (layout + paint).
- ✅ StatelessWidget — UI thuần từ constructor.
- ✅ StatefulWidget — 2 class, lifecycle initState → build → didUpdateWidget → dispose.
- ✅
setStateschedule rebuild, không sync. - ✅
mountedcheck sau async. - ✅ MaterialApp + Scaffold = skeleton chuẩn.
- ✅ Widget cơ bản: Text, Icon, Image, Container, SizedBox, Padding, Center, Button family, Card, ListTile.
- ✅ BuildContext = handle Element, lookup ancestor.
- ✅
constconstructor = perf hint chính.
16. Kết nối
- Chương 3 — Layout: constraints flow down, Row/Column/Stack/Expanded.
- Chương 6 — State management cơ bản: lifting state up, InheritedWidget.
- Chương 13 — Custom Paint: 3 cây deep dive.
- Dart chương 3 — named param +
requiredlà pattern của mọi widget constructor. - Dart chương 4 — class extends StatelessWidget / StatefulWidget.