تخطَّ إلى المحتوى

الصور في Flutter: Image.asset و Image.network و BoxFit

الدرس 11 من 33 · Flutter

تطبيق بلا صور يبدو فارغاً: شعار التطبيق، وصور المنتجات، وصورة المستخدم الشخصية. في هذا الدرس ستتعلم مصدرين للصور: صور داخل التطبيق تُحزم معه، وصور من الإنترنت تُحمَّل عند الحاجة. وستتعلم كيف تتحكم في حجمها وشكلها، وماذا تعرض إذا فشل التحميل.

الملفات التي تُحزم مع التطبيق (صور وخطوط وغيرها) تسمى الأصول (Assets). تخيّلها مثل الصور المطبوعة داخل كتاب: موجودة دائماً، ولا تحتاج إنترنت.

استخدامها يمر بثلاث خطوات:

1. أنشئ مجلداً للصور في جذر المشروع، بجانب lib:

my_app/
├─ assets/
│ └─ images/
│ ├─ logo.png
│ └─ cover.png
├─ lib/
└─ pubspec.yaml

2. عرّف المجلد في pubspec.yaml تحت قسم flutter:

pubspec.yaml
flutter:
uses-material-design: true
assets:
- assets/images/

كتابة اسم المجلد وبعده / تضيف كل الملفات الموجودة فيه مباشرة. ثم نفّذ flutter pub get.

3. اعرض الصورة:

Image.asset(
'assets/images/logo.png',
width: 120,
height: 120,
)

صور من الإنترنت: Image.network

Section titled “صور من الإنترنت: Image.network”
Image.network(
'https://dummyjson.com/image/400x200',
height: 200,
)

سهلة جداً، لكن الإنترنت ليس مضموناً: قد يكون بطيئاً، وقد ينقطع، وقد يكون الرابط خاطئاً. تطبيق محترم يتعامل مع الحالتين.

Image.network(
'https://dummyjson.com/image/400x200',
height: 200,
loadingBuilder: (context, child, progress) {
if (progress == null) return child;
return const SizedBox(
height: 200,
child: Center(child: CircularProgressIndicator()),
);
},
)

Flutter يستدعي هذه الدالة عدة مرات أثناء التحميل. عندما تكون progress تساوي null فالتحميل انتهى، فنُرجع الصورة نفسها (child). وقبل ذلك نعرض دائرة التحميل.

Image.network(
'https://example.com/missing.png',
height: 200,
errorBuilder: (context, error, stackTrace) {
return const SizedBox(
height: 200,
child: Center(
child: Icon(Icons.broken_image, size: 48, color: Colors.grey),
),
);
},
)

بدون errorBuilder يظهر للمستخدم مربع خطأ قبيح. معها يرى أيقونة هادئة تخبره أن الصورة غير متاحة.

الصورة لها أبعادها، والمكان المخصص لها له أبعاد أخرى. الخاصية fit تحدد كيف نوفّق بينهما:

Image.asset(
'assets/images/cover.png',
width: double.infinity,
height: 180,
fit: BoxFit.cover,
)
القيمة ماذا تفعل
BoxFit.cover تملأ المساحة كلها وتقص الزائد. الأكثر استخداماً للأغلفة وصور المنتجات
BoxFit.contain تُظهر الصورة كاملة داخل المساحة، وقد تترك فراغات
BoxFit.fill تمط الصورة لتملأ المساحة، فتتشوه النِّسَب
BoxFit.fitWidth تملأ العرض، وقد يُقص الارتفاع
BoxFit.none الحجم الأصلي بدون أي تغيير

تخيّل أنك تضع صورة في برواز: cover تكبّر الصورة حتى تغطي البرواز وتقص ما يخرج عنه، و contain تصغّرها حتى تدخل كلها ولو بقيت حواف فارغة، و fill تمطها بالقوة.

و width: double.infinity تعني «خذ كل العرض المتاح».

الصورة الدائرية: CircleAvatar

Section titled “الصورة الدائرية: CircleAvatar”

للصور الشخصية يوجد Widget جاهز:

