بحث باسم الموضوع المطلوب

كيفية إنشاء عملاء API في Flutter

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


واجهة برمجة تطبيقات JSONPlaceholder

في هذا البرنامج التعليمي، سنعمل مع واجهة برمجة التطبيقات JSONPlaceholder https://jsonplaceholder.typicode.com/ . توفر هذه الواجهة طرقًا وهمية باستخدام جميع طرق HTTP الممكنة. كائنات البيانات التي سنعمل معها مرتبطة بالمدونة مثل المنشورات والتعليقات. في الجدول أدناه، يمكنك رؤية جميع الطرق التي سنستخدمها أثناء هذا البرنامج التعليمي.

يحصل /المشاركات/1  https://jsonplaceholder.typicode.com/posts/1
يحصل /المشاركات/1/التعليقات  https://jsonplaceholder.typicode.com/posts/1/comments
يحصل /التعليقات؟postId=1  https://jsonplaceholder.typicode.com/comments?postId=1
بريد /دعامات
يضع /المشاركات/1
يمسح /المشاركات/1

تثبيت الحزم لإنشاء عملاء API في Flutter

لكي نتمكن من إنشاء عملاء API، نحتاج إلى تثبيت حزمة https://pub.dev/packages/chopper Chopper مع حزمة https://pub.dev/packages/chopper_generator Chopper Generator و Build Runner https://pub.dev/packages/build_runner . ويمكن القيام بذلك من خلال تنفيذ الأمر التالي داخل مشروعك:

flutter pub add chopper && flutter pub add --dev build_runner chopper_generator

بعد تنفيذ الأمر، تحقق من ملفك  pubspec.yaml بحثًا عن التبعيات المضافة. يجب أن ترى حزم Chopper وChopper Generator وBuild Runner المضمنة في  dependenciesو dev_dependencies، كما هو موضح أدناه:

dependencies:
  chopper: ^8.0.0

dev_dependencies:
  build_runner: ^2.4.9
  chopper_generator: ^8.0.0

إنشاء خدمة Chopper الأولى (عميل API)

بعد تثبيت كافة الحزم، يمكننا البدء بإنشاء فئة مجردة تعمل على توسيع ChopperServiceالفئة. يتم استخدام خدمة Chopper لتحديد عنوان URL الأساسي وجميع المسارات المقابلة.

كما ذكرنا سابقًا في هذا البرنامج التعليمي ، سنعمل مع بيانات متعلقة بالمدونة. لذا سننشئ فئة مجردة . داخل هذهPostService الفئة المجردة، سنضع جميع الطلبات اللازمة لعرض المنشورات وإضافتها وتحديثها وحذفها .
import 'package:chopper/chopper.dart';

part 'post_service.chopper.dart';

@ChopperApi(baseUrl: '/posts')
abstract class PostService extends ChopperService {
  static PostService create([ChopperClient? client]) =>
      _$PostService(client);

  @Get()
  Future<Response> posts();

  @Get(path: '/{id}')
  Future<Response> post(@Path() int id);

  @Post()
  Future<Response> add(@Body() Map<String, dynamic> json);

  @Put(path: '/{id}')
  Future<Response> update(@Path() int id, @Body() Map<String, dynamic> json);

  @Delete(path: '/{id}')
  Future<Response> delete(@Path() int id);
}

في مقتطف التعليمات البرمجية أعلاه، نبدأ بإنشاء PostServiceالفصل. يمتد هذا الفصل إلى ChopperServiceالفصل ولديه createوظيفة ثابتة لإنشاء الخدمة. في الجزء العلوي من الفصل، نستخدم الشرح ChopperApi()التوضيحي لتمرير baseUrlهذه الخدمة وهي /posts. وهذا يعني في هذه الحالة أن كل مسار سيبدأ بـ /posts.

