اتعلّم Flutter من الصفر
واشتغل مبرمج موبايل محترف
منهج متكامل من 12 وحدة، بياخدك خطوة بخطوة من كتابة أول سطر كود، لحد بناء ونشر تطبيق حقيقي على Google Play. مع أكواد كاملة، تمارين، وشرح مبسّط.
مقدمة وتهيئة بيئة العمل
Flutter هو إطار عمل (Framework) مفتوح المصدر من تطوير شركة Google، بيخليك تبني تطبيق واحد يشتغل على Android وiOS والويب وسطح المكتب من نفس الكود. ده معناه إنك تتعلم لغة واحدة وتبني لكل الأجهزة.
ليه فلاتر بالذات؟
- كود واحد لكل المنصات: بدل ما تتعلم Kotlin للأندرويد و Swift للـ iOS، تكتب مرة واحدة.
- سريع: ميزة Hot Reload بتخليك تشوف أي تعديل في التطبيق فورًا بدون ما تعيد تشغيله.
- واجهات جميلة: فيه مكتبة ضخمة من الجاهز (Widgets) + الدعم الرسمي لتصميمات Material 3.
تهيئة بيئة العمل
| الأداة | الوظيفة | ملاحظة |
|---|---|---|
| Flutter SDK | قلب فلاتر، فيه أدوات البناء والتشغيل | حمّله من flutter.dev |
| Android Studio | محرّر أكواد + Android Emulator | بيجي معاه الإيمولاتر |
| VS Code | محرّر خفيف وسريع (اختياري لكن مُفضّل) | نزّل إضافة Flutter |
| Git | لحفظ مشاريعك ومزامنتها | مهم جدًا للمستقبل |
flutter doctor — لو طلعت علّمات صح خضراء يبقى كل حاجة تمام.flutter doctor، وحلّ أي مشكلة بسيطة يظهرها، واتأكد إن الإيمولاتر بيشتغل.
أساسيات لغة Dart
قبل فلاتر، محتاج تفهم Dart. متخافش، دي الأساسيات اللي هتستعملها طول الوقت.
المتغيرات (Variables)
main.dart// المتغير بيحفظ قيمة في الذاكرة
var name = 'أحمد'; // النوع بيستنتج تلقائيًا
String city = 'القاهرة'; // نص
int age = 25; // رقم صحيح
double height = 1.75; // رقم عشري
bool isStudent = true; // صح / غلط
final pi = 3.14; // ثابت بعد التعريف
const gravity = 9.8; // ثابت من الأول
final أو const لو القيمة مش هتتغير — ده بيسرّع تطبيقك. وnull safety في Dart معناها إن المتغير مش بياخد null غير لما تقوله String?.الشروط (Conditions)
int score = 85;
if (score >= 90) {
print('ممتاز');
} else if (score >= 75) {
print('جيد جدًا');
} else {
print('محتاج تحسّن');
}
// الاختصار الثلاثي
var result = score >= 50 ? 'ناجح' : 'راسب';
الحلقات (Loops)
for (int i = 1; i <= 5; i++) {
print('الرقم: $i'); // $i بتحط قيمة المتغير جوه النص
}
// لف على عناصر قائمة
var langs = ['Dart', 'Flutter', 'Python'];
for (var lang in langs) {
print(lang);
}
الدوال (Functions)
// دالة بسيطة
int add(int a, int b) {
return a + b;
}
// Arrow function (لو سطر واحد)
int multiply(int a, int b) => a * b;
void greet(String name, {String greeting = 'أهلاً'}) {
print('$greeting $name'); // { } = معامل اختياري
}
void main() {
print(add(3, 4)); // 7
greet('سارة'); // أهلاً سارة
}
👁️ اعرض الحل
int largest(List<int> nums) {
var max = nums[0];
for (var n in nums) {
if (n > max) max = n;
}
return max;
}
Dart متقدّم: البرمجة الكائنية
الكلاسات والكائنات (Classes & Objects)
class User {
final String name;
int age;
// Constructor
User({required this.name, required this.age});
// Method
String describe() => '$name عمره $age سنة';
}
void main() {
var user = User(name: 'منى', age: 22);
print(user.describe()); // منى عمره 22 سنة
}
القوائم والخَرائط (Collections)
// List = قائمة مرتبة
List<String> fruits = ['تفاح', 'موز', 'عنب'];
fruits.add('مانجو'); // ضيف عنصر
// Map = مفتاح → قيمة
Map<String, int> scores = {'علي': 90, 'سارة': 95};
print(scores['علي']); // 90
// Set = مجموعة بدون تكرار
Set<int> ids = {1, 2, 2, 3}; // الطول = 3 (الـ 2 مرة واحدة)
Null Safety
String? nickname; // قد يكون null
print(nickname?.length); // ? = نفّذ لو مش null
print(nickname ?? 'بدون اسم'); // ?? = القيمة البديلة
Product فيه اسم وسعر وكمية، ودالة تحسب السعر الكلي (السعر × الكمية).
👁️ اعرض الحل
class Product {
final String name;
final double price;
final int quantity;
Product({required this.name, required this.price, required this.quantity});
double total() => price * quantity;
}
{1, 2, 2, 3}.length؟أول تطبيق Flutter
إنشاء المشروع
# في الـ Terminal، اكتب:
flutter create my_first_app
cd my_first_app
flutter run
هيكل المشروع
| المسار | الوظيفة |
|---|---|
lib/main.dart | الكود الأساسي — هنا بتكتب كل حاجة تقريبًا |
pubspec.yaml | ملف الإعدادات + المكتبات (packages) |
android/ و ios/ | ملفات المنصات (مش بتلمسها كتير) |
أول كود
lib/main.dartimport 'package:flutter/material.dart';
void main() {
runApp(const MyApp()); // نقطة البداية
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'أول تطبيق',
theme: ThemeData(useMaterial3: true),
home: const HomeScreen(),
);
}
}
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('أهلاً بيك 👋')),
body: const Center(
child: Text('أول تطبيق ليا بفلاتر!', style: TextStyle(fontSize: 22)),
),
);
}
}
Icon جوه الـ body بدل النص، وجرّب الـ Hot Reload.
الواجهات والـ Widgets
كل حاجة في فلاتر هي Widget! النص widget، الزر widget، وحتى الشاشة نفسها widget. في نوعين رئيسيين:
| النوع | متى تستخدمه |
|---|---|
| StatelessWidget | لما الواجهة ثابتة ومش بتتغير |
| StatefulWidget | لما في بيانات بتتغير (عداد، إدخال مستخدم) |
نصوص وتنسيق
Text(
'هذا نص منسّق',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.blue,
letterSpacing: 1.2,
),
)
الحاوية Container
Container(
width: 200,
height: 100,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.blue.shade100,
borderRadius: BorderRadius.circular(16),
boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 8)],
),
child: const Text('أنا جوه الحاوية'),
)
الأزرار
ElevatedButton(
onPressed: () {
print('تم الضغط!');
},
child: const Text('اضغط هنا'),
)
FilledButton.icon(
onPressed: () {},
icon: const Icon(Icons.add),
label: const Text('إضافة'),
)
الصور
// صورة من الإنترنت
Image.network('https://picsum.photos/200')
// صورة من ملفات المشروع (assets)
Image.asset('assets/logo.png')
Image.asset، لازم تسجّل مجلد assets في ملف pubspec.yaml الأول، وإلا هيديك خطأ.مثال StatefulWidget (عدّاد)
class CounterScreen extends StatefulWidget {
const CounterScreen({super.key});
@override
State<CounterScreen> createState() => _CounterScreenState();
}
class _CounterScreenState extends State<CounterScreen> {
int count = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('العدد: $count', style: const TextStyle(fontSize: 30)),
ElevatedButton(
onPressed: () {
setState(() => count++); // 👈 بيعيد بناء الواجهة
},
child: const Text('+1'),
),
],
),
),
);
}
}
setState() عشان فلاتر يعرف يعيد رسم الواجهة.👁️ فكرة الحل
ضيف متغيرين حالة:count، واعمل زرارين بكل واحد setState بيعدّل القيمة.التخطيطات (Layout)
| Widget | الوظيفة |
|---|---|
| Column | يرصّ العناصر رأسيًا (فوق بعض) |
| Row | يرصّ العناصر أفقيًا (جنب بعض) |
| Stack | يضع العناصر فوق بعض (طبقات) |
| ListView | قائمة قابلة للتمرير، مثالية للبيانات الكتيرة |
| Expanded | يخلي عنصر ياخد المساحة المتاحة |
Column(
children: [
Text('العنصر الأول'),
Text('العنصر الثاني'),
],
)
Row(
children: [
Expanded(child: Text('نص طويل')),
Icon(Icons.star),
],
)
// قائمة كاملة قابلة للتمرير
ListView.builder(
itemCount: 100,
itemBuilder: (context, index) {
return ListTile(
leading: CircleAvatar(child: Text('${index + 1}')),
title: Text('العنصر رقم ${index + 1}'),
);
},
)
بطاقة لائقة (Card)
Card(
child: ListTile(
leading: const Icon(Icons.person),
title: const Text('اسم المستخدم'),
subtitle: const Text('مطور فلاتر'),
trailing: const Icon(Icons.arrow_forward),
),
)
SingleChildScrollView عشان مايحصلش Overflow.إدارة الحالة (State)
الحالة (State) هي أي بيانات بتتغير في تطبيقك. في البداية، ابدأ بـ setState وافهمه كويس قبل ما تدخل في مكتبات أكبر.
التعامل مع إدخال المستخدم
class _MyFormState extends State<MyForm> {
final _controller = TextEditingController();
String savedName = '';
@override
void dispose() {
_controller.dispose(); // 👈 مهم لتحرير الذاكرة
super.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
TextField(
controller: _controller,
decoration: const InputDecoration(labelText: 'اكتب اسمك'),
),
ElevatedButton(
onPressed: () {
setState(() => savedName = _controller.text);
},
child: const Text('حفظ'),
),
Text('مرحبًا: $savedName'),
],
),
);
}
}
TextEditingController أو AnimationController اعمل له dispose في النهاية، وإلا يسبب تسريب ذاكرة.ListView لعرض المهام.
البرمجة غير المتزامنة و APIs
التطبيقات الحقيقية بتحتاج تجيب بيانات من الإنترنت. ده عملية بتاخد وقت، ولهذا نستخدم async/await.
import 'dart:convert';
import 'package:http/http.dart' as http;
Future<List<Post>> fetchPosts() async {
final res = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/posts'),
);
if (res.statusCode == 200) {
final data = json.decode(res.body) as List;
return data.map((e) => Post.fromJson(e)).toList();
} else {
throw Exception('فشل التحميل');
}
}
الكلاس Post (JSON Model)
class Post {
final int id;
final String title;
Post({required this.id, required this.title});
factory Post.fromJson(Map<String, dynamic> json) {
return Post(id: json['id'], title: json['title']);
}
}
استخدام FutureBuilder في الواجهة
FutureBuilder<List<Post>>(
future: fetchPosts(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
}
if (snapshot.hasError) {
return Text('خطأ: ${snapshot.error}');
}
final posts = snapshot.data!;
return ListView.builder(
itemCount: posts.length,
itemBuilder: (c, i) => ListTile(title: Text(posts[i].title)),
);
},
)
jsonplaceholder.typicode.com أو dummyjson.com للتجربة بدون مفاتيح.dummyjson.com/products واعرضها في ListView مع صورة واسم وسعر.
التخزين المحلي (Local Storage)
| الأداة | الاستخدام المناسب |
|---|---|
| SharedPreferences | بيانات صغيرة: إعدادات، تفضيلات، جلسة المستخدم |
| Hive | قوائم وبيانات منظّمة بسرعة ودون تعقيد |
| sqflite | قواعد بيانات كبيرة واستعلامات معقدة |
import 'package:shared_preferences/shared_preferences.dart';
// حفظ
Future<void> saveName(String name) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('name', name);
}
// قراءة
Future<String?> readName() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString('name');
}
إدارة الحالة المتقدمة
لما التطبيق يكبر، setState لوحده مابيكفيش. هنا تيجي مكتبات إدارة الحالة.
| المكتبة | المستوى | المناسبة |
|---|---|---|
| Provider | متوسط | سهلة وممتازة للبداية والمشاريع المتوسطة |
| Bloc / Cubit | متقدّم | المشاريع الكبيرة، فصل منطق واضح، مستخدمة في الشركات |
| Riverpod | متقدّم | بديل حديث لـ Provider وأقوى بأمان الأنواع |
مثال Provider
// 1) الموديل
class CounterModel extends ChangeNotifier {
int value = 0;
void increment() {
value++;
notifyListeners(); // 👈 إعلام الواجهات بالتغيير
}
}
// 2) توفير الموديل للتطبيق
ChangeNotifierProvider(
create: (_) => CounterModel(),
child: MaterialApp(home: HomeScreen()),
)
// 3) قراءة القيمة في الواجهة
final counter = context.watch<CounterModel>();
return Text('${counter.value}');
setState و Provider الأول، وبعدها انتقل لـ Bloc/Cubit بثقة.مشروع كامل + النشر
هنجمع كل حاجة اتعلمناها في مشروع واحد: تطبيق طقس.
خريطة المشروع
- الواجهة: شاشة بحث + شاشة نتائج بفيها درجة الحرارة والرطوبة.
- API: استخدم
open-meteo.com(مجاني بدون مفتاح) لجلب الطقس. - الحالة: Provider لإدارة حالة التحميل والنتيجة والخطأ.
- التخزين: احفظ آخر مدينة بحث المستخدم عنها.
- التنسيق: Dark Mode جميل وأيقونات مناسبة.
خطوات النشر على Google Play
- عدّل
pubspec.yaml: اسم التطبيق + رقم الإصدار. - غيّر الأيقونة والاسم من
android/app/. - ابنِ نسخة الإصدار:
flutter build appbundle --release - اعمل حساب مطوّر على Google Play (رسوم مرة واحدة).
- ارفع ملف
.aabواملأ بيانات المتجر.
AndroidManifest.xml، وراجع سياسة الخصوصية لو التطبيق بيجمع بيانات.خارطة طريق بعد الكورس
- اتقن Git & GitHub: ارفع كل مشاريعك، ده ملفك الشخصي.
- Clean Architecture: تنظيم المشاريع الكبيرة.
- Firebase: تسجيل دخول + قاعدة بيانات سحابية + إشعارات.
- اختبار الوحدات (Testing): يفرقك عن غيرك.
- 3 مشاريع Portfolio: متجر، تواصل اجتماعي، وأداة مفيدة.
- شارك في المجتمع: GitHub و StackOverflow ومجتمعات فلاتر العربية.
للمزيد من المصادر: الوثائق الرسمية لفلاتر