التصنيفات
Flutter

استخدام BLoC لتحميل البيانات REST API

يكاد لا يخلو تطبيق من الاتصال بـBackend وجلب البيانات عن طريق الانترنت، وتحتمل هذا الاتصال إحضار البيانات أو الفشل في الوصول لها، سنقوم في هذا الدرس بأستخدام مكتبة BLoC للإدارة الحالة للتطبيق، وفصل Logic عن UI، إذا كانت هذه المرة الأولى التي تسمع فيها عن BLoC. تابع هذه المقالة.

  • سنقوم في هذا المثال بعرض Super Heroes عن طريق رابط API وسنقوم باستخدام عدة مكتبات للتعامل مع البيانات الراجعة. في الكود التالي سنقوم باضافة المكتبات في ملف pubspec.yaml

    dependencies:
      flutter_bloc: ^7.1.0
      equatable: ^2.0.3
      json_annotation: ^4.0.1
      dio: ^4.0.0
    dev_dependencies:
      ...
      build_runner: ^1.10.0
      json_serializable: ^4.0.1
    • flutter_bloc : للتعامل مع الحالة للتطبيق،
    • equatable : يستخدم للمقارنة ويختصر عليك الوقت في كتابة == أو hashCode
    • json_annotation : يستخدم annotation كائنات Dart للتعامل مع JSON
    • json_serializable : يقوم بتوليد اكواد Dart اذا وجد json_annotaion في الكائن.
    • build_runner : يولد ملفات Dart

    لا تنسى استخدام flutter pub get بعد اضافة المكتبات.

    بُنية التطبيق :

    تعتمد بُنية التطبيق بحسب المُبرمج، ولكن في BLoC تكون بنية التطبيق تقريباً مشابهة للصورة القادمة.

سنقوم في البداية بكتابة Model الي راح نحتاجها في التطبيق. في الكود التالي البيانات التي راح نتعامل معها، سنقوم بأخذ البيانات الأساسية الاسم والرقم وبعض الخصائص.

{
  "id": 1,
  "name": "A-Bomb",
  "slug": "1-a-bomb",
  "powerstats": {
    "intelligence": 38,
    "strength": 100,
    "speed": 17,
    "durability": 80,
    "power": 24,
    "combat": 64
  },
  "appearance": {
    "gender": "Male",
    "race": "Human",
    "height": [
      "6'8",
      "203 cm"
    ],
    "weight": [
      "980 lb",
      "441 kg"
    ],
    "eyeColor": "Yellow",
    "hairColor": "No Hair"
  },
  "biography": {
    "fullName": "Richard Milhouse Jones",
    "alterEgos": "No alter egos found.",
    "aliases": [
      "Rick Jones"
    ],
    "placeOfBirth": "Scarsdale, Arizona",
    "firstAppearance": "Hulk Vol 2 #2 (April, 2008) (as A-Bomb)",
    "publisher": "Marvel Comics",
    "alignment": "good"
  },
  "work": {
    "occupation": "Musician, adventurer, author; formerly talk show host",
    "base": "-"
  },
  "connections": {
    "groupAffiliation": "Hulk Family; Excelsior (sponsor), Avengers (honorary member); formerly partner of the Hulk, Captain America and Captain Marvel; Teen Brigade; ally of Rom",
    "relatives": "Marlo Chandler-Jones (wife); Polly (aunt); Mrs. Chandler (mother-in-law); Keith Chandler, Ray Chandler, three unidentified others (brothers-in-law); unidentified father (deceased); Jackie Shorr (alleged mother; unconfirmed)"
  },
  "images": {
    "xs": "https://cdn.jsdelivr.net/gh/akabab/superhero-api@0.3.0/api/images/xs/1-a-bomb.jpg",
    "sm": "https://cdn.jsdelivr.net/gh/akabab/superhero-api@0.3.0/api/images/sm/1-a-bomb.jpg",
    "md": "https://cdn.jsdelivr.net/gh/akabab/superhero-api@0.3.0/api/images/md/1-a-bomb.jpg",
    "lg": "https://cdn.jsdelivr.net/gh/akabab/superhero-api@0.3.0/api/images/lg/1-a-bomb.jpg"
  }
}

