الوحدة 2 · التخطيط والتنسيق
قوائم تبقى سريعة: FlatList وSectionList
22 دقراءة 4 دبيئة تجريب الكود
الدرس في لمحة
اعرض سجلّ الرحلات المتنامي في Trailhead باستخدام FlatList وSectionList، وافهم الـ virtualization، وطبّق التحسينات القليلة التي تُبقي القوائم الطويلة سلسة على الهواتف متوسطة الفئة.
- 1FlatList: ارسم ما هو مرئي
- 2كيف يعمل الـ virtualization
- 3الحفاظ على السلاسة
- 4SectionList: بيانات مجمّعة
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
يبدأ سجلّ Trailhead بنحو اثنتي عشرة رحلة. المتنزّه المتحمّس يسجّل ثلاث رحلات أسبوعيًا؛ وبعد أربع سنوات يتجاوز السجلّ 600 صف، لكلٍّ منها صورة مصغّرة وعنوان وبعض الإحصاءات. اعرض ذلك باستخدام ScrollView و.map()، وسيبني التطبيق الـ native views لكل صف قبل أن يعرض أول إطار. على هاتف رائد قد لا تلاحظ. أما على هواتف Android متوسطة الفئة التي يستخدمها معظم العالم، فستحصل على شاشة فارغة وتقطّع.
FlatList: ارسم ما هو مرئي
يأخذ FlatList بياناتك ودالة ترسم عنصرًا واحدًا، ولا يركّب إلا الصفوف القريبة من منطقة العرض:
import { FlatList, Text, View } from 'react-native';
import { HikeRow } from '../components/HikeRow';
import { useHikes } from '../state/hikes';
export default function HikeLog() {
const hikes = useHikes();
return (
<FlatList
data={hikes}
keyExtractor={(hike) => hike.id}
renderItem={({ item }) => <HikeRow hike={item} />}
ItemSeparatorComponent={() => <View style={{ height: 1, backgroundColor: '#e2e8e4', marginLeft: 80 }} />}
ListEmptyComponent={<Text style={{ padding: 24, textAlign: 'center' }}>No hikes yet. Log your first one!</Text>}
contentContainerStyle={{ paddingBottom: 24 }}
/>
);
}مثل ScrollView، يحتاج FlatList إلى ارتفاع محدود، لذا يجب أن يعطيه أبوه flex: 1 (والشاشة في Expo Router تفعل ذلك أصلًا). تستقبل renderItem الكائن { item, index }. ويتيح لك ListHeaderComponent وListFooterComponent أن تتمرّر بطاقة ملخّص أو مؤشّر «تحميل المزيد» مع القائمة، بدلًا من تغليف الـ FlatList داخل ScrollView، وهذا يُبطل الـ virtualization كليًا ويُطلق تحذيرًا.
كيف يعمل الـ virtualization
يحتفظ FlatList بـ نافذة رسم (render window): الصفوف الظاهرة على الشاشة، زائد مخزون احتياطي فوقها وتحتها. وأثناء التمرير، تُركَّب الصفوف التي تدخل النافذة وتُزال التي تخرج منها، ويحلّ محلّها فراغ بالارتفاع الصحيح.
ينتج عن ذلك أمران. أولًا، يجب أن تكون المفاتيح (keys) ثابتة: يستخدمها FlatList وReact لمعرفة أي صفّ هو أي صفّ بينما تأتي الصفوف وتذهب، فاستخدم معرّفًا من بياناتك. فهارس المصفوفة تنكسر لحظة إدراج رحلة جديدة في الأعلى. ثانيًا، الصفوف تنسى الـ state المحلية حين تُزال. علامة «موسَّع» المحفوظة في useState داخل الصف تعود إلى قيمتها الأولى بعد تمرير طويل. ارفع هذه الـ state إلى الأب (expandedId) أو إلى بياناتك.
الحفاظ على السلاسة
معظم التقطّع في القوائم يأتي من إعادة الـ render لصفوف لم تتغيّر، أو من عمل ثقيل داخلها. بالترتيب بحسب العائد:
- طبّق memo على الصف. صدّر
HikeRowمغلّفًا بـmemo، فلا يُعاد الـ render للصف إلا حين يتغيّر الـ prop المسمّىhike. (إن كان مشروعك يفعّل React Compiler، فهو يتولّى الـ memoization نيابةً عنك.) - أبقِ
renderItemثابتة. الدالة السهمية المباشرة لا بأس بها مع صفّ مغلّف بـ memo، لكن لا تنشئ كائنات جديدة للـ props في كل render، مثلstyle={{…}}أوhike={{ ...item }}، فهي تُبطلmemo. - لا عمل ثقيل داخل الصفوف. نسّق التواريخ واحسب الإحصاءات مرة واحدة، حين تتغيّر البيانات، لا في الـ render الخاص بكل صف.
- صور بالحجم المناسب. صورة بعرض 4000 بكسل مصغّرة داخل صورة مصغّرة بعرض 56 نقطة تكلّف ذاكرة لكل صفّ مرئي. اطلب صورًا مصغّرة من الـ API، أو استخدم
expo-imageمع التخزين المؤقت. - ارتفاعات ثابتة؟ استخدم
getItemLayout. إن كان كل صف 72 نقطة زائد فاصل بنقطة واحدة، يستطيع FlatList تخطّي القياس:
const ROW = 73;
<FlatList
// …
getItemLayout={(_, index) => ({ length: ROW, offset: ROW * index, index })}
/>وللسجلّات المحفوظة على خادم، لا تجلب الرحلات الـ 600 كلها دفعة واحدة. يُطلَق onEndReached حين يقترب المستخدم من الأسفل بالتمرير (ومدى الاقتراب يحدّده onEndReachedThreshold، ويُقاس بارتفاعات الشاشة)، وهذه هي اللحظة لطلب الصفحة التالية وإلحاقها بـ data. اعرض مؤشّر تحميل في ListFooterComponent أثناء التحميل، واحمِ الكود من إطلاق الطلب مرتين.
ضبط windowSize وinitialNumToRender يأتي في النهاية، وبعد أن تقيس فقط. وللقوائم الضخمة جدًا أو المعقّدة، تنتقل فرق كثيرة إلى FlashList من Shopify، الذي يعيد تدوير views الصفوف بدلًا من تركيب صفوف جديدة، ويحافظ على واجهة برمجية شبيهة بـ FlatList.
SectionList: بيانات مجمّعة
يجمّع Trailhead الرحلات بحسب الشهر، مع ترويسة لاصقة لكل شهر. يفعل SectionList ذلك انطلاقًا من بيانات بهذا الشكل:
const sections = [
{ title: 'October 2026', data: [ridgeLoop, lakeTrail] },
{ title: 'September 2026', data: [oldMill] },
];<SectionList
sections={sections}
keyExtractor={(hike) => hike.id}
renderItem={({ item }) => <HikeRow hike={item} />}
renderSectionHeader={({ section }) => <Text style={styles.header}>{section.title}</Text>}
stickySectionHeadersEnabled
/>الجزء المثير للاهتمام ليس الـ component؛ بل تحويل مصفوفة مسطّحة من الرحلات إلى شكل sections هذا، مرتّبة بشكل صحيح. وهذا هو تمرينك.
يبدأ القسم 3 بعد ذلك: مع وجود شاشات حقيقية للتنقّل بينها، يحتاج Trailhead إلى نظام تنقّل (navigation).
أهم النقاط
ScrollViewمع.map()يركّب (mount) كل الصفوف دفعة واحدة؛ أماFlatListفيركّب فقط نافذة من الصفوف حول الجزء المرئي.- أعطِ
FlatListدالةkeyExtractorتُرجع معرّفًا ثابتًا من بياناتك، لا فهرس المصفوفة أبدًا. - الصفوف في القائمة الافتراضية (virtualized) تُزال حين تبتعد بالتمرير، لذا احفظ state الصف في بياناتك، لا داخل component الصف.
- طبّق memo على component الصف، وأبقِ
renderItemثابتة، وأضفgetItemLayoutحين يكون لكل الصفوف الارتفاع نفسه. - يأخذ
SectionListالخاصيةsectionsبشكل{ title, data }ويعرض قوائم مجمّعة بترويسات لاصقة.