الشرائح: CustomScrollView
ما هي الشرائح (Slivers)؟
الشرائح هي اللبنات الأساسية منخفضة المستوى للمناطق القابلة للتمرير في Flutter. بينما عناصر الراحة مثل ListView وGridView مبنية فوق الشرائح، فإن العمل مع الشرائح مباشرة عبر CustomScrollView يمنحك القوة لدمج القوائم والشبكات والعناوين والعناصر القابلة للتمرير الأخرى في تجربة تمرير موحدة واحدة.
ListView، تحصل على قائمة قابلة للتمرير. مع GridView، تحصل على شبكة قابلة للتمرير. لكن ماذا لو أردت صفحة قابلة للتمرير بها عنوان، ثم قائمة، ثم شبكة، ثم محتوى إضافي؟ هذا بالضبط ما يتيحه CustomScrollView مع الشرائح—مزج تخطيطات قابلة للتمرير مختلفة تحت متحكم تمرير واحد.
CustomScrollView
CustomScrollView هو عرض تمرير ينشئ تأثيرات تمرير مخصصة باستخدام الشرائح. بدلاً من قبول قائمة واحدة من العناصر الفرعية، يقبل قائمة من عناصر الشرائح. كل شريحة تحدد جزءًا من المنطقة القابلة للتمرير.
CustomScrollView الأساسي
CustomScrollView(
slivers: [
// شريط تطبيق شريحي
const SliverAppBar(
title: Text('صفحتي'),
floating: true,
),
// قائمة شريحية
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(
title: Text('عنصر \$index'),
),
childCount: 10,
),
),
// شبكة شريحية
SliverGrid(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
delegate: SliverChildBuilderDelegate(
(context, index) => Card(
child: Center(child: Text('شبكة \$index')),
),
childCount: 6,
),
),
],
)
SliverList
ينشئ SliverList مصفوفة خطية من العناصر على طول محور التمرير. يستخدم نمط المفوض لبناء العناصر الفرعية بشكل كسول، تمامًا مثل ListView.builder.
SliverList مع المفوض
// مفوض البناء - كسول، فعال للقوائم الكبيرة
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return Card(
margin: const EdgeInsets.symmetric(
horizontal: 16, vertical: 4,
),
child: ListTile(
leading: CircleAvatar(child: Text('\$index')),
title: Text('عنصر \$index'),
subtitle: Text('وصف العنصر \$index'),
),
);
},
childCount: 100,
),
)
// مفوض قائمة ثابتة - للقوائم الصغيرة المعروفة
SliverList.list(
children: [
const ListTile(title: Text('العنصر الأول')),
const ListTile(title: Text('العنصر الثاني')),
const ListTile(title: Text('العنصر الثالث')),
],
)
SliverGrid
ينشئ SliverGrid ترتيبًا ثنائي الأبعاد للعناصر الفرعية في منطقة قابلة للتمرير. مثل SliverList، يستخدم المفوضين للبناء الكسول.
أمثلة SliverGrid
// عدد أعمدة ثابت
SliverGrid(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: 1.0,
),
delegate: SliverChildBuilderDelegate(
(context, index) => Container(
color: Colors.primaries[index % Colors.primaries.length],
child: Center(
child: Text(
'\$index',
style: const TextStyle(
color: Colors.white, fontSize: 20,
),
),
),
),
childCount: 30,
),
)
// أقصى امتداد لكل بلاطة
SliverGrid(
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
),
delegate: SliverChildBuilderDelegate(
(context, index) => Card(
child: Center(child: Text('بلاطة \$index')),
),
childCount: 20,
),
)
SliverToBoxAdapter
يلف SliverToBoxAdapter عنصرًا عاديًا (غير شريحي) حتى يمكن استخدامه داخل CustomScrollView. هذه هي الطريقة لإدراج العناوين واللافتات أو أي عنصر صندوقي بين الشرائح.
استخدام SliverToBoxAdapter
CustomScrollView(
slivers: [
const SliverAppBar(
title: Text('المتجر'),
expandedHeight: 200,
flexibleSpace: FlexibleSpaceBar(
background: Image.network(
'https://example.com/banner.jpg',
fit: BoxFit.cover,
),
),
),
// عنوان القسم
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(
'المنتجات المميزة',
style: Theme.of(context).textTheme.headlineSmall,
),
),
),
// شبكة المنتجات
SliverGrid(
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
delegate: SliverChildBuilderDelegate(
(context, index) => const ProductCard(),
childCount: 6,
),
),
// عنوان قسم آخر
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(
'جميع المنتجات',
style: Theme.of(context).textTheme.headlineSmall,
),
),
),
// قائمة المنتجات
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => const ProductListTile(),
childCount: 20,
),
),
],
)
SliverToBoxAdapter باعتدال للعناصر غير المتكررة مثل العناوين واللافتات. لقوائم العناصر، فضّل دائمًا SliverList أو SliverGrid مع مفوضي البناء للحصول على أداء مثالي.
SliverPadding
يضيف SliverPadding حشوًا حول شريحة، بشكل مشابه لكيفية عمل Padding لعناصر الصندوق. لا يمكنك لف شريحة بعنصر Padding عادي—يجب استخدام SliverPadding.
مثال SliverPadding
CustomScrollView(
slivers: [
SliverPadding(
padding: const EdgeInsets.all(16),
sliver: SliverGrid(
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
delegate: SliverChildBuilderDelegate(
(context, index) => Card(
child: Center(child: Text('عنصر \$index')),
),
childCount: 10,
),
),
),
],
)
SliverFillRemaining
يملأ SliverFillRemaining المساحة المتبقية في منطقة العرض. هذا مفيد للغاية لإنشاء تخطيطات حيث يجب أن يتوسع العنصر الأخير لملء أي مساحة متبقية، مثل محتوى التذييل أو حالات الفراغ.
SliverFillRemaining للتذييل
CustomScrollView(
slivers: [
SliverList.list(
children: [
const SizedBox(height: 200, child: Text('العنوان')),
const SizedBox(height: 100, child: Text('المحتوى')),
],
),
// هذا يملأ أي مساحة متبقية
SliverFillRemaining(
hasScrollBody: false,
child: Container(
color: Colors.grey[200],
child: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text('محتوى التذييل'),
SizedBox(height: 24),
Text('حقوق النشر 2024'),
SizedBox(height: 16),
],
),
),
),
),
],
)
hasScrollBody: false على SliverFillRemaining عندما لا يكون عنصره الفرعي عنصرًا قابلاً للتمرير. إذا كان hasScrollBody هو true (الافتراضي)، يُتوقع أن يكون الفرعي قابلاً للتمرير مثل ListView. ضبطه بشكل خاطئ يمكن أن يسبب مشاكل في التخطيط.
دمج شرائح متعددة
القوة الحقيقية لـ CustomScrollView تأتي من دمج شرائح متعددة في صفحة واحدة قابلة للتمرير. إليك مثال شامل:
صفحة قابلة للتمرير معقدة
class ShopPage extends StatelessWidget {
const ShopPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
// شريط تطبيق قابل للطي مع صورة
SliverAppBar(
expandedHeight: 250,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
title: const Text('المتجر الإلكتروني'),
background: Image.network(
'https://example.com/hero.jpg',
fit: BoxFit.cover,
),
),
),
// شريط البحث
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(16),
child: SearchBar(
hintText: 'ابحث عن المنتجات...',
leading: const Icon(Icons.search),
),
),
),
// شرائح الفئات
SliverToBoxAdapter(
child: SizedBox(
height: 50,
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: 8,
itemBuilder: (context, index) => Padding(
padding: const EdgeInsets.only(right: 8),
child: Chip(label: Text('فئة \$index')),
),
),
),
),
// عنوان قسم المميز
_buildSectionHeader(context, 'المميزة'),
// شبكة المميز
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16),
sliver: SliverGrid(
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 0.75,
),
delegate: SliverChildBuilderDelegate(
(context, index) => _buildProductCard(index),
childCount: 4,
),
),
),
// عنوان قسم الأحدث
_buildSectionHeader(context, 'المُضافة حديثًا'),
// قائمة المنتجات الأحدث
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => _buildProductTile(index),
childCount: 15,
),
),
],
),
);
}
Widget _buildSectionHeader(BuildContext context, String title) {
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 24, 16, 8),
child: Text(title,
style: Theme.of(context).textTheme.titleLarge),
),
);
}
Widget _buildProductCard(int index) {
return Card(
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Container(
color: Colors.primaries[index % Colors.primaries.length][100],
child: const Center(child: Icon(Icons.image, size: 48)),
),
),
Padding(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('منتج \$index',
style: const TextStyle(fontWeight: FontWeight.bold)),
Text('\\$\${(index + 1) * 19.99}',
style: const TextStyle(color: Colors.green)),
],
),
),
],
),
);
}
Widget _buildProductTile(int index) {
return ListTile(
leading: Container(
width: 56,
height: 56,
color: Colors.grey[200],
child: const Icon(Icons.shopping_bag),
),
title: Text('منتج \$index'),
subtitle: Text('\\$\${(index + 1) * 9.99}'),
trailing: const Icon(Icons.chevron_right),
);
}
}
فوائد الأداء
توفر الشرائح مزايا أداء كبيرة مقارنة بتداخل عناصر قابلة للتمرير متعددة:
- العرض الكسول: فقط العناصر الفرعية المرئية يتم بناؤها وتخطيطها. العناصر خارج الشاشة لا تستهلك أي موارد.
- متحكم تمرير واحد: جميع الشرائح تتشارك نفس فيزياء التمرير والمتحكم، مما ينشئ تجربة تمرير سلسة وموحدة.
- لا مشاكل تمرير متداخلة: بدلاً من لف
ListViewفيColumnمعshrinkWrap: true(الذي يلغي التحميل الكسول)، تتركب الشرائح بشكل طبيعي. - ذاكرة فعالة: مفوضو البناء يتخلصون من العناصر خارج الشاشة، مما يحافظ على استخدام ذاكرة ثابت بغض النظر عن حجم القائمة.
shrinkWrap: true على ListView داخل Column للقوائم الكبيرة. هذا يجبر Flutter على قياس جميع العناصر مسبقًا، مما يدمر التحميل الكسول. استخدم CustomScrollView مع الشرائح بدلاً من ذلك.
مثال عملي: تمرير لانهائي كسول
تمرير لانهائي مع الشرائح
class InfiniteScrollPage extends StatefulWidget {
const InfiniteScrollPage({super.key});
@override
State<InfiniteScrollPage> createState() => _InfiniteScrollPageState();
}
class _InfiniteScrollPageState extends State<InfiniteScrollPage> {
final List<String> _items = List.generate(20, (i) => 'عنصر \$i');
bool _isLoading = false;
Future<void> _loadMore() async {
if (_isLoading) return;
setState(() => _isLoading = true);
// محاكاة تأخير الشبكة
await Future.delayed(const Duration(seconds: 1));
setState(() {
final start = _items.length;
_items.addAll(
List.generate(20, (i) => 'عنصر \${start + i}'),
);
_isLoading = false;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
const SliverAppBar(
title: Text('تمرير لانهائي'),
floating: true,
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
if (index == _items.length) {
// تفعيل تحميل المزيد
_loadMore();
return const Padding(
padding: EdgeInsets.all(16),
child: Center(
child: CircularProgressIndicator(),
),
);
}
return ListTile(
title: Text(_items[index]),
leading: CircleAvatar(
child: Text('\$index'),
),
);
},
childCount: _items.length + 1,
),
),
],
),
);
}
}
CustomScrollView عندما تحتاج لدمج القوائم والشبكات ومحتوى آخر في تمرير واحد. SliverList وSliverGrid يوفران عرضًا كسولًا فعالًا، وSliverToBoxAdapter يدمج العناصر غير الشريحية، وSliverPadding يضيف تباعدًا، وSliverFillRemaining يتعامل مع المساحة المتبقية في منطقة العرض. فضّل الشرائح على العناصر القابلة للتمرير المتداخلة مع shrinkWrap لأداء أفضل.