© Khmer Angkor Academy - sophearithput168

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។