const CircleAvatar(
radius: 32,
backgroundImage: AssetImage('assets/images/logo.png'),
),
const CircleAvatar(
radius: 32,
backgroundColor: Colors.indigo,
child: Text('س', style: TextStyle(color: Colors.white, fontSize: 24)),
),
  • radius: نصف القطر، فالقطر الكامل هنا 64.
  • backgroundImage: الصورة. لاحظ أنها تأخذ AssetImage أو NetworkImage، وليس Image.asset. الفرق أن AssetImage مصدر صورة، أما Image.asset فهو Widget يعرضها.
  • إذا لم توجد صورة، ضع أول حرف من الاسم في child على خلفية ملونة، كما تفعل تطبيقات المحادثة.

لوضع نص فوق صورة، اجعل الصورة خلفية لـ Container عبر DecorationImage:

Container(
height: 160,
alignment: Alignment.bottomRight,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
image: const DecorationImage(
image: AssetImage('assets/images/cover.png'),
fit: BoxFit.cover,
),
),
child: const Text(
'رحلة الصيف',
style: TextStyle(color: Colors.white, fontSize: 22),
),
)

هنا الزوايا الدائرية تقص الصورة فعلاً، لأن الصورة جزء من الزخرفة نفسها وليست ابناً داخل الصندوق.

المشروع: بطاقة ملف شخصي

Section titled “المشروع: بطاقة ملف شخصي”
lib/profile_header.dart
class ProfileHeader extends StatelessWidget {
const ProfileHeader({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('الملف الشخصي')),
body: Column(
children: [
Image.asset(
'assets/images/cover.png',
width: double.infinity,
height: 160,
fit: BoxFit.cover,
),
const SizedBox(height: 16),
const CircleAvatar(
radius: 44,
backgroundImage: AssetImage('assets/images/logo.png'),
),
const SizedBox(height: 12),
const Text(
'سارة أحمد',
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
),
const Text('مطورة تطبيقات'),
const SizedBox(height: 16),
Image.network(
'https://dummyjson.com/image/400x200',
height: 120,
errorBuilder: (context, error, stackTrace) {
return const SizedBox(
height: 120,
child: Center(child: Icon(Icons.broken_image, size: 40)),
);
},
),
],
),
);
}
}

الشاشة تعرض صورة غلاف بعرض الشاشة، وتحتها الصورة الشخصية داخل دائرة، ثم الاسم والوظيفة، ثم صورة من الإنترنت. جرّب أن تطفئ الإنترنت وتعيد تشغيل التطبيق: ستظهر أيقونة الصورة المكسورة بدلاً من رسالة خطأ.

  1. أضف صورتين إلى مشروعك وعرّفهما في pubspec.yaml واعرضهما.
  2. اعرض نفس الصورة ثلاث مرات في مربعات 150×150، بـ cover ثم contain ثم fill، وقارن بينها.
  3. أضف loadingBuilder إلى صورة الإنترنت في المشروع.
  4. تحدٍّ: اجعل الصورة الشخصية تتداخل مع الغلاف: نصفها فوقه ونصفها تحته. (تلميح: Stack، وستتعلمها بالتفصيل قريباً.)
  • كتابة مسار الصورة بدون تعريفه في pubspec.yaml: يظهر الخطأ «Unable to load asset». كل صورة يجب أن تكون داخل مجلد معرَّف.
  • استخدام BoxFit.fill لصور المنتجات: تتشوه الصورة. استخدم cover أو contain.
  • حزم صور ضخمة مع التطبيق: صورة بحجم 5 ميجابايت تكبّر حجم التطبيق وتبطئه. صغّر الصور قبل إضافتها.

سؤال

ما قيمة fit التي تملأ المساحة كلها بدون تشويه النِّسَب، ولو قُص جزء من الصورة؟

سؤال

أضفت صورة إلى مجلد assets لكن التطبيق يقول Unable to load asset. ما أول شيء تفحصه؟

  • صور الأصول: مجلد في المشروع، ثم تعريف في pubspec.yaml، ثم Image.asset.
  • Image.network لصور الإنترنت، مع loadingBuilder للتحميل و errorBuilder للفشل.
  • BoxFit.cover للملء مع القص، و contain لإظهار الصورة كاملة.
  • CircleAvatar للصور الشخصية، وتأخذ AssetImage أو NetworkImage.
  • DecorationImage لجعل الصورة خلفية لصندوق فوقها محتوى.