دليل شامل لاستخدام نمط Provider في Flutter لإدارة الحالة بكفاءة
مقدمة إلى نمط Provider في Flutter
في عالم تطوير تطبيقات Flutter، تُعد إدارة الحالة (State Management) تحديًا أساسيًا يواجهه المطورون. تتعدد الأنماط والحلول المتاحة، مثل هندسة BLoC، ولكن نمط Provider يبرز كأحد أبسط وأكثر الحلول فعالية وشعبية. على الرغم من أن بعض الأنماط الأخرى قد تستخدم Provider داخليًا، إلا أنه يتميز بسهولة التعلم وقلة التعليمات البرمجية المتكررة (boilerplate code) التي يتطلبها.
في هذا المقال، سنقوم بتحويل تطبيق العداد الافتراضي الذي توفره Flutter، وإعادة هيكلته لاستخدام نمط Provider. هذا سيمكننا من فهم كيفية تطبيق هذا النمط عمليًا والاستفادة من مزاياه. إذا كنت مهتمًا بمعرفة رأي فريق Flutter في Google حول نمط Provider، يمكنك الاطلاع على هذا الحديث من عام 2019.
البدء: تهيئة مشروع Flutter
للبدء، قم بإنشاء مشروع Flutter جديد وامنحه الاسم الذي تفضله. بعد إنشاء المشروع، سنحتاج إلى تنظيف الكود الافتراضي عن طريق إزالة جميع التعليقات للحصول على بيئة عمل نظيفة.
الكود الافتراضي لتطبيق العداد قبل التعديل
إليك كيف سيبدو ملف main.dart بعد إزالة التعليقات:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
إضافة حزمة Provider إلى المشروع
الخطوة التالية هي إضافة اعتمادية (dependency) نمط Provider إلى ملف pubspec.yaml. وقت كتابة هذا المقال، كانت أحدث نسخة هي 4.1.2. تأكد من استخدام أحدث إصدار متاح عند تطبيقك.
name: provider_pattern_explained
description: A new Flutter project.
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: ">=2.7.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
provider: ^4.1.2 # أضف هذا السطر
cupertino_icons: ^0.1.3
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
uses-material-design: true
لاحظ أن التطبيق الافتراضي يعتمد على StatefulWidget، والذي يقوم بإعادة بناء الواجهة (rebuild) في كل مرة تضغط فيها على زر FloatingActionButton (الذي يستدعي setState()). هدفنا الآن هو تحويله إلى StatelessWidget باستخدام Provider.
إنشاء Provider الخاص بنا
لنقم بإنشاء Provider الخاص بنا. سيكون هذا هو المصدر الوحيد للحقيقة (single source of truth) لحالة تطبيقنا. هنا سنقوم بتخزين حالتنا، والتي في هذه الحالة هي قيمة العداد الحالية.
تعريف فئة العداد (Counter Class)
أنشئ فئة جديدة باسم Counter وأضف إليها المتغير _count:
import 'package:flutter/material.dart';
class Counter {
var _count = 0;
}
تحويل فئة العداد إلى Provider
لتحويلها إلى فئة Provider، يجب أن ترث الفئة Counter من ChangeNotifier الموجودة في حزمة material.dart. توفر لنا هذه الفئة الدالة notifyListeners()، والتي ستقوم بإخطار جميع المستمعين (listeners) في كل مرة نغير فيها قيمة.
الآن، أضف دالة لزيادة العداد:
import 'package:flutter/material.dart';
class Counter extends ChangeNotifier {
var _count = 0;
void incrementCounter() {
_count += 1;
}
}
في نهاية دالة incrementCounter()، سنستدعي notifyListeners(). هذا سيؤدي إلى إطلاق تغيير في جميع أنحاء التطبيق لأي widget يستمع إليه. هذه هي روعة نمط Provider في Flutter؛ لا داعي للقلق بشأن الإرسال اليدوي إلى التدفقات (streams).
أخيرًا، أنشئ دالة جالبة (getter) لإرجاع قيمة العداد. سنستخدمها لعرض أحدث قيمة:
import 'package:flutter/material.dart';
class Counter extends ChangeNotifier {
var _count = 0;
int get getCounter {
return _count;
}
void incrementCounter() {
_count += 1;
notifyListeners();
}
}
الاستماع إلى نقرات الأزرار وتحديث الواجهة
الآن بعد أن قمنا بإعداد Provider، يمكننا استخدامه في الـ widget الرئيسي الخاص بنا.
تحويل MyHomePage إلى StatelessWidget
أولًا، لنقم بتحويل MyHomePage إلى StatelessWidget بدلًا من StatefulWidget. سيتعين علينا إزالة استدعاء setState() لأنه متاح فقط في StatefulWidget:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatelessWidget {
final String title;
MyHomePage({this.title});
void _incrementCounter(BuildContext context) {
// سيتم إضافة الكود هنا لاحقًا
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter', // هذه القيمة تحتاج إلى تحديث من Provider
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => _incrementCounter(context),
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
بهذا، يمكننا الآن استخدام نمط Provider في Flutter لتعيين والحصول على قيمة العداد. عند كل نقرة زر، نحتاج إلى زيادة قيمة العداد بواحد. لذا، في دالة _incrementCounter (التي تُستدعى عند الضغط على الزر) أضف هذا السطر:
Provider.of<Counter>(context, listen: false).incrementCounter();
ما يحدث هنا هو أنك طلبت من Flutter البحث في شجرة الـ widget عن أول مكان تم فيه توفير Counter. هذا ما تفعله الدالة Provider.of(). القيم العامة (generics) داخل الأقواس <> تخبر Flutter عن نوع الـ provider الذي يجب البحث عنه. ثم يتنقل Flutter عبر شجرة الـ widget حتى يجد القيمة الموفرة. إذا لم يتم توفير القيمة في أي مكان، يتم إطلاق استثناء.
أخيرًا، بمجرد حصولك على الـ provider، يمكنك استدعاء أي دالة عليه. هنا نستدعي دالة incrementCounter الخاصة بنا. لكننا نحتاج أيضًا إلى context، لذلك نقبل الـ context كوسيط ونعدل دالة onPressed لتمرير الـ context أيضًا:
void _incrementCounter(BuildContext context) {
Provider.of<Counter>(context, listen: false).incrementCounter();
}
ملاحظة: لقد قمنا بتعيين listen إلى false في هذه الحالة لأننا لا نحتاج إلى الاستماع إلى أي قيم هنا. نحن فقط نرسل إجراءً ليتم تنفيذه.
توفير Provider في شجرة الـ Widget
سيبحث نمط Provider في Flutter عن أحدث قيمة موفرة. سيساعدك الرسم البياني أدناه على فهم أفضل لآلية التوفير.

في هذا الرسم البياني، سيكون الكائن الأخضر A متاحًا لبقية العناصر الموجودة أسفله، وهي B, C, D, E, F. الآن لنفترض أننا نريد إضافة بعض الوظائف إلى التطبيق وننشئ provider آخر، Z. الكائن Z مطلوب من قبل E و F. إذن، ما هو أفضل مكان لإضافته؟ يمكننا إضافته إلى الجذر فوق A. هذا سيعمل:

لكن هذه الطريقة ليست فعالة جدًا. سيتعين على Flutter المرور عبر جميع الـ widgets أعلاه ثم أخيرًا الانتقال إلى الجذر. إذا كانت لديك أشجار widget طويلة جدًا – وهو ما سيكون لديك بالتأكيد في تطبيق إنتاجي – فليس من الجيد وضع كل شيء في الجذر.
بدلًا من ذلك، يمكننا النظر إلى القاسم المشترك بين E و F. وهو C. لذا إذا وضعنا Z فوق E و F مباشرةً، فسيعمل ذلك بكفاءة أكبر.

ولكن ماذا لو أردنا إضافة كائن آخر X مطلوب من قبل E و F؟ سنفعل الشيء نفسه. ولكن لاحظ كيف تستمر الشجرة في النمو.

هناك طريقة أفضل لإدارة ذلك. ماذا لو وفرنا جميع الكائنات على مستوى واحد؟

هذا مثالي، وهو كيف سنقوم بتطبيق نمط Provider في Flutter. سنستفيد مما يسمى MultiProvider والذي يتيح لنا الإعلان عن عدة providers على مستوى واحد. سنجعل MultiProvider يغلف الـ widget MaterialApp:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider.value(
value: Counter(),
),
],
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: "AndroidVille Provider Pattern"),
),
);
}
}
بهذا، نكون قد وفرنا الـ provider لشجرة الـ widget الخاصة بنا ويمكننا استخدامه في أي مكان أسفل هذا المستوى في الشجرة.
تحديث قيمة العداد المعروضة
لتحديث النص المعروض، نحصل على الـ provider في دالة build الخاصة بالـ widget MyHomePage. سنستخدم الدالة الجالبة getCounter التي أنشأناها للحصول على أحدث قيمة. ثم نضيف هذه القيمة إلى الـ widget Text أدناه. وهكذا نكون قد انتهينا!
ملف main.dart النهائي
إليك كيف يجب أن يبدو ملف main.dart النهائي:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:provider_pattern_explained/counter.dart'; // تأكد من وجود هذا الملف
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider.value(
value: Counter(),
),
],
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: "AndroidVille Provider Pattern"),
),
);
}
}
class MyHomePage extends StatelessWidget {
final String title;
MyHomePage({this.title});
void _incrementCounter(BuildContext context) {
Provider.of<Counter>(context, listen: false).incrementCounter();
}
@override
Widget build(BuildContext context) {
// هنا نستخدم Provider.of للحصول على قيمة العداد والاستماع للتحديثات
var counter = Provider.of<Counter>(context).getCounter;
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => _incrementCounter(context),
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
ملاحظة: لم نقم بتعيين listen: false في هذه الحالة عند الحصول على قيمة العداد (Provider.of<Counter>(context).getCounter) لأننا نريد الاستماع إلى أي تحديثات في قيمة العداد وعكسها على الواجهة. القيمة الافتراضية لـ listen هي true.
يمكنك الاطلاع على الكود المصدري الكامل للمشروع على GitHub.
الخلاصة التقنية
يُقدم نمط Provider في Flutter حلًا أنيقًا وفعالًا لإدارة الحالة، متفوقًا في بساطته على العديد من الأنماط المعقدة الأخرى. من خلال استخدام ChangeNotifier و MultiProvider، يمكن للمطورين بناء تطبيقات قابلة للتوسع والصيانة بسهولة، مع تقليل التعليمات البرمجية المتكررة وتحسين أداء الواجهة. القدرة على التحكم في الاستماع للتغييرات (عبر listen: false) تمنح مرونة كبيرة في تحسين إعادة بناء الـ widgets، مما يجعله خيارًا ممتازًا للمشاريع بجميع أحجامها.