معالجة البيانات غير المتزامنة مع AsyncValue
معالجة البيانات غير المتزامنة مع AsyncValue
من أقوى ميزات Riverpod هي AsyncValue، وهي فئة مختومة (sealed class) تُجسّد بأناقة الحالات الثلاث لأي عملية غير متزامنة: التحميل، والبيانات، والخطأ. بدلاً من إدارة علامات بوليانية مثل isLoading وكائنات أخطاء قابلة للإلغاء يدوياً، تُجبرك AsyncValue على معالجة كل حالة ممكنة بطريقة آمنة النوع وشاملة.
عند جلب البيانات من واجهة REST API أو قراءة ملف أو الاستعلام من قاعدة بيانات، يمكن أن تكون العملية في إحدى ثلاث حالات بالضبط في أي لحظة. تُرمّز AsyncValue هذه الحقيقة مباشرةً في نظام الأنواع، مما يجعل الحالات المستحيلة غير قابلة للتمثيل.
AsyncValue هي فئة مختومة في Riverpod 2.x. لها ثلاثة أنواع فرعية محددة: AsyncLoading، وAsyncData<T>، وAsyncError. عند استخدام مطابقة الأنماط بـ .when()، يمكن لمُترجم Dart التحقق من أنك تعاملت مع الحالات الثلاث.الحالات الثلاث لـ AsyncValue
كل AsyncValue<T> هي بالضبط واحدة مما يلي في أي لحظة:
- AsyncLoading — العملية غير المتزامنة قيد التنفيذ؛ لا توجد بيانات أو خطأ بعد.
- AsyncData<T> — اكتملت العملية بنجاح وتحتوي على قيمة من النوع
T. - AsyncError — فشلت العملية؛ تحتوي على الاستثناء وتتبع المكدس (stack trace).
FutureProvider: جلب البيانات البعيدة
FutureProvider هو أبسط طريقة لتغليف Future في Riverpod. تُعرّفه مرة واحدة على المستوى الأعلى، ويقوم Riverpod تلقائياً بتغليف النتيجة في AsyncValue. يراقبه المستهلكون ويُعيدون البناء كلما انتقلت الحالة بين التحميل والبيانات والخطأ.
مثال FutureProvider — جلب ملف تعريف المستخدم
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
// نموذج المجال
class UserProfile {
final int id;
final String name;
final String email;
const UserProfile({
required this.id,
required this.name,
required this.email,
});
factory UserProfile.fromJson(Map<String, dynamic> json) {
return UserProfile(
id: json['id'] as int,
name: json['name'] as String,
email: json['email'] as String,
);
}
}
// FutureProvider — يُرجع AsyncValue<UserProfile> تلقائياً
final userProfileProvider = FutureProvider.family<UserProfile, int>((ref, userId) async {
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/users/$userId'),
);
if (response.statusCode != 200) {
throw Exception('Failed to load user: HTTP ${response.statusCode}');
}
return UserProfile.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
});
استهلاك AsyncValue مع .when()
الطريقة الأساسية لعرض AsyncValue في واجهة المستخدم هي الدالة .when(). تتطلب استدعاءات راجعة للحالات الثلاث جميعها، مما يضمن أنك لن تنسى أبداً معالجة سيناريوهات التحميل أو الخطأ. هذا هو النمط المعياري في Riverpod للعمليات غير المتزامنة.
استهلاك FutureProvider بـ .when()
class UserProfileScreen extends ConsumerWidget {
final int userId;
const UserProfileScreen({super.key, required this.userId});
@override
Widget build(BuildContext context, WidgetRef ref) {
// مراقبة المزود — يحصل تلقائياً على AsyncValue<UserProfile>
final asyncUser = ref.watch(userProfileProvider(userId));
return Scaffold(
appBar: AppBar(title: const Text('User Profile')),
body: asyncUser.when(
// حالة التحميل — عرض هيكل عظمي أو مؤشر
loading: () => const Center(
child: CircularProgressIndicator(),
),
// حالة الخطأ — رسالة خطأ مناسبة مع إمكانية الإعادة
error: (error, stackTrace) => Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error_outline, size: 48, color: Colors.red),
const SizedBox(height: 16),
Text(
'Failed to load profile:\n$error',
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => ref.invalidate(userProfileProvider(userId)),
child: const Text('Retry'),
),
],
),
),
// حالة البيانات — عرض المحتوى الفعلي
data: (user) => ListView(
padding: const EdgeInsets.all(16),
children: [
CircleAvatar(
radius: 40,
child: Text(
user.name[0].toUpperCase(),
style: const TextStyle(fontSize: 32),
),
),
const SizedBox(height: 16),
ListTile(title: const Text('Name'), subtitle: Text(user.name)),
ListTile(title: const Text('Email'), subtitle: Text(user.email)),
ListTile(title: const Text('ID'), subtitle: Text('${user.id}')),
],
),
),
);
}
}
AsyncNotifier: الحالة غير المتزامنة مع الإجراءات
عندما تحتاج إلى بيانات غير متزامنة وأيضاً القدرة على إجراء تعديلات (إنشاء، تحديث، حذف)، استخدم AsyncNotifier. يمتد من Notifier ويُغلّف حالته في AsyncValue، مما يمنحك تحكماً كاملاً في انتقالات الحالة.
AsyncNotifier — خلاصة المنشورات مع التحديث
class Post {
final int id;
final String title;
final String body;
const Post({required this.id, required this.title, required this.body});
factory Post.fromJson(Map<String, dynamic> json) => Post(
id: json['id'] as int,
title: json['title'] as String,
body: json['body'] as String,
);
}
// AsyncNotifier يُغلّف الحالة تلقائياً في AsyncValue<List<Post>>
class PostsNotifier extends AsyncNotifier<List<Post>> {
@override
Future<List<Post>> build() async {
// هذا Future يُغلَّف تلقائياً في AsyncValue
return _fetchPosts();
}
Future<List<Post>> _fetchPosts() async {
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/posts'),
);
final List<dynamic> json = jsonDecode(response.body) as List<dynamic>;
return json
.map((e) => Post.fromJson(e as Map<String, dynamic>))
.toList();
}
// إجراء: التحديث اليدوي للقائمة
Future<void> refresh() async {
state = const AsyncLoading(); // العودة إلى حالة التحميل
state = await AsyncValue.guard(_fetchPosts);
}
// إجراء: إضافة منشور جديد بشكل تفاؤلي
Future<void> addPost(String title, String body) async {
final previous = state;
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
final newPost = Post(
id: DateTime.now().millisecondsSinceEpoch,
title: title,
body: body,
);
return [newPost, ...previous.valueOrNull ?? []];
});
}
}
final postsProvider = AsyncNotifierProvider<PostsNotifier, List<Post>>(
PostsNotifier.new,
);
AsyncValue.guard() هو مساعد ثابت يُنفّذ استدعاءً راجعاً يُرجع Future ويُغلّف النتيجة في AsyncData عند النجاح أو AsyncError عند الاستثناء. يُلغي هذا الأسلوب كتابة try/catch يدوياً عند تعيين حالة الـ notifier.عرض هياكل التحميل (Loading Skeletons)
شاشة الهيكل العظمي هي واجهة مستخدم بديلة تُحاكي شكل المحتوى الحقيقي أثناء تحميل البيانات. إن استدعاء AsyncValue.loading في Riverpod هو النقطة الطبيعية لتطبيق هذا النمط. استخدم تأثير Shimmer (من حزمة shimmer) أو حاويات رمادية بسيطة للتعبير عن التقدم دون حظر المستخدم بدوّار كامل الشاشة.
CircularProgressIndicator عند كل تحميل. يُنشئ هذا حالة فارغة مُزعجة كاملة الشاشة عند كل انتقال. يُفضَّل استخدام هياكل التحميل أو الاحتفاظ بالبيانات السابقة مرئية (skipLoadingOnRefresh: true في .when()) عند تحديث المحتوى المُحمَّل مسبقاً.متغيرات .whenOrNull() و.maybeWhen()
أحياناً تحتاج فقط للاستجابة لحالة أو حالتين. يوفر Riverpod مساعدات مطابقة أنماط إضافية:
.whenData()— يُحوّل قيمة البيانات فقط؛ تمر حالات التحميل والخطأ دون تغيير..maybeWhen(orElse: ...)— معالجة بعض الحالات مع احتياطي للحالات غير المُعالَجة..whenOrNull()— يُرجعnullللحالات التي لا تعالجها..valueOrNull— خاصية تُرجع قيمة البيانات أوnullإذا كانت في حالة تحميل أو خطأ..isLoading،.hasError،.hasValue— خصائص بوليانية مريحة.
ملخص
AsyncValue هي إجابة Riverpod على مشكلة واجهة المستخدم غير المتزامنة الكلاسيكية: تتبع التحميل والنجاح والفشل بطريقة آمنة النوع وسهلة الاستخدام. استخدم FutureProvider للبيانات غير المتزامنة للقراءة فقط، وAsyncNotifier عندما تحتاج أيضاً لإجراء تعديلات، وطابق دائماً الأنماط بـ .when() لضمان معالجة شاملة لجميع الحالات. اقرن حالة التحميل بهياكل التحميل للحصول على تجربة مستخدم متصقلة تتجنب التبدّل المفاجئ للحالات الفارغة.