الكائنات Model :

سنقوم بإنشاء تقريباً أربع كائنات للتعامل معها.

import 'package:json_annotation/json_annotation.dart';

part 'power_stats.g.dart';

@JsonSerializable(explicitToJson: true)
class PowerStats extends Equatable {
  final int? intelligence;
  final int? strength;
  final int? speed;
  final int? durability;
  final int? power;
  final int? combat;

  PowerStats(this.intelligence, this.strength, this.speed, this.durability,
      this.power, this.combat);

  factory PowerStats.fromJson(Map<String, dynamic> json) =>
      _$PowerStatsFromJson(json);

  Map<String, dynamic> toJson() => _$PowerStatsToJson(this);

  @override
  // TODO: implement props
  List<Object?> get props =>
      [intelligence, strength, speed, durability, power, combat];
}

سنقوم بكتابة الكلاس بالشكل هذا، ستظهر لك بعض الأخطاء وذلك بسبب ان بعض الاكواد لم يتم توليدها أو إنشاؤها باستخدام builder.

قم بتشغيل الأمر التالي في مسار التطبيق، سيقوم بتوليد بعض الملفات الجديدة في مجلد Model. بعد تفعيل هذا الامر ستختفي جميع الأخطاء الموجودة سابقًا.

 flutter packages pub run build_runner build 
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:riverpod_future/model/power_stats.dart';

part 'super_hero.g.dart';

@JsonSerializable(explicitToJson: true)
class SuperHero extends Equatable {
  final int id;
  final String name;
  final String slug;
  @JsonKey(name: 'powerstats')
  final PowerStats powerStats;
  final Appearance appearance;
  final Images images;
  SuperHero(this.id, this.name, this.slug, this.powerStats, this.appearance,
      this.images);

  factory SuperHero.fromJson(Map<String, dynamic> json) =>
      _$SuperHeroFromJson(json);

  Map<String, dynamic> toJson() => _$SuperHeroToJson(this);

  @override
  List<Object?> get props => [id, name, slug];
}

تابع بقية Models في ملف الدرس على Github. كانت لمحة سريعة عن json_annotation وكيف يساعدنا في العمل.

الـRepository:

سنقوم بإنشاء Class للاتصال بالانترنت وجلب البيانات، سنقوم باستخدام مكتبة dio تكلمنا فى درس سابقًا في طريقة التعامل معها،

سنقوم بإنشاء abstract لجميع الدوال التي نريد استخدامها، بعد ذلك سنقوم بعمل implement لكلاس الاتصال.

import 'package:dio/dio.dart';

abstract class ISuperHeroRepository {
  Future<List<SuperHero>> getSuperheros();
}

class SuperHeroRepository implements ISuperHeroRepository {
  final _dioClient = Dio();
  final url =
      'https://cdn.jsdelivr.net/gh/akabab/superhero-api@0.3.0/api/all.json';

  @override
  Future<List<SuperHero>> getSuperheros() async {
    try {
      final result = await _dioClient.get(url);
      if (result.statusCode == 200) {
        return (result.data as List).map((i) => SuperHero.fromJson(i)).toList();
      } else {
        throw Exception();
      }
    } catch (_) {
      print(_.toString());
      throw Exception();
    }
  }
}

في الكود السابق يوجد لدينا Class للدوال في تطبيقنا و Class آخر فيه implements للكلاس الأول. بعد ذلك عرفنا Dio للإتصال بالإنترنت ودالة getSuperHeros ستقوم بالاتصال وإحضار البيانات وفي حالة الفشل سيقوم التطبيق بعمل Exception.

