Stateful Widgets
Stateful Widgets
Stateful Widget គឺជា widget ដែលមាន state អាចផ្លាស់ប្តូរបាន។ ប្រើសម្រាប់ interactive UI ដែលឆ្លើយតបទៅនឹងការប្រែប្រួលរបស់ទិន្នន័យ។ Flutter manages state efficiently through setState() mechanism។
📚 Stateful Widget Architecture
Two-Class Pattern:
Stateful Widget Structure
1️⃣ StatefulWidget Class (Immutable)
- Widget configuration (final properties)
- createState() method
2️⃣ State Class (Mutable)
- Holds changing data (state variables)
- Lifecycle methods (initState, dispose, etc.)
- build() method
- setState() for updates
Why Two Classes?
✅ Widget remains immutable
✅ State can change independently
✅ Better memory management
✅ Clear separation of concerns
Complete Lifecycle:
State Object Lifecycle
1️⃣ createState()
↓
2️⃣ initState() - Called once when state created
↓
3️⃣ didChangeDependencies() - When inherited widget changes
↓
4️⃣ build() - First render
↓
5️⃣ User Interaction / Data Change
↓
6️⃣ setState() called
↓
7️⃣ build() - Re-render with new state
↓ (loop 5-7 repeats)
8️⃣ deactivate() - Widget removed from tree
↓
9️⃣ dispose() - State object destroyed
Hot Reload:
↓
reassemble() - Development only
Lifecycle Methods Comparison:
Method | Called When | Use Case | Frequency |
---|---|---|---|
initState() |
State created (once) | Initialize data, subscribe to streams | 1 time |
didChangeDependencies() |
After initState, when dependencies change | React to inherited widget changes | Few times |
build() |
After initState, every setState() | Return widget tree | Many times |
didUpdateWidget() |
Parent widget changes configuration | Compare old/new widget properties | Sometimes |
setState() |
You call it when state changes | Trigger rebuild | User-driven |
deactivate() |
Widget removed from tree | Cleanup before removal | 1 time |
dispose() |
State destroyed permanently | Cancel streams, dispose controllers | 1 time |
🔄 setState() Deep Dive
How setState() Works:
setState() Process
1️⃣ You call setState(() { /* change state */ })
↓
2️⃣ Flutter marks widget as "dirty"
↓
3️⃣ State changes execute immediately
↓
4️⃣ build() scheduled for next frame
↓
5️⃣ Flutter calls build() method
↓
6️⃣ New Widget tree created
↓
7️⃣ Flutter compares old vs new tree (diffing)
↓
8️⃣ Only changed widgets re-rendered (efficient!)
↓
9️⃣ UI updated on screen
⚠️ setState() Rules:
- ❌ Don't call setState() in build() method (infinite loop!)
- ❌ Don't call setState() after dispose() (error!)
- ✅ Always put state changes INSIDE setState(() {...})
- ✅ Keep setState() callbacks fast and synchronous
🎯 ពេលណាប្រើ Stateful?
Scenario | Example | Why Stateful? |
---|---|---|
User Interaction | Buttons, forms, checkboxes | Need to track user input changes |
Animations | Fade, scale, rotation | State changes over time |
Real-time Data | Chat messages, notifications | Data updates from server |
Timers/Counters | Stopwatch, countdown | Value changes periodically |
API Calls | Loading states, fetched data | Asynchronous state changes |
Form Validation | Email validation, password strength | Track validation state |
📝 Stateful Widget Structure
Basic Template:
class Counter extends StatefulWidget {
// Optional: Properties from parent (final)
final int initialValue;
const Counter({Key? key, this.initialValue = 0}) : super(key: key);
@override
_CounterState createState() => _CounterState();
}
class _CounterState extends State<Counter> {
// State variables (mutable, not final)
int count = 0;
@override
void initState() {
super.initState();
// Initialize state from widget property
count = widget.initialValue;
print('Counter initialized with: ' + count.toString());
}
void increment() {
setState(() {
count++; // This triggers rebuild
});
}
void decrement() {
setState(() {
count--;
});
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Count: ' + count.toString(),
style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
),
SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: decrement,
child: Text('-'),
),
SizedBox(width: 20),
ElevatedButton(
onPressed: increment,
child: Text('+'),
),
],
),
],
);
}
@override
void dispose() {
// Cleanup resources
print('Counter disposed');
super.dispose();
}
}
✅ Real-World Examples
1. Toggle Switch with Color Change
class ToggleExample extends StatefulWidget {
@override
_ToggleExampleState createState() => _ToggleExampleState();
}
class _ToggleExampleState extends State<ToggleExample> {
bool isOn = false;
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
color: isOn ? Colors.green : Colors.grey,
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
Text(
isOn ? 'ON' : 'OFF',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
SizedBox(height: 10),
Switch(
value: isOn,
activeColor: Colors.white,
onChanged: (value) {
setState(() {
isOn = value;
});
},
),
],
),
);
}
}
2. Form Input with Validation
class LoginForm extends StatefulWidget {
@override
_LoginFormState createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
final _formKey = GlobalKey<FormState>();
String email = '';
String password = '';
bool isLoading = false;
bool isPasswordVisible = false;
@override
void initState() {
super.initState();
print('Login form initialized');
}
Future<void> _handleSubmit() async {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
setState(() {
isLoading = true;
});
// Simulate API call
await Future.delayed(Duration(seconds: 2));
setState(() {
isLoading = false;
});
print('Login successful!');
}
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
decoration: InputDecoration(labelText: 'Email'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter email';
}
if (!value.contains('@')) {
return 'Invalid email';
}
return null;
},
onSaved: (value) {
email = value!;
},
),
SizedBox(height: 16),
TextFormField(
obscureText: !isPasswordVisible,
decoration: InputDecoration(
labelText: 'Password',
suffixIcon: IconButton(
icon: Icon(
isPasswordVisible ? Icons.visibility : Icons.visibility_off,
),
onPressed: () {
setState(() {
isPasswordVisible = !isPasswordVisible;
});
},
),
),
validator: (value) {
if (value == null || value.length < 6) {
return 'Password must be at least 6 characters';
}
return null;
},
onSaved: (value) {
password = value!;
},
),
SizedBox(height: 24),
ElevatedButton(
onPressed: isLoading ? null : _handleSubmit,
child: isLoading
? CircularProgressIndicator(color: Colors.white)
: Text('Login'),
),
],
),
);
}
}
3. Timer/Stopwatch
class Stopwatch extends StatefulWidget {
@override
_StopwatchState createState() => _StopwatchState();
}
class _StopwatchState extends State<Stopwatch> {
int seconds = 0;
bool isRunning = false;
Timer? timer;
@override
void initState() {
super.initState();
print('Stopwatch initialized');
}
void startTimer() {
setState(() {
isRunning = true;
});
timer = Timer.periodic(Duration(seconds: 1), (timer) {
setState(() {
seconds++;
});
});
}
void stopTimer() {
setState(() {
isRunning = false;
});
timer?.cancel();
}
void resetTimer() {
stopTimer();
setState(() {
seconds = 0;
});
}
String get formattedTime {
int mins = seconds ~/ 60;
int secs = seconds % 60;
return mins.toString().padLeft(2, '0') + ':' +
secs.toString().padLeft(2, '0');
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
formattedTime,
style: TextStyle(fontSize: 48, fontWeight: FontWeight.bold),
),
SizedBox(height: 30),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: isRunning ? null : startTimer,
style: ElevatedButton.styleFrom(backgroundColor: Colors.green),
child: Text('Start'),
),
SizedBox(width: 10),
ElevatedButton(
onPressed: isRunning ? stopTimer : null,
style: ElevatedButton.styleFrom(backgroundColor: Colors.orange),
child: Text('Stop'),
),
SizedBox(width: 10),
ElevatedButton(
onPressed: resetTimer,
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
child: Text('Reset'),
),
],
),
],
);
}
@override
void dispose() {
timer?.cancel(); // Important: Cancel timer to prevent memory leak!
print('Stopwatch disposed');
super.dispose();
}
}
4. API Data Fetching with Loading State
class UserList extends StatefulWidget {
@override
_UserListState createState() => _UserListState();
}
class _UserListState extends State<UserList> {
List<User> users = [];
bool isLoading = false;
String? errorMessage;
@override
void initState() {
super.initState();
fetchUsers();
}
Future<void> fetchUsers() async {
setState(() {
isLoading = true;
errorMessage = null;
});
try {
// Simulate API call
await Future.delayed(Duration(seconds: 2));
// Mock data
List<User> fetchedUsers = [
User(id: 1, name: 'សុខា'),
User(id: 2, name: 'ដារា'),
User(id: 3, name: 'សុភា'),
];
setState(() {
users = fetchedUsers;
isLoading = false;
});
} catch (e) {
setState(() {
errorMessage = 'Failed to load users';
isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
if (isLoading) {
return Center(child: CircularProgressIndicator());
}
if (errorMessage != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(errorMessage!, style: TextStyle(color: Colors.red)),
SizedBox(height: 20),
ElevatedButton(
onPressed: fetchUsers,
child: Text('Retry'),
),
],
),
);
}
return ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
User user = users[index];
return ListTile(
title: Text(user.name),
subtitle: Text('ID: ' + user.id.toString()),
);
},
);
}
}
class User {
final int id;
final String name;
User({required this.id, required this.name});
}
5. Favorite/Like Button
class FavoriteButton extends StatefulWidget {
final int itemId;
const FavoriteButton({Key? key, required this.itemId}) : super(key: key);
@override
_FavoriteButtonState createState() => _FavoriteButtonState();
}
class _FavoriteButtonState extends State<FavoriteButton>
with SingleTickerProviderStateMixin {
bool isFavorite = false;
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 300),
vsync: this,
);
_loadFavoriteStatus();
}
Future<void> _loadFavoriteStatus() async {
// Load from local storage or API
await Future.delayed(Duration(milliseconds: 500));
setState(() {
isFavorite = false; // Default value
});
}
void _toggleFavorite() {
setState(() {
isFavorite = !isFavorite;
});
if (isFavorite) {
_controller.forward();
} else {
_controller.reverse();
}
// Save to backend
_saveFavoriteStatus();
}
Future<void> _saveFavoriteStatus() async {
// API call to save
print('Item ' + widget.itemId.toString() + ' favorite: ' + isFavorite.toString());
}
@override
Widget build(BuildContext context) {
return IconButton(
icon: Icon(
isFavorite ? Icons.favorite : Icons.favorite_border,
color: isFavorite ? Colors.red : Colors.grey,
),
onPressed: _toggleFavorite,
);
}
@override
void dispose() {
_controller.dispose(); // Always dispose animation controllers!
super.dispose();
}
}
⚡ Performance Optimization
1. Use const Widgets
class MyWidget extends StatefulWidget {
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
int count = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
// ✅ This widget won't rebuild (const)
const Text('Static Title'),
// ❌ This widget rebuilds every time
Text('Count: ' + count.toString()),
// ✅ Button is const, only onPressed reference changes
const ElevatedButton(
onPressed: null, // Would need non-const for real button
child: const Text('Click'),
),
],
);
}
}
2. Extract Widgets that Don't Change
// ❌ Bad: Entire widget rebuilds
class BadExample extends StatefulWidget {
@override
_BadExampleState createState() => _BadExampleState();
}
class _BadExampleState extends State<BadExample> {
int count = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
_buildHeader(), // Rebuilds unnecessarily!
Text('Count: ' + count.toString()),
ElevatedButton(
onPressed: () => setState(() => count++),
child: Text('Increment'),
),
],
);
}
Widget _buildHeader() {
return Container(
padding: EdgeInsets.all(20),
child: Text('My App Header'),
);
}
}
// ✅ Good: Extract to separate StatelessWidget
class GoodExample extends StatefulWidget {
@override
_GoodExampleState createState() => _GoodExampleState();
}
class _GoodExampleState extends State<GoodExample> {
int count = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
const AppHeader(), // Won't rebuild!
Text('Count: ' + count.toString()),
ElevatedButton(
onPressed: () => setState(() => count++),
child: Text('Increment'),
),
],
);
}
}
class AppHeader extends StatelessWidget {
const AppHeader({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(20),
child: Text('My App Header'),
);
}
}
3. Conditional setState()
void updateValue(int newValue) {
// ✅ Only call setState if value actually changed
if (value != newValue) {
setState(() {
value = newValue;
});
}
// ❌ Always calling setState (wasteful)
// setState(() {
// value = newValue;
// });
}
💡 Best Practices:
- ✅ Always call
super.initState()
first in initState() - ✅ Always call
super.dispose()
last in dispose() - ✅ Cancel timers, streams in dispose() to prevent memory leaks
- ✅ Use
const
for widgets that don't change - ✅ Extract static parts to separate StatelessWidgets
- ✅ Keep setState() callbacks small and fast
- ✅ Check
mounted
before setState() in async operations - ✅ Use
widget.propertyName
to access widget properties in State class
⚠️ Common Mistakes:
- ❌ Calling setState() in build() method (causes infinite loop)
- ❌ Forgetting to dispose controllers and timers (memory leaks)
- ❌ Calling setState() after widget is disposed (error)
- ❌ Not checking
mounted
before async setState() - ❌ Modifying state outside setState() (UI won't update)
- ❌ Making state variables
final
(can't change them!) - ❌ Heavy computation inside setState() (UI freezes)
🔍 Mounted Check for Async Operations
class SafeAsyncWidget extends StatefulWidget {
@override
_SafeAsyncWidgetState createState() => _SafeAsyncWidgetState();
}
class _SafeAsyncWidgetState extends State<SafeAsyncWidget> {
String data = 'Loading...';
@override
void initState() {
super.initState();
loadData();
}
Future<void> loadData() async {
await Future.delayed(Duration(seconds: 3));
// ✅ Check if widget is still mounted before calling setState
if (mounted) {
setState(() {
data = 'Data loaded!';
});
}
// Without mounted check: Error if user navigates away before 3 seconds
}
@override
Widget build(BuildContext context) {
return Text(data);
}
}
💡 ជំនួយ: ប្រើ Stateful Widget នៅពេលដែល UI ត្រូវការផ្លាស់ប្តូរ។ ចងចាំហៅ setState() រាល់ពេលដែល state ផ្លាស់ប្តូរ។ Dispose controllers and timers ដើម្បីកុំឱ្យមាន memory leaks។ Use const widgets for performance។