كيفية التعامل مع البيانات وواجهات API في Flutter؟ دليل خطوة بخطوة

Amine
11/12/2024

العمل مع البيانات وواجهات API في Flutter

في معظم التطبيقات الحديثة، نحتاج إلى التعامل مع البيانات من مصادر مختلفة: سواء كانت من خادم بعيد (API)، أو قاعدة بيانات محلية، أو ملفات. في هذا الدرس، سنتعلم كيفية التعامل مع البيانات بكفاءة في تطبيقات Flutter.

في هذا الدرس سنتعلم:

  • التعامل مع واجهات API الخارجية
  • تخزين البيانات محلياً
  • معالجة البيانات بتنسيق JSON
  • التعامل مع الصور والملفات
  • إدارة حالات التحميل والأخطاء

التعامل مع واجهات API

للتعامل مع واجهات API، نستخدم مكتبة http. أضف المكتبة إلى ملف pubspec.yaml:

dependencies:
  http: ^1.1.0

مثال على استدعاء API:

import 'package:http/http.dart' as http;
import 'dart:convert';
// نموذج البيانات
class User {
  final int id;
  final String name;
  final String email;
  User({
    required this.id,
    required this.name,
    required this.email,
  });
  factory User.fromJson(Map json) {
    return User(
      id: json['id'],
      name: json['name'],
      email: json['email'],
    );
  }
}
// دالة لجلب البيانات
Future fetchUser(int id) async {
  final response = await http.get(
    Uri.parse('https://api.example.com/users/$id'),
  );
  if (response.statusCode == 200) {
    return User.fromJson(jsonDecode(response.body));
  } else {
    throw Exception('فشل في تحميل المستخدم');
  }
}
// استخدام FutureBuilder لعرض البيانات
class UserWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
      future: fetchUser(1),
      builder: (context, snapshot) {
        if (snapshot.hasData) {
          return Column(
            children: [
              Text('الاسم: ${snapshot.data!.name}'),
              Text('البريد: ${snapshot.data!.email}'),
            ],
          );
        } else if (snapshot.hasError) {
          return Text('حدث خطأ: ${snapshot.error}');
        }
        return CircularProgressIndicator();
      },
    );
  }
}

التخزين المحلي

يمكننا استخدام shared_preferences لتخزين البيانات البسيطة، أو sqflite لقاعدة بيانات SQL محلية:

dependencies:
  shared_preferences: ^2.2.0
  sqflite: ^2.3.0

مثال على استخدام SharedPreferences:

import 'package:shared_preferences.dart';
class SettingsService {
  static const String THEME_KEY = 'theme_mode';
  static const String LANGUAGE_KEY = 'language';
  Future saveThemeMode(bool isDark) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setBool(THEME_KEY, isDark);
  }
  Future getThemeMode() async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getBool(THEME_KEY) ?? false;
  }
  Future saveLanguage(String language) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString(LANGUAGE_KEY, language);
  }
  Future getLanguage() async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getString(LANGUAGE_KEY) ?? 'ar';
  }
}

مثال على استخدام SQLite:

import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
class DatabaseHelper {
  static final DatabaseHelper instance = DatabaseHelper._init();
  static Database? _database;
  DatabaseHelper._init();
  Future get database async {
    if (_database != null) return _database!;
    _database = await _initDB('notes.db');
    return _database!;
  }
  Future _initDB(String filePath) async {
    final dbPath = await getDatabasesPath();
    final path = join(dbPath, filePath);
    return await openDatabase(
      path,
      version: 1,
      onCreate: _createDB,
    );
  }
  Future _createDB(Database db, int version) async {
    await db.execute('''
      CREATE TABLE notes (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        title TEXT NOT NULL,
        content TEXT NOT NULL,
        createdAt TEXT NOT NULL
      )
    ''');
  }
  Future createNote(Note note) async {
    final db = await instance.database;
    return await db.insert('notes', note.toJson());
  }
  Future> getAllNotes() async {
    final db = await instance.database;
    final result = await db.query('notes', orderBy: 'createdAt DESC');
    return result.map((json) => Note.fromJson(json)).toList();
  }
  Future updateNote(Note note) async {
    final db = await instance.database;
    return await db.update(
      'notes',
      note.toJson(),
      where: 'id = ?',
      whereArgs: [note.id],
    );
  }
  Future deleteNote(int id) async {
    final db = await instance.database;
    return await db.delete(
      'notes',
      where: 'id = ?',
      whereArgs: [id],
    );
  }
}

التعامل مع الصور والملفات

للتعامل مع الصور والملفات، نستخدم مكتبات مثل image_picker و path_provider:

dependencies:
  image_picker: ^1.0.4
  path_provider: ^2.1.1

مثال على التقاط وحفظ صورة:

