آشنایی با StatefulWidget در Flutter

اگه بخوای توی Flutter UI زنده باشه و با کلیک/اسکرول/داده‌ی جدید عوض بشه، باید بری سراغ StatefulWidget.
یه ویجت + یه State که مثل «حافظه‌ی کوتاه‌مدت» همون ویجته. باهاش می‌تونی داده نگه داری، UI رو آپدیت کنی و رفتار صفحه رو کنترل کنی.

⚡️ StatefulWidget چیه؟

StatefulWidget مثل چراغ مطالعه‌ست:

  • خود چراغ عوض نمی‌شه (Widget immutable)

  • ولی حالتش روشن/خاموش می‌شه (State تغییر می‌کنه)

  • تو هم با یه دکمه (setState) می‌گی «وضعیت عوض شد، دوباره بساز!» 😎

📌 مهم‌ترین بخش‌های StatefulWidget:

🔹 1. createState()

اتصال ویجت به کلاس State. همیشه یه کلاس State می‌سازی که منطق و وضعیت رو نگه می‌داره.

class MyWidget extends StatefulWidget {
  const MyWidget({super.key});
  @override
  State<MyWidget> createState() => _MyWidgetState();
}

🔹 2. کلاس State

اینجا متغیرهای قابل تغییرت رو می‌ذاری و UI رو می‌سازی.

class _MyWidgetState extends State<MyWidget> {
  int counter = 0;

  @override
  Widget build(BuildContext context) {
    return Text('$counter');
  }
}

🔹 3. setState(() { ... })

به Flutter می‌گه «وضعیت عوض شد» تا متد build دوباره اجرا بشه.

🔹 4. initState - dispose

  • initState: یک‌بار برای مقداردهی اولیه (کنترلر، تایمر، استریم)

  • dispose: جمع‌کردن منابع برای جلوگیری از Memory Leak

🔹 5. build(context)

خروجی UI بر اساس وضعیت فعلی. قرار نیست اینجا کار سنگین یا Side Effect انجام بدی.

🔹 6. mounted

می‌گه State هنوز توی درخته یا نه. بعد از async قبل از setState چک کن.

🔹 7. didUpdateWidget / didChangeDependencies

وقتی props والد عوض بشه یا Inheritedها تغییر کنن (Theme/Locale/MediaQuery)، اینا به کار میان.

🚀 یک مثال کامل StatefulWidget:

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(
      debugShowCheckedModeBanner: false,
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _count = 0;
  late final TextEditingController _controller;
  bool _loading = false;
  String? _greeting;

  @override
  void initState() {
    super.initState();
    _controller = TextEditingController();
  }

  @override
  void dispose() {
    _controller.dispose(); // خیلی مهم!
    super.dispose();
  }

  Future<void> _sayHi() async {
    setState(() => _loading = true);

    // شبیه‌سازی درخواست غیرهمزمان
    await Future.delayed(const Duration(milliseconds: 800));

    final name = _controller.text.trim().isEmpty
        ? 'دوست عزیز'
        : _controller.text.trim();

    if (!mounted) return; // جلوگیری از setState بعد از dispose

    setState(() {
      _greeting = 'سلام $name 👋';
      _loading = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("مثال StatefulWidget")),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Text('$_count', style: const TextStyle(fontSize: 48)),
              const SizedBox(height: 8),
              ElevatedButton.icon(
                onPressed: () => setState(() => _count++),
                icon: const Icon(Icons.add),
                label: const Text('اضافه کن'),
              ),
              const SizedBox(height: 24),
              TextField(
                controller: _controller,
                decoration: const InputDecoration(
                  labelText: 'اسم‌ت رو بنویس',
                  border: OutlineInputBorder(),
                ),
              ),
              const SizedBox(height: 8),
              _loading
                  ? const CircularProgressIndicator()
                  : ElevatedButton(
                      onPressed: _sayHi,
                      child: const Text('سلام بده'),
                    ),
              if (_greeting != null) ...[
                const SizedBox(height: 8),
                Text(_greeting!, style: const TextStyle(fontSize: 18)),
              ],
            ],
          ),
        ),
      ),
    );
  }
}

🎯 خروجی این کد:

  • شمارنده‌ای که با هر کلیک آپدیت می‌شه

  • یک TextField برای گرفتن اسم

  • دکمه‌ای که بعد از یه تأخیر کوتاه سلام می‌ده (با رعایت mounted)

  • همه‌چی داخل یک StatefulWidget مدیریت می‌شه

✨ نکته‌ها و ترفندها:

setState داخل build صدا نزن. اگه لازم شد کاری بعد از اولین رندر انجام بدی:

WidgetsBinding.instance.addPostFrameCallback((_) {
  // مثلا نمایش SnackBar
});
  • هر منبعی ساختی (Controller/Timer/AnimationController/StreamSubscription) رو تو dispose آزاد کن.

  • وقتی props والد عوض می‌شه و باید State هماهنگ بشه، از didUpdateWidget با احتیاط استفاده کن.

  • برای بهبود عملکرد، از const استفاده کن و UI رو به ویجت‌های کوچک‌تر بشکن تا محدوده‌ی rebuild کم بشه.

  • وضعیت خیلی ساده داری؟ ValueNotifier + ValueListenableBuilder گزینه‌ی سبکه.

  • وضعیت بین چند صفحه/بخش مشترک شد؟ برو سراغ Provider/Riverpod/BLoC.

  • می‌خوای وضعیت یه تب حفظ بشه؟ از AutomaticKeepAliveClientMixin استفاده کن.

🆚 کی Stateless و کی Stateful؟

  • فقط نمایش داده از بیرون → StatelessWidget

  • وضعیت محلی و موقتی (فرم، شمارنده، انیمیشن کوتاه) → StatefulWidget

  • وضعیت اشتراکی/پیچیده → ابزار مدیریت وضعیت

موفق باشید ✨

📌 خیلی خلاصه مهم‌ترین بخش‌های StatefulWidget:

  • createState: اتصال ویجت به کلاس State

  • کلاس State: جایی برای متغیرهای قابل تغییر و ساخت UI

  • setState: اعلام تغییر وضعیت برای rebuild

  • initState و dispose: شروع و جمع‌کردن منابع (Controller/Timer/Stream)

  • didUpdateWidget / didChangeDependencies: واکنش به تغییر props والد یا Inheritedها

  • mounted: قبل از setState بعد از async چک کن


پ.ن— باتوجه به پست قبلی که در مورد آشنایی «pubspec.yaml» بود
برای فعال‌کردن فارسی و راست‌به‌چپ، پکیج رسمی flutter_localizations (جزئی از خود Flutter) رو به dependencies اضافه کردم و بعد توی MaterialApp لوکال رو fa ست کردم:

dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter

✅ با همین چند خط، TextField و کل UI راست‌به‌چپ می‌شن.