مقدمة
«الحالة» هي أي بيانات قد تتغير وتؤثر في ما يُعرض: نص في حقل، قائمة مواضيع، هل المستخدم مسجل الدخول. معظم أخطاء الواجهات سببها حالة في المكان الخطأ: مكررة في موضعين فتتناقض، أو مخفية داخل عنصر يحتاجها غيره. قبل اختيار مكتبة، افهم أنواع الحالة.
أهداف التعلم
- التمييز بين الحالة المؤقتة (Ephemeral) وحالة التطبيق.
- رفع الحالة (Lifting state up) إلى أقرب أب مشترك.
- تمثيل الحالة غير المتزامنة (تحميل/نجاح/خطأ).
- معرفة متى تحتاج مكتبة مثل Riverpod أو Bloc.
1. الحالة المؤقتة: ابقها محلية
ما إذا كانت قائمة منسدلة مفتوحة، أو موضع التبويب الحالي — لا يهم أي جزء آخر من التطبيق. StatefulWidget كافٍ تماماً:
class LikeButton extends StatefulWidget {
const LikeButton({super.key});
@override
State<LikeButton> createState() => _LikeButtonState();
}
class _LikeButtonState extends State<LikeButton> {
bool liked = false;
@override
Widget build(BuildContext context) => IconButton(
icon: Icon(liked ? Icons.favorite : Icons.favorite_border),
onPressed: () => setState(() => liked = !liked),
);
}
(في تطبيق حقيقي حالة الإعجاب تُحفظ على الخادم، فهي ليست مؤقتة — وهذا بالضبط سؤال التصنيف.)
2. رفع الحالة
إذا احتاج عنصران الحالة نفسها، انقلها إلى أقرب أب مشترك ومررها للأسفل مع دوال التغيير. مثال: عداد السلة في الشريط العلوي وزر «أضف للسلة» في الصفحة يحتاجان الحالة نفسها.
3. الحالة غير المتزامنة: ثلاث حالات دائماً
كل بيانات من الشبكة لها ثلاث حالات على الأقل: تحميل، نجاح، خطأ. مثّلها صراحة:
sealed class LoadState<T> {}
class Loading<T> extends LoadState<T> {}
class Loaded<T> extends LoadState<T> { Loaded(this.data); final T data; }
class Failed<T> extends LoadState<T> { Failed(this.message); final String message; }
Widget build(BuildContext context) => switch (state) {
Loading() => const Center(child: CircularProgressIndicator()),
Loaded(:final data) => TopicList(topics: data),
Failed(:final message) => ErrorView(message: message, onRetry: reload),
};
sealed يجعل المترجم يحذرك إن نسيت حالة.
4. حالة التطبيق مع Riverpod
حين تتشارك شاشات كثيرة حالة (المستخدم الحالي، الإعدادات، بيانات مخزنة مؤقتاً)، تحتاج مكاناً واحداً لها:
final topicsProvider = FutureProvider<List<Topic>>((ref) async {
final repo = ref.watch(topicsRepositoryProvider);
return repo.latest();
});
class TopicsPage extends ConsumerWidget {
const TopicsPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final topics = ref.watch(topicsProvider);
return topics.when(
loading: () => const CircularProgressIndicator(),
error: (e, _) => ErrorView(message: 'تعذر التحميل', onRetry: () => ref.invalidate(topicsProvider)),
data: (list) => TopicList(topics: list),
);
}
}
نفس المبدأ في Jetpack Compose: remember للحالة المحلية، وViewModel مع StateFlow لحالة الشاشة.
5. متى تحتاج مكتبة؟
- تطبيق صغير بشاشات قليلة:
setStateورفع الحالة يكفيان. - حالة مشتركة بين شاشات كثيرة، أو تخزين مؤقت، أو اختبارات للمنطق: مكتبة مثل Riverpod أو Bloc توفر عليك الكثير.
- لا تختر المكتبة لأنها «رائجة»؛ اختر واحدة واثبت عليها في المشروع.
تمرين عملي
- ابنِ شاشة قائمة مواضيع بالحالات الثلاث (تحميل/نجاح/خطأ) مع زر إعادة المحاولة.
- أضف شارة عدد في الشريط العلوي تعتمد على الحالة نفسها، وارفع الحالة حتى تتزامن.
- اكتب اختباراً يحاكي فشل المستودع ويتأكد من ظهور زر إعادة المحاولة.
أخطاء شائعة
- نسخ الحالة نفسها في مكانين فتتعارض.
- حالة مشتركة على مستوى التطبيق لأشياء مؤقتة (تضخم وإعادة رسم غير لازمة).
- تجاهل حالة الخطأ وترك المستخدم أمام مؤشر تحميل لا ينتهي.
- تعديل كائن الحالة مباشرة بدل إنشاء نسخة جديدة، فلا يُعاد الرسم.
الخلاصة
صنّف الحالة أولاً: مؤقتة تبقى محلية، ومشتركة تُرفع أو توضع في مخزن واحد. مثّل البيانات غير المتزامنة بحالاتها الثلاث صراحة، واختر المكتبة على قدر الحاجة.
مصادر مقترحة
- توثيق Flutter: «State management» و«Differentiate between ephemeral state and app state».
- توثيق Riverpod الرسمي.
- Android Developers: «State and Jetpack Compose».