ليش نستخدم كلاس للدوال وبعدين نسوي له implements لكلاس ثاني! ليش مانكتب الاتصال على طول بدون مانعيد نفسنا 🤔 !

التطبيق عندنا يعتبر بسيط وسهل! ولكن في التطبيقات الكبيرة تحتاج تسوي Testing وهذا يساعدك في اضافة جميع الدوال في تطبيقك بدون ما تتذكرها او تضيع وقتك عليها. ايضًا مُفيدة إذا تطبيقك يدعم الاتصال بالإنترنت وفي نفس الوقت يشتغل Offline فيكون اضافة الميزات أسهل وأسرع.

إنشاء BLoC :

إذا اطلعت على الدرس السابق ستجد أن BLoC يحتاج الى إنشاء State و Event و ايضاً BLoC للتعامل مع الكلاسات السابقة. حقيقة المتعب هو كتابة الحالات والـEvents للتطبيق. سنبدأ بكتابة Event والذي سيقوم بتحميل البيانات من الرابط.

Events :
part of 'super_hero_bloc.dart';

abstract class SuperHeroEvent extends Equatable {
  const SuperHeroEvent();
}

class FetchSuperHero extends SuperHeroEvent {
  @override
  List<Object?> get props => [];
}

كلاس بسيط مهمته يقوم باعطاء BLoC امر بتحميل البيانات، سنقوم في كلاس BLoC بشرح كيف نفرق بين Events ونقوم بتحميل البيانات بناءًا عليها.

States

سنقوم بتعريف 4 حالات :

  • Initial : هذي هي الحالة الافتراضية او البدائية لـBLoC.
  • Loading : هنا يقوم ببداية تحميل البيانات.
  • Success : هنا في حال نجحت عملية تحميل البيانات.
  • Failure : هنا في حال فشل تحميل البيانات،
part of 'super_hero_bloc.dart';


abstract class SuperHeroState extends Equatable {
  const SuperHeroState();
}

class SuperHeroInitial extends SuperHeroState {
  @override
  List<Object> get props => [];
}

class SuperHeroLoading extends SuperHeroState {
  @override
  List<Object> get props => [];
}

class SuperHeroFailure extends SuperHeroState {
  @override
  List<Object> get props => [];
}

class SuperHeroSuccess extends SuperHeroState {
  const SuperHeroSuccess(this.superHeroes);

  final List<SuperHero> superHeroes;

  @override
  List<Object> get props => [superHeroes];
}

تلاحظ جميع Body للكلاسات متشابهة المتغير فقط اسماء الكلاسات، ماعدا كلاس SuperHeroSuccess مختلف يحتوي على List<SuprtHero> لأن في حال نجح تحميل البيانات المتوقع أن يقوم BLoC بإعادة البيانات إلى واجهة المستخدم.

BLoC:

هنا نقوم بالتعامل مع State والـEvents من حيث التعرف على Event المرسل وايضاً إعادة الحالة المتوقعة من الاربع حالات السابقة.

import 'dart:async';

import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';

part 'super_hero_event.dart';
part 'super_hero_state.dart';

class SuperHeroBloc extends Bloc<SuperHeroEvent, SuperHeroState> {
  SuperHeroBloc(this._superHeroRepository) : super(SuperHeroInitial());
  final SuperHeroRepository _superHeroRepository;

  @override
  Stream<SuperHeroState> mapEventToState(
    SuperHeroEvent event,
  ) async* {
    if (event is FetchSuperHero) {
      try {
        yield SuperHeroLoading();
        var superHeroesList = await _superHeroRepository.getSuperheros();
        yield SuperHeroSuccess(superHeroesList);
      } catch (_) {
        yield SuperHeroFailure();
      }
    }
  }
}

