تعلم كيفية بناء تطبيقات جميلة وسريعة ومتعددة المنصات باستخدام إطار عمل فلاتر من جوجل
فلاتر (Flutter) هو إطار عمل مفتوح المصدر من جوجل لبناء تطبيقات جميلة وسريعة ومتعددة المنصات من قاعدة شيفرة واحدة. يستخدم فلاتر لغة دارت (Dart) لكتابة التطبيقات، وهي لغة برمجة مطورة أيضًا من قبل جوجل.
على عكس معظم أطر عمل تطوير التطبيقات الأخرى، فلاتر لا يستخدم مكونات واجهة المستخدم الأصلية للمنصة، بل يرسم كل بكسل على شاشة الجهاز. يوفر هذا النهج الفريد عدة مزايا:
معمارية فلاتر بشكل مبسط - المصدر: توثيق فلاتر الرسمي
للحصول على أداء متميز، فلاتر يتجنب استخدام جسر JavaScript (كما في React Native) ويجمع التطبيق مباشرة إلى التعليمات البرمجية الأصلية.
قبل البدء في تطوير تطبيقات فلاتر، تحتاج إلى تثبيت مجموعة أدوات فلاتر SDK وإعداد بيئة التطوير الخاصة بك. اتبع الخطوات التالية:
للعمل مع فلاتر، ستحتاج إلى:
قم بتحميل أحدث إصدار من فلاتر SDK من الموقع الرسمي:
اختر نظام التشغيل المناسب واتبع التعليمات الخاصة بالتثبيت.
أضف مجلد flutter/bin
إلى متغير PATH لنظام التشغيل:
أضف السطر التالي إلى ملف $HOME/.bash_profile
أو $HOME/.zshrc
:
export PATH="$PATH:[مسار مجلد فلاتر]/flutter/bin"
بعد ذلك، قم بتنفيذ الأمر التالي لتحديث المتغيرات:
source $HOME/.bash_profile
أضف السطر التالي إلى ملف $HOME/.bashrc
:
export PATH="$PATH:[مسار مجلد فلاتر]/flutter/bin"
بعد ذلك، قم بتنفيذ الأمر التالي لتحديث المتغيرات:
source $HOME/.bashrc
قم بتشغيل الأمر التالي للتحقق من وجود أي متطلبات إضافية:
flutter doctor
سيقوم هذا الأمر بفحص النظام وإظهار قائمة بالبرامج المطلوبة لتطوير تطبيقات فلاتر. قم بتثبيت أي برامج مطلوبة مذكورة في النتائج.
يمكنك استخدام أحد محررات الأكواد المدعومة:
قبل الغوص في تفاصيل تطوير تطبيقات فلاتر، من المهم فهم بعض المفاهيم الأساسية التي يرتكز عليها الإطار.
لإنشاء مشروع فلاتر جديد، قم بتنفيذ الأمر التالي في الطرفية:
flutter create my_app
سيقوم هذا الأمر بإنشاء مشروع جديد باسم "my_app". للدخول إلى المشروع وتشغيله:
cd my_app
flutter run
عند إنشاء مشروع فلاتر جديد، سيتم إنشاء بنية ملفات أساسية:
my_app/ ├── .dart_tool/ # أدوات دارت ├── .idea/ # ملفات IntelliJ IDEA ├── android/ # مشروع Android ├── ios/ # مشروع iOS ├── lib/ # ملفات Dart الرئيسية │ └── main.dart # نقطة دخول التطبيق ├── test/ # اختبارات الوحدة والتكامل ├── web/ # ملفات تطبيق الويب ├── .gitignore # ملفات مستثناة من git ├── .metadata # بيانات وصفية للمشروع ├── pubspec.yaml # تبعيات المشروع والإعدادات └── README.md # وثائق المشروع
الملف pubspec.yaml
يحتوي على معلومات المشروع والتبعيات (المكتبات الخارجية).
الملف main.dart
هو نقطة البداية للتطبيق.
إليك مثال بسيط لتطبيق "Hello World" في فلاتر:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'مرحباً بالعالم',
theme: ThemeData(
primarySwatch: Colors.green,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('تطبيق فلاتر الأول'),
centerTitle: true,
),
body: const Center(
child: Text(
'مرحباً بالعالم!',
style: TextStyle(fontSize: 24),
),
),
);
}
}
لنشرح المثال السابق:
runApp()
مع الويدجت الرئيسي.في فلاتر، كل شيء هو ويدجت: الأزرار، النصوص، الصور، التخطيطات، وحتى التطبيق نفسه. تعمل الويدجات كلبنات بناء لواجهة المستخدم.
كل ويدجت تحتوي على دالة build()
تحدد كيفية عرض الويدجت على الشاشة. هذه الدالة يتم استدعاؤها عندما يحتاج فلاتر لإعادة رسم الويدجت.
فلاتر يوفر ميزتين مهمتين لتسريع دورة التطوير:
فلاتر يوفر مجموعة غنية من الويدجات المدمجة لبناء واجهات المستخدم. سنستعرض هنا أهم الويدجات الأساسية التي ستحتاجها في معظم التطبيقات.
لعرض سلسلة نصية بتنسيق معين.
Text(
'مرحباً بالعالم',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
)
حقل إدخال للنصوص.
TextField(
decoration: InputDecoration(
labelText: 'أدخل اسمك',
border: OutlineInputBorder(),
),
onChanged: (value) {
print('تم الإدخال: $value');
},
)
لعرض نص بتنسيقات مختلفة ضمن نفس النص.
RichText(
text: TextSpan(
style: TextStyle(color: Colors.black),
children: [
TextSpan(text: 'مرحباً '),
TextSpan(
text: 'بالعالم',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
],
),
)
لتجميع حقول إدخال مع إمكانية التحقق من الصحة.
Form(
key: _formKey,
child: Column(
children: [
TextFormField(
validator: (value) {
if (value == null || value.isEmpty) {
return 'يرجى إدخال نص';
}
return null;
},
),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
// تنفيذ الإجراء
}
},
child: Text('إرسال'),
),
],
),
)
زر بارز مرتفع.
ElevatedButton(
onPressed: () {
print('تم النقر');
},
child: Text('زر'),
)
زر نصي بدون خلفية.
TextButton(
onPressed: () {},
child: Text('زر نصي'),
)
زر يحتوي على أيقونة فقط.
IconButton(
icon: Icon(Icons.favorite),
onPressed: () {},
)
للكشف عن الإيماءات المختلفة (النقر، السحب، إلخ).
GestureDetector(
onTap: () {
print('تم النقر');
},
onDoubleTap: () {
print('نقر مزدوج');
},
child: Container(
height: 100,
width: 100,
color: Colors.blue,
child: Center(
child: Text('انقر هنا'),
),
),
)
زر عائم دائري بارز.
FloatingActionButton(
onPressed: () {},
child: Icon(Icons.add),
backgroundColor: Colors.green,
)
لعرض الصور من مصادر مختلفة.
// صورة من الإنترنت
Image.network(
'https://example.com/image.jpg',
width: 200,
height: 200,
fit: BoxFit.cover,
)
// صورة من الأصول (assets)
Image.asset(
'assets/images/logo.png',
width: 100,
height: 100,
)
لعرض الأيقونات.
Icon(
Icons.star,
color: Colors.yellow,
size: 24,
)
مؤشر تقدم دائري.
CircularProgressIndicator(
value: 0.7, // قيمة التقدم (0.0 إلى 1.0)
backgroundColor: Colors.grey[200],
color: Colors.blue,
)
مؤشر تقدم خطي.
LinearProgressIndicator(
value: 0.5, // قيمة التقدم (0.0 إلى 1.0)
backgroundColor: Colors.grey[200],
color: Colors.green,
)
يمكنك الاطلاع على كافة الويدجات المتاحة في توثيق فلاتر الرسمي.
تخطيطات الواجهة (Layouts) هي ويدجات تساعد في تنظيم وترتيب الويدجات الأخرى داخلها وفقاً لأنماط معينة. تعتبر أساسية في بناء واجهات المستخدم.
ويدجت متعدد الاستخدامات يمكن استخدامه لإضافة الأنماط والتنسيق والحشوات والهوامش.
Container(
margin: EdgeInsets.all(10),
padding: EdgeInsets.all(15),
width: 200,
height: 100,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.black26,
blurRadius: 5,
offset: Offset(0, 2),
),
],
),
child: Text(
'مربع منسق',
style: TextStyle(color: Colors.white),
),
)
يرتب الويدجات أفقياً جنباً إلى جنب.
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(Icons.star, size: 30),
Text('نجمة'),
ElevatedButton(
onPressed: () {},
child: Text('زر'),
),
],
)
يرتب الويدجات عمودياً من أعلى إلى أسفل.
Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('العنوان', style: TextStyle(fontSize: 20)),
SizedBox(height: 10),
Text('وصف تفصيلي هنا...'),
SizedBox(height: 15),
ElevatedButton(
onPressed: () {},
child: Text('تأكيد'),
),
],
)
يضع الويدجات فوق بعضها البعض، مثل طبقات.
Stack(
children: [
Container(
width: 200,
height: 200,
color: Colors.blue,
),
Positioned(
top: 10,
right: 10,
child: Icon(
Icons.close,
color: Colors.white,
),
),
Center(
child: Text(
'محتوى',
style: TextStyle(color: Colors.white),
),
),
],
)
يسمحان للويدجات بملء المساحة المتبقية في Row أو Column.
Row(
children: [
Container(width: 50, color: Colors.red),
Expanded(
flex: 2,
child: Container(color: Colors.green),
),
Expanded(
flex: 1,
child: Container(color: Colors.blue),
),
],
)
يشبه Row لكنه ينقل الويدجات إلى السطر التالي إذا لم تكن هناك مساحة كافية.
Wrap(
spacing: 8.0, // المسافة بين العناصر أفقياً
runSpacing: 8.0, // المسافة بين الصفوف
children: [
Chip(label: Text('فلاتر')),
Chip(label: Text('دارت')),
Chip(label: Text('واجهة المستخدم')),
Chip(label: Text('تطوير')),
Chip(label: Text('تطبيقات')),
],
)
لنرى مثالاً على كيفية استخدام التخطيطات المختلفة معاً لبناء واجهة بطاقة منتج:
Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// صورة المنتج
Stack(
children: [
Image.network(
'https://example.com/product.jpg',
height: 200,
width: double.infinity,
fit: BoxFit.cover,
),
Positioned(
top: 10,
right: 10,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(20),
),
child: Text(
'خصم 20%',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
),
),
],
),
// معلومات المنتج
Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'عنوان المنتج هنا',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8),
Text(
'وصف مختصر للمنتج يظهر هنا ويمكن أن يمتد لعدة أسطر.',
style: TextStyle(color: Colors.grey[600]),
),
SizedBox(height: 16),
// السعر والتقييم
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'99 ريال',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.green,
),
),
Row(
children: [
Text('4.5'),
Icon(Icons.star, color: Colors.amber, size: 16),
],
),
],
),
Divider(height: 24),
// أزرار التفاعل
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
primary: Colors.green,
),
child: Text('إضافة للسلة'),
),
),
SizedBox(width: 8),
IconButton(
icon: Icon(Icons.favorite_border),
onPressed: () {},
),
],
),
],
),
),
],
),
)
عند تصميم واجهات في فلاتر، فكر دائماً بالطريقة التي تتداخل بها التخطيطات. يمكن وضع Row داخل Column، أو Stack داخل Container، وهكذا.
إدارة الحالة (State Management) هي أحد أهم المفاهيم في فلاتر، حيث تتحكم في كيفية تخزين بيانات التطبيق والتفاعل معها وتحديث واجهة المستخدم عند تغيرها.
تخص ويدجت واحد فقط، مثل حالة زر أو حقل نص. تُدار عادة باستخدام StatefulWidget
و setState()
.
تُشارك بين عدة ويدجات في التطبيق، مثل بيانات المستخدم أو سلة التسوق. تحتاج إلى حلول إدارة حالة أكثر تقدماً.
الطريقة الأساسية لإدارة الحالة المحلية في فلاتر هي استخدام StatefulWidget
:
class CounterPage extends StatefulWidget {
@override
_CounterPageState createState() => _CounterPageState();
}
class _CounterPageState extends State {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('عداد بسيط')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'لقد ضغطت على الزر هذا العدد من المرات:',
),
Text(
'$_counter',
style: TextStyle(fontSize: 36, fontWeight: FontWeight.bold),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'زيادة',
child: Icon(Icons.add),
),
);
}
}
في المثال السابق:
StatefulWidget
يسمى CounterPage
_CounterPageState
تحتوي على متغير _counter
_incrementCounter()
التي تستخدم setState()
لتغيير قيمة _counter
setState()
يخبر فلاتر بإعادة بناء الويدجت لتعكس التغييرات
بينما يعمل setState
بشكل جيد للحالات البسيطة، تحتاج التطبيقات الأكبر إلى حلول أكثر تنظيمًا. إليك بعض أشهر الحلول:
حل بسيط لإدارة الحالة مدعوم من فريق فلاتر. يعتمد على نمط الوصاية (InheritedWidget) ويسهل مشاركة وتحديث الحالة.
// إنشاء نموذج البيانات
class CounterModel extends ChangeNotifier {
int _counter = 0;
int get counter => _counter;
void increment() {
_counter++;
notifyListeners();
}
}
// استخدام Provider في الشجرة
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CounterModel(),
child: MyApp(),
),
);
}
// الاستماع للتغييرات في أي ويدجت
class CounterDisplay extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Text(
'${context.watch().counter}',
style: TextStyle(fontSize: 36),
);
}
}
// تحديث الحالة من أي ويدجت
class IncrementButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
context.read().increment();
},
child: Text('زيادة'),
);
}
}
نمط متقدم يفصل بشكل واضح بين المنطق وعرض واجهة المستخدم، باستخدام تدفق الأحداث.
// تعريف Cubit (أبسط من Bloc)
class CounterCubit extends Cubit {
CounterCubit() : super(0);
void increment() => emit(state + 1);
void decrement() => emit(state - 1);
}
// استخدام في التطبيق
BlocProvider(
create: (context) => CounterCubit(),
child: CounterScreen(),
);
// عرض الحالة
BlocBuilder(
builder: (context, count) {
return Text('$count',
style: TextStyle(fontSize: 36),
);
},
);
// تحديث الحالة
ElevatedButton(
onPressed: () {
context.read().increment();
},
child: Text('زيادة'),
)
إطار عمل خفيف وقوي يوفر إدارة حالة، تنقل، وحقن اعتماديات وأدوات متنوعة.
// وحدة التحكم
class CounterController extends GetxController {
var count = 0.obs;
void increment() => count++;
}
// استخدام في الويدجت
class HomeView extends StatelessWidget {
final CounterController c = Get.put(CounterController());
@override
Widget build(context) {
return Scaffold(
body: Center(
child: Obx(() => Text("${c.count}")),
),
floatingActionButton: FloatingActionButton(
onPressed: c.increment,
child: Icon(Icons.add),
),
);
}
}
تطوير أكثر تقدمًا لمفهوم Provider، يعالج بعض القيود ويوفر إمكانات إضافية.
// تعريف Provider
final counterProvider = StateNotifierProvider((ref) {
return Counter();
});
class Counter extends StateNotifier {
Counter() : super(0);
void increment() => state = state + 1;
}
// استخدام في الويدجت
class CounterWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Column(
children: [
Text('$count'),
ElevatedButton(
onPressed: () => ref.read(counterProvider.notifier).increment(),
child: Text('زيادة'),
),
],
);
}
}
اختيار نظام إدارة الحالة المناسب يعتمد على حجم التطبيق ومتطلباته. للمشاريع الصغيرة، setState
أو Provider
يكون كافيًا. للمشاريع الكبيرة، قد تفضل Bloc
أو Riverpod
للحصول على هيكل أكثر صلابة.
معظم التطبيقات تتعامل مع البيانات بطريقة أو بأخرى: قراءتها، حفظها، مزامنتها. سنلقي نظرة على طرق التعامل مع البيانات في فلاتر.
تخزين بسيط للبيانات البدائية على شكل مفتاح-قيمة.
// تثبيت المكتبة في pubspec.yaml
// shared_preferences: ^2.0.13
// حفظ البيانات
Future saveUser(String username) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('username', username);
await prefs.setBool('isLoggedIn', true);
}
// قراءة البيانات
Future loadUser() async {
final prefs = await SharedPreferences.getInstance();
final username = prefs.getString('username') ?? '';
final isLoggedIn = prefs.getBool('isLoggedIn') ?? false;
}
قاعدة بيانات NoSQL خفيفة وسريعة مكتوبة بلغة دارت.
// تثبيت المكتبة في pubspec.yaml
// hive: ^2.0.5
// hive_flutter: ^1.1.0
// في main.dart
await Hive.initFlutter();
Hive.registerAdapter(PersonAdapter());
await Hive.openBox('people');
// حفظ البيانات
final peopleBox = Hive.box('people');
peopleBox.put('key', Person('أحمد', 25));
peopleBox.add(Person('محمد', 30)); // مفتاح تلقائي
// قراءة البيانات
final person = peopleBox.get('key');
final allPeople = peopleBox.values.toList();
قاعدة بيانات علائقية كاملة المزايا.
// تثبيت المكتبة في pubspec.yaml
// sqflite: ^2.0.2
// path: ^1.8.0
// إنشاء قاعدة البيانات والجدول
Future getDatabase() async {
final dbPath = await getDatabasesPath();
final path = join(dbPath, 'my_app.db');
return openDatabase(
path,
onCreate: (db, version) {
return db.execute(
'CREATE TABLE users(id INTEGER PRIMARY KEY, name TEXT, age INTEGER)',
);
},
version: 1,
);
}
// إدخال بيانات
Future insertUser(User user) async {
final db = await getDatabase();
await db.insert(
'users',
user.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
// قراءة البيانات
Future> getUsers() async {
final db = await getDatabase();
final List
// تثبيت المكتبة في pubspec.yaml
// path_provider: ^2.0.9
import 'dart:io';
import 'package:path_provider/path_provider.dart';
// الحصول على مسار الملف
Future get _localPath async {
final directory = await getApplicationDocumentsDirectory();
return directory.path;
}
// كتابة بيانات إلى ملف
Future writeToFile(String data) async {
final path = await _localPath;
final file = File('$path/data.txt');
await file.writeAsString(data);
}
// قراءة بيانات من ملف
Future readFromFile() async {
try {
final path = await _localPath;
final file = File('$path/data.txt');
return await file.readAsString();
} catch (e) {
return '';
}
}
تتواصل معظم التطبيقات الحديثة مع خدمات ويب خارجية من خلال واجهات API. سنتعلم كيفية إجراء طلبات HTTP وإدارة البيانات المستلمة.
// تثبيت المكتبة في pubspec.yaml
// http: ^0.13.4
import 'package:http/http.dart' as http;
import 'dart:convert';
// طلب GET
Future> fetchPosts() async {
final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/posts'));
if (response.statusCode == 200) {
final List data = jsonDecode(response.body);
return data.map((json) => Post.fromJson(json)).toList();
} else {
throw Exception('فشل في تحميل المنشورات');
}
}
// طلب POST
Future createPost(Post post) async {
final response = await http.post(
Uri.parse('https://jsonplaceholder.typicode.com/posts'),
headers: {
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode(post.toJson()),
);
if (response.statusCode == 201) {
return Post.fromJson(jsonDecode(response.body));
} else {
throw Exception('فشل في إنشاء المنشور');
}
}
// نموذج البيانات
class Post {
final int userId;
final int? id;
final String title;
final String body;
Post({required this.userId, this.id, required this.title, required this.body});
factory Post.fromJson(Map json) {
return Post(
userId: json['userId'],
id: json['id'],
title: json['title'],
body: json['body'],
);
}
Map toJson() {
return {
'userId': userId,
'title': title,
'body': body,
};
}
}
// تثبيت المكتبة في pubspec.yaml
// dio: ^4.0.6
import 'package:dio/dio.dart';
class ApiService {
final Dio _dio = Dio(BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: 5000,
receiveTimeout: 3000,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
));
// إضافة معترض لتوكن التفويض
void addAuthToken(String token) {
_dio.interceptors.clear();
_dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
options.headers['Authorization'] = 'Bearer $token';
return handler.next(options);
},
onError: (DioError e, handler) {
if (e.response?.statusCode == 401) {
// تحديث التوكن أو تسجيل الخروج
}
return handler.next(e);
},
));
}
// طلب GET
Future> getUsers() async {
try {
final response = await _dio.get('/users');
final List data = response.data;
return data.map((json) => User.fromJson(json)).toList();
} catch (e) {
throw Exception('حدث خطأ: $e');
}
}
// طلب POST
Future createUser(User user) async {
try {
final response = await _dio.post('/users', data: user.toJson());
return User.fromJson(response.data);
} catch (e) {
throw Exception('حدث خطأ: $e');
}
}
}
class PostsScreen extends StatefulWidget {
@override
_PostsScreenState createState() => _PostsScreenState();
}
class _PostsScreenState extends State {
late Future> futurePosts;
@override
void initState() {
super.initState();
futurePosts = fetchPosts();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('المنشورات')),
body: FutureBuilder>(
future: futurePosts,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(child: Text('حدث خطأ: ${snapshot.error}'));
} else if (!snapshot.hasData || snapshot.data!.isEmpty) {
return Center(child: Text('لا توجد منشورات'));
} else {
return ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (context, index) {
final post = snapshot.data![index];
return ListTile(
title: Text(post.title),
subtitle: Text(post.body),
);
},
);
}
},
),
);
}
}
للتطبيقات الإنتاجية، ينصح بإنشاء طبقة خدمة منفصلة لإدارة طلبات API، مما يساعد على فصل منطق عرض البيانات عن منطق الحصول عليها، ويسهل اختبار التطبيق.
الاختبار جزء أساسي من دورة تطوير البرمجيات. فلاتر يدعم ثلاثة أنواع من الاختبارات: اختبارات الوحدة، واختبارات الويدجت، واختبارات التكامل.
تختبر وحدة واحدة من الكود، مثل دالة أو فئة، بعيداً عن باقي مكونات التطبيق.
// في ملف test/calculator_test.dart
import 'package:test/test.dart';
import 'package:my_app/calculator.dart';
void main() {
group('Calculator', () {
late Calculator calculator;
setUp(() {
calculator = Calculator();
});
test('يجب أن تجمع رقمين بشكل صحيح', () {
expect(calculator.add(2, 3), 5);
});
test('يجب أن تطرح رقمين بشكل صحيح', () {
expect(calculator.subtract(5, 2), 3);
});
test('يجب أن ترفع استثناء عند القسمة على صفر', () {
expect(() => calculator.divide(10, 0), throwsA(isA()));
});
});
}
تختبر ويدجت واحد أو مجموعة صغيرة من الويدجات.
// في ملف test/counter_widget_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/counter_widget.dart';
void main() {
testWidgets('يجب أن يزيد العداد عند النقر', (WidgetTester tester) async {
// بناء التطبيق وتشغيل دورة واحدة من الرسم
await tester.pumpWidget(MaterialApp(
home: CounterWidget(),
));
// التحقق من وجود نص '0'
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// الضغط على زر الزيادة
await tester.tap(find.byIcon(Icons.add));
// انتظار انتهاء الرسوم المتحركة
await tester.pump();
// التحقق من تغير العداد إلى '1'
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}
تختبر تطبيقك بالكامل كما هو في بيئة حقيقية.
// في ملف integration_test/app_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:my_app/main.dart' as app;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('تطبيق كامل', () {
testWidgets('التكامل المبسط', (WidgetTester tester) async {
// تشغيل التطبيق
app.main();
await tester.pumpAndSettle();
// التحقق من العناصر الأولية
expect(find.text('العداد: 0'), findsOneWidget);
// النقر على زر الزيادة
await tester.tap(find.byIcon(Icons.add));
await tester.pumpAndSettle();
// التحقق من تحديث العداد
expect(find.text('العداد: 1'), findsOneWidget);
// الانتقال إلى صفحة جديدة
await tester.tap(find.byType(ElevatedButton));
await tester.pumpAndSettle();
// التحقق من الصفحة الجديدة
expect(find.text('الصفحة الثانية'), findsOneWidget);
});
});
}
# تشغيل اختبارات الوحدة
flutter test test/calculator_test.dart
# تشغيل اختبارات الويدجت
flutter test test/counter_widget_test.dart
# تشغيل اختبارات التكامل
flutter test integration_test
الاختبارات الجيدة تساعد على الكشف عن الأخطاء مبكرًا وتحسين جودة التطبيق. حاول دائمًا تغطية أكبر قدر ممكن من التعليمات البرمجية الخاصة بك بالاختبارات.
بعد الانتهاء من تطوير تطبيقك، حان الوقت لنشره وتوزيعه على المستخدمين. هناك عدة منصات وخطوات تحتاج إلى اتباعها.
pubspec.yaml
# بناء ملف APK لنظام Android
flutter build apk --release
# بناء حزمة Android App Bundle (AAB)
flutter build appbundle --release
# بناء تطبيق iOS (يتطلب جهاز Mac)
flutter build ios --release
تهانينا! لقد نجحت في نشر تطبيق فلاتر الخاص بك. تذكر أن نجاح التطبيق عملية مستمرة تتضمن التحديثات والتحسينات المنتظمة بناءً على احتياجات المستخدمين.
لقد تعلمنا في هذا الدليل الشامل أساسيات تطوير تطبيقات فلاتر، بدءًا من التثبيت والإعداد، مرورًا بالويدجات والتخطيطات، وإدارة الحالة، والتنقل، والتعامل مع البيانات، وحتى اختبار ونشر التطبيق.
فلاتر هو إطار عمل متطور باستمرار، وتظهر ميزات وأدوات جديدة بانتظام. لذا، من المهم متابعة المصادر الرسمية والمجتمع للبقاء على اطلاع بأحدث التطورات والممارسات.
نتمنى لك التوفيق في رحلة تطوير تطبيقات فلاتر الخاصة بك!