import 'package:image_picker/image_picker.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:io';
class ImageService {
  final ImagePicker _picker = ImagePicker();
  Future pickImage() async {
    try {
      final XFile? image = await _picker.pickImage(
        source: ImageSource.gallery,
        maxWidth: 800,
        maxHeight: 800,
        imageQuality: 85,
      );
      if (image == null) return null;
      // نسخ الصورة إلى مجلد التطبيق
      final directory = await getApplicationDocumentsDirectory();
      final name = basename(image.path);
      final File imageFile = File('${directory.path}/$name');
      
      return File(image.path).copy(imageFile.path);
    } catch (e) {
      print('خطأ في التقاط الصورة: $e');
      return null;
    }
  }
  Future saveImage(File image, String fileName) async {
    try {
      final directory = await getApplicationDocumentsDirectory();
      final path = '${directory.path}/$fileName';
      await image.copy(path);
    } catch (e) {
      print('خطأ في حفظ الصورة: $e');
    }
  }
  Future> getLocalImages() async {
    try {
      final directory = await getApplicationDocumentsDirectory();
      final List files = directory.listSync();
      return files
          .whereType()
          .where((file) => 
              file.path.endsWith('.jpg') || 
              file.path.endsWith('.png'))
          .toList();
    } catch (e) {
      print('خطأ في قراءة الصور: $e');
      return [];
    }
  }
}

إدارة حالات التحميل والأخطاء

من المهم التعامل مع حالات التحميل والأخطاء بشكل صحيح. هنا مثال على كيفية تنفيذ ذلك:

// حالات البيانات
enum DataState { initial, loading, success, error }
// نموذج لإدارة حالة البيانات
class DataState {
  final T? data;
  final String? error;
  final bool isLoading;
  DataState({
    this.data,
    this.error,
    this.isLoading = false,
  });
  factory DataState.initial() => DataState();
  
  factory DataState.loading() => DataState(isLoading: true);
  
  factory DataState.success(T data) => DataState(data: data);
  
  factory DataState.error(String message) => DataState(error: message);
}
// مثال على استخدام حالات البيانات مع Provider
class ProductsProvider extends ChangeNotifier {
  DataState> _state = DataState.initial();
  DataState> get state => _state;
  Future fetchProducts() async {
    try {
      _state = DataState.loading();
      notifyListeners();
      final response = await http.get(
        Uri.parse('https://api.example.com/products'),
      );
      if (response.statusCode == 200) {
        final List data = jsonDecode(response.body);
        final products = data
            .map((json) => Product.fromJson(json))
            .toList();
        _state = DataState.success(products);
      } else {
        _state = DataState.error('فشل في تحميل المنتجات');
      }
    } catch (e) {
      _state = DataState.error(e.toString());
    }
    notifyListeners();
  }
}
// استخدام حالات البيانات في الواجهة
class ProductsScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer(
      builder: (context, provider, child) {
        final state = provider.state;
        if (state.isLoading) {
          return Center(child: CircularProgressIndicator());
        }
        if (state.error != null) {
          return Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text('حدث خطأ: ${state.error}'),
                ElevatedButton(
                  onPressed: () => provider.fetchProducts(),
                  child: Text('إعادة المحاولة'),
                ),
              ],
            ),
          );
        }
        if (state.data == null || state.data!.isEmpty) {
          return Center(
            child: Text('لا توجد منتجات'),
          );
        }
        return ListView.builder(
          itemCount: state.data!.length,
          itemBuilder: (context, index) {
            final product = state.data![index];
            return ListTile(
              title: Text(product.name),
              subtitle: Text(product.price.toString()),
            );
          },
        );
      },
    );
  }
}

أفضل الممارسات

  • الفصل بين المسؤوليات: افصل منطق البيانات عن واجهة المستخدم
  • التعامل مع الأخطاء: دائماً قم بمعالجة الأخطاء وعرض رسائل مناسبة للمستخدم
  • التخزين المؤقت: خزن البيانات محلياً لتحسين أداء التطبيق
  • التحقق من الاتصال: تحقق من اتصال الإنترنت قبل طلب البيانات
  • الأمان: لا تخزن البيانات الحساسة بشكل غير مشفر

الخاتمة

التعامل مع البيانات هو جزء أساسي من أي تطبيق حديث. في هذا الدرس، تعلمنا كيفية التعامل مع مختلف مصادر البيانات في Flutter، من واجهات API إلى التخزين المحلي. تذكر دائماً أن تتعامل مع البيانات بشكل آمن وفعال، وأن تقدم تجربة مستخدم جيدة حتى في حالات الفشل.

مشروع تطبيقي:

  • قم بإنشاء تطبيق لعرض الأخبار يستخدم API عام
  • أضف ميزة حفظ المقالات المفضلة محلياً
  • نفذ ميزة البحث والتصفية
  • أضف دعم الوضع غير المتصل

التعليقات

اترك تعليقاً