نلاحظ في السطر العاشر اضفنا Repository الى Constrcture الخاص بالـBLoC لاننا نريد الوصول الى دالة getSuperHeros ، بعد ذلك اضفنا if statment للتأكد ان ِEvent المرسل لنا هو FetchSuperHero ، في هذه الحالة نبدأ في تحميل البيانات اذ تم تحميل البيانات سنقوم باعادة حالة النجاح مع البيانات `yield SuperHeroSuccess(superHeroesList);` في حال الفشل سنقوم بارسال انه تم فشل تحميل البيانات، انتهينا من الجزء الخاص بـBLoC، لكن كيف نقوم بايصال هذه البيانات الى UI او واجهة المستخدم٫

واجهة المستخدم:

RepositoryProvider:

الجميل ان الجزء الصعب عدى، الان نحتاج فقط نضيف Widgets مثل اي Widgets في Flutter للتعامل مع BLoC. في البداية سنقوم بإضافة Repository في بداية التطبيق حتى يكون سهل التواصل معه من أي BLoC لديك، بحكم ان اغلب التطبيقات يكون لها Repository واحد. هذا يسهل عليك Unit Test وايضًا يخلي التواصل سهل بين BLoC والـData Provider.

class SuperHeroesApp extends StatelessWidget {
  const SuperHeroesApp(
      {Key? key, required SuperHeroRepository superHeroRepository})
      : _superHeroRepository = superHeroRepository,
        super(key: key);

  final SuperHeroRepository _superHeroRepository;

  @override
  Widget build(BuildContext context) {
    return RepositoryProvider.value(
      value: _superHeroRepository,
      child: SuperHeroesAppView(),
    );
  }
}

قمنا بإضافة MaterialApp في داخل RepositoryProvider حتى يتم التواصل معه من أي مكان في التطبيق. الآن سنتقل الى شاشة عرض البيانات ونشوف كيف نقدر نوصل للـRepository ونرسله للـBLoC.

BlocProvider
class SuperHeroPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => SuperHeroBloc(context.read<SuperHeroRepository>())
        ..add(FetchSuperHero()),
      child: SuperHeroView(),
    );
  }
}

استخدمنا BlocProvider وأضفنا له create الي هو BLoC الي قمنا بكتابته سابقٍا، وفي قمنا بارسال Repository لـBLoC باستخدام context.read ، وطلبنا منه تنفيذ Event تحميل البيانات FetchSuperHero. و child يقوم بعرض البيانات.

BlocBuilder

ايضا Widget عادي يقوم باخذ BLoC والـState وبناء على State الراجعة يقوم بعرض البيانات على الشاشة ، استخدت if statement للتفريق بين الحالات وعرضها للمستخدم.

class SuperHeroView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Super Heroes"),
      ),
      body: BlocBuilder<SuperHeroBloc, SuperHeroState>(
        builder: (context, state) {
          if (state is SuperHeroLoading) {
            return Center(child: SuperHeroesLoading());
          }
          if (state is SuperHeroFailure) {
            return CircularProgressIndicator();
          }
          if (state is SuperHeroSuccess) {
            var list = state.superHeroes;
            return SuperHeroResult(list: list);
          }
          return const Center(child: CircularProgressIndicator());
        },
      ),
    );
  }
}

في حال بداية التحميل يقوم بعرض ProgressIndicator للمستخدم ينبه ان البيانات قيد التحميل، اذا نجح التحميل سنتوصل للبيانات من خلال SuperHeroSuccess، وفي حال فشل تحميل البيانات سيعرض للمستخدم رسالة خطأ،

اذا وجدت ان هناك الكثير لكتابته في BLoC يمكنك استخدام Extension المتوفرة لـVS Code او Plugin المتوفر لأندرويد ستديو، بالإضافة الآن BLoC لديه نسخة مخففة منه اسمها Cubit متوفرة ايضًا من ضمن المكتبة، اتمنى اني وفقت في الشرح و القاكم في درس قادم. ايضا اطلع على هذا الدرس ربما يكون مفيد لك.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني.