StatefulWidget ودورة الحياة
StatefulWidget مقابل StatelessWidget
بينما StatelessWidget غير قابل للتغيير ولا يمكن أن يتغير بعد بنائه، فإن StatefulWidget يحتفظ بحالة قابلة للتغيير يمكن أن تتغير خلال عمر الودجت. عندما تتغير الحالة، يعيد الودجت بناء واجهة المستخدم لتعكس الحالة الجديدة.
يتكون StatefulWidget من فئتين:
- فئة الودجت نفسها — غير قابلة للتغيير، تحدد الإعدادات
- فئة State — قابلة للتغيير، تحتفظ بالحالة وتبني واجهة المستخدم
هيكل StatefulWidget
import 'package:flutter/material.dart';
class CounterWidget extends StatefulWidget {
final int initialValue;
const CounterWidget({super.key, this.initialValue = 0});
@override
State<CounterWidget> createState() => _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget> {
late int _count;
@override
void initState() {
super.initState();
_count = widget.initialValue;
}
@override
Widget build(BuildContext context) {
return Text('Count: \$_count');
}
}
_CounterWidgetState) مما يجعلها خاصة بالمكتبة. هذا هو اصطلاح Dart لتفاصيل التنفيذ التي لا ينبغي الوصول إليها من الخارج.دالة createState()
دالة createState() هي الدالة الوحيدة المطلوبة في StatefulWidget. تُنشئ كائن State القابل للتغيير الذي سيُربط بالودجت. يستدعي Flutter هذه الدالة عندما يدرج الودجت في الشجرة لأول مرة.
شرح createState()
class ToggleSwitch extends StatefulWidget {
final String label;
final ValueChanged<bool>? onChanged;
const ToggleSwitch({
super.key,
required this.label,
this.onChanged,
});
@override
State<ToggleSwitch> createState() => _ToggleSwitchState();
}
class _ToggleSwitchState extends State<ToggleSwitch> {
bool _isOn = false;
void _toggle() {
setState(() {
_isOn = !_isOn;
});
widget.onChanged?.call(_isOn);
}
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(widget.label),
const SizedBox(width: 8),
Switch(value: _isOn, onChanged: (_) => _toggle()),
],
);
}
}
لاحظ كيف تصل فئة State إلى خصائص الودجت من خلال خاصية widget. هذا يمنح كائن State الوصول إلى الإعدادات غير القابلة للتغيير المعرّفة في StatefulWidget.
فئة State ودوال دورة الحياة
تحتوي فئة State على عدة دوال دورة حياة يستدعيها Flutter في لحظات محددة. فهم هذه الدوال أمر حاسم لإدارة الموارد والاشتراكات والتأثيرات الجانبية.
نظرة عامة كاملة على دورة الحياة
class LifecycleDemo extends StatefulWidget {
final String title;
const LifecycleDemo({super.key, required this.title});
@override
State<LifecycleDemo> createState() => _LifecycleDemoState();
}
class _LifecycleDemoState extends State<LifecycleDemo> {
// 1. تُستدعى مرة واحدة عند إنشاء State
@override
void initState() {
super.initState();
debugPrint('initState called');
// تهيئة الحالة، بدء الرسوم المتحركة، الاشتراك في التدفقات
}
// 2. تُستدعى عند تغير التبعيات (مثل InheritedWidget)
@override
void didChangeDependencies() {
super.didChangeDependencies();
debugPrint('didChangeDependencies called');
// الاستجابة لتغييرات الودجت الموروثة
}
// 3. تُستدعى في كل مرة يحتاج فيها UI للعرض
@override
Widget build(BuildContext context) {
debugPrint('build called');
return Text(widget.title);
}
// 4. تُستدعى عندما يعيد الأب البناء بودجت جديد
@override
void didUpdateWidget(covariant LifecycleDemo oldWidget) {
super.didUpdateWidget(oldWidget);
debugPrint('didUpdateWidget called');
if (oldWidget.title != widget.title) {
// الاستجابة لتغييرات الإعدادات
}
}
// 5. تُستدعى عند إزالة الودجت مؤقتاً
@override
void deactivate() {
debugPrint('deactivate called');
super.deactivate();
}
// 6. تُستدعى عند إزالة الودجت نهائياً
@override
void dispose() {
debugPrint('dispose called');
// التنظيف: إلغاء المؤقتات، إغلاق التدفقات، التخلص من المتحكمات
super.dispose();
}
}
initState() — التهيئة
تُستدعى دالة initState() مرة واحدة بالضبط عند إنشاء كائن State لأول مرة. هنا يجب عليك:
- تهيئة متغيرات الحالة التي تعتمد على خصائص الودجت
- إعداد متحكمات الرسوم المتحركة
- الاشتراك في التدفقات أو المستمعين
- بدء العمليات غير المتزامنة لمرة واحدة
مثال على initState()
class AnimatedGreeting extends StatefulWidget {
final String name;
const AnimatedGreeting({super.key, required this.name});
@override
State<AnimatedGreeting> createState() => _AnimatedGreetingState();
}
class _AnimatedGreetingState extends State<AnimatedGreeting>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_fadeAnimation = Tween<double>(begin: 0.0, end: 1.0)
.animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeIn,
));
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return FadeTransition(
opacity: _fadeAnimation,
child: Text('مرحباً، \${widget.name}!',
style: const TextStyle(fontSize: 24),
),
);
}
}
setState() داخل initState(). أيضاً، BuildContext غير متاح بالكامل بعد، لذا لا تستخدم Theme.of(context) أو MediaQuery.of(context) هنا. استخدم didChangeDependencies() لذلك بدلاً من ذلك.didChangeDependencies()
تُستدعى هذه الدالة مباشرة بعد initState() وكلما تغير InheritedWidget الذي تعتمد عليه هذه الحالة. هذا هو المكان المناسب للوصول إلى BuildContext لأول مرة.
مثال على didChangeDependencies()
class ThemeAwareWidget extends StatefulWidget {
const ThemeAwareWidget({super.key});
@override
State<ThemeAwareWidget> createState() => _ThemeAwareWidgetState();
}
class _ThemeAwareWidgetState extends State<ThemeAwareWidget> {
late TextStyle _headerStyle;
@override
void didChangeDependencies() {
super.didChangeDependencies();
// آمن لاستخدام context هنا
final theme = Theme.of(context);
_headerStyle = theme.textTheme.headlineMedium!.copyWith(
color: theme.colorScheme.primary,
);
}
@override
Widget build(BuildContext context) {
return Text('عنوان بسمة', style: _headerStyle);
}
}
didUpdateWidget() — الاستجابة لتغييرات الأب
عندما يعيد الودجت الأب البناء ويوفر نسخة ودجت جديدة بنفس runtimeType وkey، يستدعي Flutter دالة didUpdateWidget() على كائن State الحالي. هذا يسمح لك بالاستجابة لتغييرات الإعدادات دون فقدان الحالة.
مثال على didUpdateWidget()
class CountdownTimer extends StatefulWidget {
final int seconds;
const CountdownTimer({super.key, required this.seconds});
@override
State<CountdownTimer> createState() => _CountdownTimerState();
}
class _CountdownTimerState extends State<CountdownTimer> {
late int _remaining;
Timer? _timer;
@override
void initState() {
super.initState();
_remaining = widget.seconds;
_startTimer();
}
@override
void didUpdateWidget(covariant CountdownTimer oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.seconds != widget.seconds) {
_timer?.cancel();
_remaining = widget.seconds;
_startTimer();
}
}
void _startTimer() {
_timer = Timer.periodic(
const Duration(seconds: 1),
(timer) {
if (_remaining > 0) {
setState(() => _remaining--);
} else {
timer.cancel();
}
},
);
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Text(
'\$_remaining ث',
style: TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
color: _remaining < 10 ? Colors.red : Colors.black,
),
);
}
}
setState() — تشغيل إعادة البناء
دالة setState() هي الطريقة التي تخبر بها Flutter أن الحالة قد تغيرت وأن واجهة المستخدم تحتاج لإعادة البناء. عدّل دائماً متغيرات الحالة داخل دالة setState().
أنماط استخدام setState()
class ShoppingCart extends StatefulWidget {
const ShoppingCart({super.key});
@override
State<ShoppingCart> createState() => _ShoppingCartState();
}
class _ShoppingCartState extends State<ShoppingCart> {
final List<String> _items = [];
int _totalCount = 0;
void _addItem(String item) {
setState(() {
_items.add(item);
_totalCount = _items.length;
});
}
void _removeItem(int index) {
setState(() {
_items.removeAt(index);
_totalCount = _items.length;
});
}
void _clearCart() {
setState(() {
_items.clear();
_totalCount = 0;
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('السلة: \$_totalCount عنصر',
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
..._items.asMap().entries.map((entry) => ListTile(
title: Text(entry.value),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () => _removeItem(entry.key),
),
)),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: () => _addItem('عنصر \${_totalCount + 1}'),
child: const Text('إضافة عنصر'),
),
TextButton(
onPressed: _items.isEmpty ? null : _clearCart,
child: const Text('مسح الكل'),
),
],
),
],
);
}
}
setState() بسيطاً — فقط عدّل متغيرات الحالة. لا تقم بعمليات حسابية ثقيلة أو عمليات غير متزامنة داخل الدالة المُمررة. عدّل المتغيرات ودع دالة build() تتعامل مع منطق العرض.خاصية mounted
خاصية mounted تشير إلى ما إذا كان كائن State حالياً في شجرة الودجت. هذا حاسم للعمليات غير المتزامنة — يجب عليك التحقق من mounted قبل استدعاء setState() بعد await.
التحقق من mounted قبل setState
class DataLoader extends StatefulWidget {
final String url;
const DataLoader({super.key, required this.url});
@override
State<DataLoader> createState() => _DataLoaderState();
}
class _DataLoaderState extends State<DataLoader> {
String? _data;
bool _isLoading = true;
String? _error;
@override
void initState() {
super.initState();
_loadData();
}
Future<void> _loadData() async {
try {
// محاكاة طلب شبكة
await Future.delayed(const Duration(seconds: 2));
final result = 'تم تحميل البيانات من \${widget.url}';
// حاسم: التحقق من mounted قبل setState
if (!mounted) return;
setState(() {
_data = result;
_isLoading = false;
});
} catch (e) {
if (!mounted) return;
setState(() {
_error = e.toString();
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (_error != null) {
return Center(child: Text('خطأ: \$_error'));
}
return Center(child: Text(_data ?? 'لا توجد بيانات'));
}
}
setState() على كائن State غير مُركّب يُسبب خطأ. تحقق دائماً من if (!mounted) return; بعد أي فجوة غير متزامنة (بعد await). هذا يمنع الأعطال عندما ينتقل المستخدم بعيداً قبل اكتمال العملية غير المتزامنة.deactivate() و dispose()
deactivate() تُستدعى عندما يُزال State مؤقتاً من الشجرة (مثلاً عند نقل ودجت باستخدام GlobalKey). dispose() تُستدعى عندما يُزال State نهائياً ولن يبني مرة أخرى أبداً.
تنظيف الموارد بشكل صحيح
class StreamListenerWidget extends StatefulWidget {
const StreamListenerWidget({super.key});
@override
State<StreamListenerWidget> createState() => _StreamListenerWidgetState();
}
class _StreamListenerWidgetState extends State<StreamListenerWidget> {
late final ScrollController _scrollController;
late final TextEditingController _textController;
StreamSubscription? _subscription;
@override
void initState() {
super.initState();
_scrollController = ScrollController();
_textController = TextEditingController();
// الاشتراك في تدفق
_subscription = Stream.periodic(
const Duration(seconds: 1),
(i) => i,
).listen((value) {
if (mounted) {
setState(() {
// تحديث واجهة المستخدم بقيمة التدفق
});
}
});
}
@override
void deactivate() {
// تُستدعى عند الإزالة المؤقتة من الشجرة
debugPrint('تم إلغاء تنشيط الودجت');
super.deactivate();
}
@override
void dispose() {
// تنظيف جميع الموارد
_scrollController.dispose();
_textController.dispose();
_subscription?.cancel();
debugPrint('تم التخلص من الودجت - تم تنظيف الموارد');
super.dispose();
}
@override
Widget build(BuildContext context) {
return const Text('مستمع التدفق نشط');
}
}
مثال عملي: عدّاد تفاعلي
لنبني عدّاداً غنياً بالميزات يوضح أنماط StatefulWidget:
ودجت عدّاد تفاعلي
class InteractiveCounter extends StatefulWidget {
final int min;
final int max;
final int step;
final ValueChanged<int>? onChanged;
const InteractiveCounter({
super.key,
this.min = 0,
this.max = 100,
this.step = 1,
this.onChanged,
});
@override
State<InteractiveCounter> createState() => _InteractiveCounterState();
}
class _InteractiveCounterState extends State<InteractiveCounter> {
late int _value;
@override
void initState() {
super.initState();
_value = widget.min;
}
void _increment() {
if (_value + widget.step <= widget.max) {
setState(() => _value += widget.step);
widget.onChanged?.call(_value);
}
}
void _decrement() {
if (_value - widget.step >= widget.min) {
setState(() => _value -= widget.step);
widget.onChanged?.call(_value);
}
}
void _reset() {
setState(() => _value = widget.min);
widget.onChanged?.call(_value);
}
@override
Widget build(BuildContext context) {
final progress = (_value - widget.min) / (widget.max - widget.min);
return Card(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'\$_value',
style: TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
color: Color.lerp(Colors.blue, Colors.red, progress),
),
),
const SizedBox(height: 8),
LinearProgressIndicator(value: progress),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton.filled(
onPressed: _value > widget.min ? _decrement : null,
icon: const Icon(Icons.remove),
),
const SizedBox(width: 16),
IconButton.outlined(
onPressed: _reset,
icon: const Icon(Icons.refresh),
),
const SizedBox(width: 16),
IconButton.filled(
onPressed: _value < widget.max ? _increment : null,
icon: const Icon(Icons.add),
),
],
),
],
),
),
);
}
}
مثال عملي: نموذج مع حالة
النماذج هي حالة استخدام كلاسيكية لـ StatefulWidget لأنها تحتاج لتتبع قيم الإدخال وحالة التحقق:
نموذج تسجيل بسيط
class RegistrationForm extends StatefulWidget {
const RegistrationForm({super.key});
@override
State<RegistrationForm> createState() => _RegistrationFormState();
}
class _RegistrationFormState extends State<RegistrationForm> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
final _emailController = TextEditingController();
bool _agreedToTerms = false;
bool _isSubmitting = false;
@override
void dispose() {
_nameController.dispose();
_emailController.dispose();
super.dispose();
}
Future<void> _submit() async {
if (!_formKey.currentState!.validate() || !_agreedToTerms) return;
setState(() => _isSubmitting = true);
// محاكاة استدعاء API
await Future.delayed(const Duration(seconds: 2));
if (!mounted) return;
setState(() => _isSubmitting = false);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('تم التسجيل بنجاح!')),
);
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
controller: _nameController,
decoration: const InputDecoration(
labelText: 'الاسم الكامل',
prefixIcon: Icon(Icons.person),
),
validator: (value) =>
value?.isEmpty ?? true ? 'الاسم مطلوب' : null,
),
const SizedBox(height: 16),
TextFormField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'البريد الإلكتروني',
prefixIcon: Icon(Icons.email),
),
validator: (value) {
if (value?.isEmpty ?? true) return 'البريد الإلكتروني مطلوب';
if (!value!.contains('@')) return 'بريد إلكتروني غير صالح';
return null;
},
),
const SizedBox(height: 16),
CheckboxListTile(
value: _agreedToTerms,
onChanged: (v) => setState(() => _agreedToTerms = v ?? false),
title: const Text('أوافق على الشروط'),
controlAffinity: ListTileControlAffinity.leading,
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _isSubmitting ? null : _submit,
child: _isSubmitting
? const SizedBox(
height: 20, width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('تسجيل'),
),
],
),
),
);
}
}
الملخص
في هذا الدرس، تعلمت:
- StatefulWidget يتكون من فئتين: الودجت (غير قابل للتغيير) والحالة (قابلة للتغيير)
- createState() تُنشئ كائن State القابل للتغيير المرتبط بالودجت
- initState() تعمل مرة واحدة للتهيئة؛ dispose() تعمل مرة واحدة للتنظيف
- didChangeDependencies() تستجيب لتغييرات InheritedWidget وآمنة لاستخدام BuildContext
- didUpdateWidget() تعمل عندما يوفر الأب إعدادات ودجت جديدة
- setState() تُشغّل إعادة البناء — تحقق دائماً من mounted بعد الفجوات غير المتزامنة
- تخلّص دائماً من المتحكمات والاشتراكات والمؤقتات لمنع تسرب الذاكرة