داخل الخدمة، نقوم بتعريف 5 وظائف:

  1. posts:لإرجاع كافة المشاركات.
  2. post: الذي يأخذ idمعلمة ويعيد منشورًا واحدًا.
  3. add: والذي يتطلب jsonنصًا وسيُحاكي إضافة منشور.
  4. update: الذي يأخذ idمعلمة ويتطلب jsonنصًا وسيعمل على محاكاة تحديث المنشور.
  5. delete: والذي يأخذ idمعلمة وسيقوم بمحاكاة حذف المنشور.  
كما ترى، يتم تعريف طرق HTTP باستخدام التعليقات التوضيحية مثل @get()و @post()و @put()وفوق @delete()الوظائف. يمكن أن تحتوي التعليقات التوضيحية على pathمعلمة يمكن استخدامها لإضافة مسار الطلب. في هذه الحالة، يتم استخدامها لإضافة ديناميكيًا idإلى المسارات. داخل الوظائف addو updateلدينا أيضًا @Body()معلمة سيتم استخدامها لتمرير البيانات للمنشور.



إنشاء post_service.chopper.dart

في سطر 3التعليمات البرمجية، قمنا بتضمين توجيه الجزء الذي يشير إلى post_service.chopper.dartالملف. سيتم إنشاء هذا الملف بواسطة حزمة Chopper. داخل هذا الملف، ستستخدم Chopper جميع الوظائف المحددة لخدمتنا لإنشاء طلبات واجهة برمجة التطبيقات. لإنشاء الملف، ما عليك سوى تنفيذ الأمر التالي داخل مشروعنا:



dart run build_runner build --delete-conflicting-outputs
ليس من الضروري أن يكون العلم  --delete-conflicting-ouputs ضروريًا، ولكنه يضمن الكتابة فوق الملفات التي تم إنشاؤها.

إنشاء عميل Chopper

بعد إنشاء ملفنا PostService، يمكننا المتابعة بإنشاء مثيل لملف ChopperClient. ChopperClientيحتوي ملف . على السمة المطلوبة baseUrlالتي سنقوم بتعيينها لعنوان URL الخاص بواجهة برمجة التطبيقات الخاصة بنا: https://jsonplaceholder.typicode.com . كما أنه يأخذ قائمة ChopperServiceبالمثيلات.

import 'package:chopper/chopper.dart';

import 'package:chopper_client_generator/post_service.dart';


void main() async {

  final chopper = ChopperClient(

    baseUrl: Uri.parse('https://jsonplaceholder.typicode.com'),

    services: [

      PostService.create(),

    ],

  );


  final postService = chopper.getService<PostService>();


  try {

    final response = await postService.posts();


    if (response.isSuccessful) {

      final body = response.body;


      print(body);

    } else {

      throw Exception(response.error);

    }

  } catch (error) {

    print(error);

  }

}


في المثال أعلاه، قمنا بإنشاء مثيل من ChopperClient وحفظه في chopperالمتغير. داخل العميل، قمنا بتحديد عنوان URL الأساسي الخاص بنا وأضفنا PostService.


بعد ذلك، نستخدم chopperالمتغير للوصول إلى PostServiceالدالة getService. في الخدمة، يمكننا استدعاء جميع مساراتنا. يمكنك أن ترى أننا قمنا بتغليف استدعاء واجهة برمجة التطبيقات باستخدام try catch لالتقاط الاستثناءات المحتملة.


بخلاف ذلك، ستعيد مكالمة واجهة برمجة التطبيقات استجابة. في الاستجابة، لدينا إمكانية الوصول إلى isSuccessfulgetter is، وسيتحقق هذا getter مما إذا statusCodeكانت قيمة الاستجابة أكبر أو تساوي 200أو أصغر من 300.


إذا كانت الاستجابة ناجحة فسوف نقوم بطباعة النص وإلا فسوف نلقي الخطأ.


الاتصال بخدمة الحصول على المنشورات وطلبات النشر

الآن عندما نقوم بتشغيل mainوظيفتنا نحصل على النتيجة التالية من واجهة برمجة التطبيقات داخل محطتنا:

[

  {

    "userId": 1,

    "id": 1,

    "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",

    "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"

  },

  {

    "userId": 1,

    "id": 2,

    "title": "qui est esse",

    "body": "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla"

  },

]

كما ترى فإن الرد الذي تلقيناه عبارة عن قائمة من كائنات JSON المنشورة.


إذا أردنا تقديم طلب مختلف، يتعين علينا فقط تغيير الوظيفة التي نستدعيها على postService.

import 'package:chopper/chopper.dart';

import 'package:chopper_client_generator/post_service.dart';


void main() async {

  final chopper = ChopperClient(

    baseUrl: Uri.parse('https://jsonplaceholder.typicode.com'),

    services: [

      PostService.create(),

    ],

  );


  final postService = chopper.getService<PostService>();


  try {

    final response = await postService.post(2);


    if (response.isSuccessful) {

      final body = response.body;


      print(body);

    } else {

      throw Exception(response.error);

    }

  } catch (error) {

    print(error);

  }

}

في الكود أعلاه، قمنا بتغيير الدالة التي نستدعيها إلى postService. postتأخذ postالدالة idمعلمة وستعيد المنشور الذي يحتوي على المطابقة id:


[

  {

    "userId": 1,

    "id": 1,

    "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",

    "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"

  },

]


بالطبع، يمكنك استدعاء عدد لا حصر له من الطلبات المختلفة على النظام postServiceكما تريد. ومع ذلك، تأكد من تضمين طلباتك دائمًا في أمر try-catch، في حالة حدوث خطأ ما في الواجهة الخلفية.


استدعاء طلبات الإضافة والتحديث والحذف

لاستدعاء addالطلبات update، نحتاج إلى إجراء بعض التغييرات الإضافية لضمان تحويل نص استجابتنا بشكل صحيح إلى JSON.


import 'package:chopper/chopper.dart';

import 'package:chopper_client_generator/post_service.dart';


void main() async {

  final chopper = ChopperClient(

    baseUrl: Uri.parse('https://jsonplaceholder.typicode.com'),

    converter: const JsonConverter(),

    errorConverter: const JsonConverter(),

    services: [

      PostService.create(),

    ],

  );


  final postService = chopper.getService<PostService>();


  try {

    final response = await postService.add({

      'title': 'foo',

      'body': 'bar',

      'userId': 1,

    });


    if (response.isSuccessful) {

      final body = response.body;


      print(body);

    } else {

      throw Exception(response.error);

    }

  } catch (error) {

    print(error);

  }

}

في مقتطف التعليمات البرمجية هذا، أضفنا معلمة converterand errorConverterإلى مثيلنا ChopperClient. توفر حزمة Chopper فئة JsonConverterيمكن استخدامها لتحويل JSON. errorConverterلا تكون المعلمة ضرورية في هذه الحالة. ومع ذلك، أريد فقط أن أوضح لك أنه يمكنك أيضًا تحويل أخطائك إلى JSON.

{title: foo, body: bar, userId: 1, id: 101}


كما هو موضح من قبل، تتطلب addوظيفة الفصل PostServiceنصًا ونوفر نص JSON https://jsonplaceholder.typicode.com/guide/ الذي تتوقعه واجهة برمجة التطبيقات JSONPlaceholder الخاصة بنا . عندما نقوم بتشغيل الكود الخاص بنا، سترى أننا نتلقى الاستجابة التالية:

 

لتحديث منشور، يمكننا استدعاء updateالوظيفة باستخدام idالمنشور الذي نريد تعديله وجسم JSON بالقيم المتغيرة.
final response = await postService.update(1, {
  'title': 'foo',
  'body': 'bar',
  'userId': 1,
});

بعد تشغيل هذا الكود سوف ترى نص الاستجابة التالي داخل محطتك:

{title: foo, body: bar, userId: 1, id: 1}

وأخيرًا، يمكننا أيضًا حذف منشور عن طريق استدعاء deleteالدالة مع idالمنشور الذي نريد حذفه.
final response = await postService.delete(1);

إذا قمنا باستدعاء الوظيفة أعلاه فسوف نستقبل كائنًا فارغًا من نظامنا الخلفي.

{}
نظرًا لأننا نستخدم واجهة برمجة تطبيقات وهمية، فإننا لا نضيف أو نحدّث أو نحذف المنشورات فعليًا، ومع ذلك، نتلقى استجابة مماثلة من الواجهة الخلفية كما تفعل مع واجهة برمجة التطبيقات الفعلية.
حل المسار ومعلمات الاستعلام

لقد استخدمنا بالفعل pathمعلمة ملاحظة الطلب لإضافة idإلى عنوان URL. ولكن يمكنك أيضًا استخدام pathالمعلمة لتعديل عنوان URL بشكل أكبر. ليس هذا فحسب، بل يمكننا أيضًا إضافة معلمات الاستعلام إلى عنوان URL.

import 'package:chopper/chopper.dart';

part 'post_service.chopper.dart';

@ChopperApi(baseUrl: '/posts')
abstract class PostService extends ChopperService {
  static PostService create([ChopperClient? client]) =>
      _$PostService(client);

  @Get()
  Future<Response> posts();

  @Get(path: '/{id}')
  Future<Response> post(@Path() int id);

  @Post()
  Future<Response> add(@Body() Map<String, dynamic> json);

  @Put(path: '/{id}')
  Future<Response> update(@Path() int id, @Body() Map<String, dynamic> json);

  @Delete(path: '/{id}')
  Future<Response> delete(@Path() int id);

  @Get(path: '/{id}/comments')
  Future<Response> comments(@Path() int id);

  @Get(path: 'https://jsonplaceholder.typicode.com/comments')
  Future<Response> commentsWithQuery({@Query() int postId = 1});
}

في المثال أعلاه، قمنا بإنشاء الدالتين commentsو commentsWithQuery. أضفنا لاحقة إلى عنوان URL للمسار commentsللتأكد /commentsمن أننا نسترد التعليقات من الواجهة الخلفية. commentsتأخذ الدالتان أيضًا اللاحقة idمن المنشور.

بالنسبة commentsWithQueryللوظيفة، نلغي المسار الكامل لأن عنوان URL لا يتضمن عنوان /postsURL الأساسي. كما أضفنا أيضًا @Query()المعلمة التي ستلحق عنوان URL المحدد في المسار بـ ?postId=1where 1هو id.

تذكر أنه بعد إجراء تغييرات على ملف، ChopperServiceيتعين عليك إعادة إنشاء chopper.dartالملف. ويمكن القيام بذلك عن طريق تنفيذ نفس الأمر كما في السابق:

dart run build_runner build --delete-conflicting-outputs
الآن يمكننا استدعاء commentsالوظيفة على PostService.

final response = await postService.comments(1);
سيعطينا هذا الاستجابة JSON التالية:
[
  {
    "postId": 1,
    "id": 1,
    "name": "id labore ex et quam laborum",
    "email": "Eliseo@gardner.biz",
    "body": "laudantium enim quasi est quidem magnam voluptate ipsam eos\ntempora quo necessitatibus\ndolor quam autem quasi\nreiciendis et nam sapiente accusantium"
  }
]

ويمكننا أيضًا استدعاء commentsWithQueryالوظيفة.

final response = await postService.commentsWithQuery(postId: 5);
وسوف يؤدي هذا إلى ما يلي:
[
  {
    "postId": 5,
    "id": 21,
    "name": "aliquid rerum mollitia qui a consectetur eum sed",
    "email": "Noemie@marques.me",
    "body": "deleniti aut sed molestias explicabo\ncommodi odio ratione nesciunt\nvoluptate doloremque est\nnam autem error delectus"
  }
]

تحديد أنواع الاستجابة في خدمة ما بعد النشر

حتى الآن كنا نعمل مع استجابات JSON البسيطة. ورغم أن هذا النهج يعمل، إلا أنه ليس ملائمًا للغاية داخل قاعدة التعليمات البرمجية الخاصة بنا. والنهج الأفضل هو تحويل JSON إلى كائنات Dart. ومن حسن الحظ أن الحزمة مرنة، فلنلق نظرة على التنفيذ.

إنشاء نموذج تدوينة

أولاً، نريد إنشاء BlogPostنموذج لتحويل المنشورات التي نتلقاها من برنامجنا الخلفي.
class BlogPost {
  int userId;
  int id;
  String title;
  String body;

  BlogPost({
    required this.userId,
    required this.id,
    required this.title,
    required this.body,
  });

  factory BlogPost.fromJson(Map<String, dynamic> json) => BlogPost(
        userId: json["userId"],
        id: json["id"],
        title: json["title"],
        body: json["body"],
      );

  Map<String, dynamic> toJson() => {
        "userId": userId,
        "id": id,
        "title": title,
        "body": body,
      };
}

لقد قمنا بإنشاء BlogPostفئة بنفس السمات التي نتلقاها من واجهة برمجة التطبيقات. لقد أضفنا استدعاءً factoryحتى fromJsonنتمكن من إنشاء مثيل من BlogPostJSON. لقد قمنا أيضًا بإنشاء toJsonالدالة حتى نتمكن من تحويل BlogPostالمثيل إلى JSON.

إذا كنت تتابع الأمر، فتأكد من تسمية الفئة أعلاه BlogPostوليس Post. أوافق على أن Postهذا اسم أفضل. ومع ذلك، سيتعارض مع فئة Post الخاصة بالحزمة ولن يعمل التنفيذ الذي تمت مناقشته في هذا القسم.

الآن بدلاً من إرجاع عادي Response، يمكننا الآن إرجاع a Responseبنوع.


import 'package:chopper/chopper.dart';

import 'package:chopper_client_generator/blog_post.dart';


part 'post_service.chopper.dart';


@ChopperApi(baseUrl: '/posts')

abstract class PostService extends ChopperService {

  static PostService create([ChopperClient? client]) =>

      _$PostService(client);


  @Get()

  Future<Response<List<BlogPost>>> posts();


  @Get(path: '/{id}')

  Future<Response<BlogPost?>> post(@Path() int id);


  @Post()

  Future<Response<BlogPost>> add(@Body() Map<String, dynamic> json);


  @Put(path: '/{id}')

  Future<Response<BlogPost>> update(@Path() int id, @Body() Map<String, dynamic> json);


  @Delete(path: '/{id}')

  Future<Response> delete(@Path() int id);


  @Get(path: '/{id}/comments')

  Future<Response> comments(@Path() int id);


  @Get(path: 'https://jsonplaceholder.typicode.com/comments')

  Future<Response> commentsWithQuery({@Query() int postId = 1});

}

 في مقتطف التعليمات البرمجية أعلاه، بدلاً من إرجاع a فقط، Responseفإننا نرجع a Reponseمع قائمة من BlogPostالكائنات أو a Responseمع كائن واحد BlogPost. بهذه الطريقة، سيتم تحويل ملف JSON المستلم من واجهة برمجة التطبيقات على الفور إلى كائن Dart.


بالطبع، عندما نقوم بإجراء تغييرات على ChopperServiceفئة، يتعين علينا إعادة إنشاء ملف المروحية:

dart run build_runner build --delete-conflicting-outputs

إنشاء JsonSerializationConverter

إن مجرد تغيير أنواع الاستجابة لـ a ChopperServiceليس كافيًا. لأنه كما في السابق، يتعين علينا التأكد من تحويلها بشكل صحيح. لسوء الحظ، فإن استخدام الحزمة JsonConverterلن يؤدي الغرض. ومع ذلك، يمكننا إنشاء المحول التالي الذي سيكون قادرًا على تسلسل البيانات وإلغاء تسلسلها من واجهة برمجة التطبيقات باستخدام كائنات Dart.

import 'dart:async' show FutureOr;


import 'package:chopper/chopper.dart';


typedef JsonFactory<T> = T Function(Map<String, dynamic> json);


class JsonSerializationConverter extends JsonConverter {

  const JsonSerializationConverter(this.factories);


  final Map<Type, JsonFactory> factories;


  T? _decodeMap<T>(Map<String, dynamic> values) {

    final jsonFactory = factories[T];


    if (jsonFactory == null || jsonFactory is! JsonFactory<T>) {

      return null;

    }


    return jsonFactory(values);

  }


  List<T> _decodeList<T>(Iterable values) =>

      values.where((v) => v != null).map<T>((v) => _decode<T>(v)).toList();


  dynamic _decode<T>(entity) {

    if (entity is Iterable) return _decodeList<T>(entity as List);


    if (entity is Map) return _decodeMap<T>(entity as Map<String, dynamic>);


    return entity;

  }


  @override

  FutureOr<Response<ResultType>> convertResponse<ResultType, Item>(

    Response response,

  ) async {

    final jsonResponse = await super.convertResponse(response);


    return jsonResponse.copyWith<ResultType>(

        body: _decode<Item>(jsonResponse.body));

  }

}


في هذه الحالة، JsonSerializationConverterنلغي convertResponseالوظيفة. داخل الإلغاء، نستمر في استدعاء convertResponseوظيفة الفئة الأصلية. ومع ذلك، نغير استجابة الإرجاع لإرجاع قائمة من كائنات Dart أو كائن Dart واحد.


يتم ذلك داخل _decodeالدالة. تحدد هذه الدالة بناءً على ما هو معطى jsonResponse.bodyما إذا كنا نريد إرجاع قائمة من الكائنات أو كائن واحد. بمجرد أن تحدد الدالة ما الذي يجب إرجاعه، ستتحقق داخل السمة factoriesما إذا كانت تستطيع العثور على النوع الصحيح.


في هذه الحالة، يبحث عن BlogPostالنوع. بمجرد العثور على النوع الصحيح، سيحول استجابة JSON إلى القائمة الصحيحة من BlogPostالكائنات أو BlogPostكائن واحد.


تنفيذ JsonSerializationConverter

بعد إنشاء الملف JsonSerializationConverterيجب علينا التأكد من استخداماتنا ChopperClientله.

import 'package:chopper/chopper.dart' show ChopperClient, JsonConverter;

import 'package:chopper_client_generator/blog_post.dart';

import 'package:chopper_client_generator/json_serialization_converter.dart';

import 'package:chopper_client_generator/post_service.dart';


void main() async {

  final chopper = ChopperClient(

    baseUrl: Uri.parse('https://jsonplaceholder.typicode.com'),

    converter: const JsonSerializationConverter({BlogPost: BlogPost.fromJson}),

    errorConverter: const JsonConverter(),

    services: [

      PostService.create(),

    ],

  );


  final postService = chopper.getService<PostService>();


  try {

    final response = await postService.post(5);


    if (response.isSuccessful) {

      final body = response.body;


      print(body);

    } else {

      throw Exception(response.error);

    }

  } catch (error) {

    print(error);

  }

}


في الكود أعلاه، بدلاً من استخدام العادي، JsonConverterنقوم الآن بتمرير مثيل من JsonSerializationConverter. المخصص لدينا. يأخذ المحول المخصص لدينا خريطة من Typeو factoryواستخدمناها لتمرير BlogPostالنوع الخاص بنا ومصنعه.

بعد ذلك، يمكننا تشغيل mainوظيفتنا كما في السابق وسوف ترى أننا حصلنا على الإخراج التالي:

Instance of 'BlogPost'


خاتمة

في هذه المقالة، تعلمت كيفية إنشاء عملاء واجهة برمجة التطبيقات في Flutter باستخدام حزمة Chopper. قد يتطلب الإعداد الأولي بعض الوقت والمعرفة، ولكن بعد الانتهاء منه، يمكنك إعداد اتصال سريع مع الواجهة الخلفية لديك.

لقد تعلمت أيضًا كيفية تحويل بيانات JSON إلى كائنات Dart، مما يجعل التعامل مع البيانات أسهل كثيرًا. وبينما قمنا بتغطية العديد من جوانب حزمة Chopper في هذا المنشور، لا يزال هناك الكثير لنتعلمه. إذا كنت مهتمًا، فلا تتردد في مراجعة وثائقها الرسمية .