القوائم باستخدام LazyColumn في Jetpack Compose

2021-09-02 8 دقائق للقراءة

قبل Jetpack Compose ، اعتدنا على إنشاء RecyclerView مع Adapter لعرض مجموعة كبيرة من القوائم. الآن، نستطيع استخدام LazyColumn أو LazyRow لإظهار مجموعة كبيرة من القوائم عموديًا أو أفقيًا في أسطر قليلة مثلما سنوضح في هذه المدونة.

النسخة الإنجليزية هنا.

ماذا سنبني؟

سنقوم ببناء تطبيق يعرض قائمة بالنباتات الطبيعية المستخدمة في مستحضرات التجميل والعناية بالبشرة.

النتيجة النهائية (الثيم الليلي)

النتيجة النهائية (الثيم النهاري)

الخطوة 1: إنشاء التطبيق

افتح أحدث إصدار من Android Studio -> اختر مشروع جديد (new project) -> اختر “Empty Compose Activity” من نماذج الهواتف والأجهزة اللوحية -> اكتب [اسم مشروعك].

الخطوة 2: إضافة معلومات مؤقتة للعرض

أولاً، قم بإنشاء data class للنبتة، هذا الكلاس يجب أن يحتوي على ID، واسم النبتة، ووصفها، ومرجع صورتها. سيكون مرجع الصورة من نوع Int لأننا سنشير إلى معرف الصورة في ملف res باستخدام كلاس R.

لإنشاء data class نضيف ملف جديد من نوع Kotlin class/file -> نختار data class -> نسمي الملف Plant، ملفك يجب أن تحتوي على الكود التالي بعد كتابة معلومات النبتة كما وضحنا أعلاه:

data class Plant(
   val id: Int,
   val name: String,
   val description: String,
   val imageRes: Int
)

ثانيًا، قم بإضافة ملف جديد من نوع Kotlin class/file -> نختار النوع file -> نسميه Plants، داخل هذا الملف انسخ قائمة النباتات الموجودة هنا، بعدها سترى خطأ في مرجع الصورة وهوا imageRes لأننا لم نضف الصور إلى ملف res بعد، سنفعل ذلك الآن.

ثالثًا، قم بتنزيل صور النباتات من خلال هذا الرابط، ثم داخل Android Studio، انتقل إلى “Resource Manager” -> وفي Drawable ، اضغط على زر الزائد “+” -> اختر “import drawables” -> حدد 12 صورة التي تم تنزيلها مسبقًا على جهازك -> اضغط على استيراد.

بعد هذه الخطوة ستلاحظ أن الخطأ في ملف Plants اختفى.

الخطوة 3: بناء واجهة المستخدم

MainActivity.kt

نرجع لملف “MainActivity” ونحذف دالة “Greeting” واستدعاءاتها من داخل app theme ومن الـ preview أيضاً.

داخل app theme نستدعي دالة “AllPlants” والتي سنكتب تعريفها بعد قليل، ونمرر بداخلها قائمة النباتات “plants”. الكود في ملف “MainActivity” سيكون بهذا الشكل:

class MainActivity : ComponentActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContent {
           YOUR_APP_NAME_Theme {
               Surface(color = MaterialTheme.colors.background) {
                   AllPlants(plants)
               }
           }
       }
   }
}

AllPlants.kt

قم بإضافة ملف جديد من نوع Kotlin class/file -> نختار النوع file -> نسميه AllPlant

داخل هذا الملف، نعرف “AllPlants” وهي دالة مميزة قابلة للتكوين (Composable) وهذا النوع من الدوال هي وحدة البناء الأساسية لعناصر واجهة المستخدم في Jetpack Compose.

دالة AllPlants​​ تأخذ قائمة بالنباتات ليتم عرضها لاحقًا باستخدام LazyColumn. في الوقت الحالي، AllPlants​​ ستكون بهذا الشكل:

@Composable
fun AllPlants(platList: List<Plant>) {}

بالنسبة لتصميم هذا التطبيق، نريد App Bar عادي يعرض اسم التطبيق، ثم تحته نريد عرض قائمة النباتات مع معلوماتها.

ولتحقيق ذلك سنستخدم “Scaffold” وهو دالة قابلة للتكوين (composable) معرفة مسبقاً وتسمح لنا بتقسيم الشاشة إلى App Bar ومحتوى أساسي.

تأخذ الـ Scaffold باراميتر يُسمى topBar والذي بدوره يستقبل دالة قابلة للتكوين أيضاً: ولهذا سنستخدم دالة TopAppBar المعرفة أيضا مسبقاً، والتي تأخذ أيضًا باراميترات معينة للتحكم في شكل وكيفية عرضها.

من هذه البراميترات، نحدد لون خلفية TopAppBar باستخدام backgroundColor ليكون نفس اللون الأساسي للتطبيق. أيضًا، نحدد عنوان الـ TopAppBar ولتحديد العنوان سنستخدم دالة Text ونمرر لها النص المراد عرضه.

للتنويه، دالة Text تأخذ نصاً يمكن أن يكون قيمة ثابتة معرّفة مباشرة في الكود، أو قيمة ديناميكية موجودة في ملف strings.xml مما يساعد​​ في عملية توطين التطبيق (Localization). الآن سنستخدم الطريقة الثانية للإشارة إلى اسم التطبيق.

لتحويل جميع ماتحدثنا عنه إلى كود، سيكون بالشكل التالي:

@Composable
fun AllPlants(platList: List<Plant>) {
   Scaffold(
       topBar = {
           TopAppBar(
               backgroundColor = MaterialTheme.colors.primary,
               title = { Text(stringResource(R.string.app_name)) }
           )
       }
   ) {}
}

الآن بعد أن أصبح عنوان التطبيق جاهزًا على شكل App Bar، سننتقل إلى المحتوى الأساسي الموجود تحته والذي سيكون عنوان نصي بسيط ثم تحته قائمة بالنباتات ومعلوماتها.

لعرض قائمة طويلة من العناصر يُنصح باستخدام LazyColumn أو LazyRow، وبالنسبة لحالتنا، فنحن نريد عرض القائمة عموديًا لذلك سنستخدم LazyColumn.

نريد أن تأخذ هذه القائمة أكبر مساحة متاحة بعد المسافة البادئة أو الـ paading، لذا سنقوم بتعيين العرض ليكون fillMaxWidth باستخدام Modifier.

أيضًا, سنضيف مسافات للمحتوى داخل القائمة بقيمة 16.dp باستخدام ContentPadding حتى لا تلتصق العناصر بحواف الشاشة. كودك سيكون بالشكل هذا:

@Composable
fun AllPlants(platList: List<Plant>) {
   Scaffold(
       topBar = {
           TopAppBar(
               backgroundColor = MaterialTheme.colors.primary,
               title = { Text(stringResource(R.string.app_name)) }
           )
       }
   ) {
       LazyColumn(
           modifier = Modifier.fillMaxWidth(),
           contentPadding = PaddingValues(16.dp)
       ) {}
   }
}

داخل LazyColumn يمكننا تحديد عنصر (item) واحد يمكن رسمه مرة واحدة و عناصر أخرى (items) يمكن رسمها حين يمرر المستخدم عليها.

عنوان نصي (الثيم الليلي)

عنوان نصي (الثيم النهاري)

لتجربة تعريف عنصر واحد سنستخدم “item” داخل LazyColumn ونضيف داخله Row يأخذ مساحة العرض القصوى المتبقية بعد المسافات البادئة والتي نريد أن تكون بقيمة 25.dp والطول سيكون حسب القيمة المعرفة من خلال استخدام wrapContentHeight باستخدام الـ Modifier.

تسمح لنا دالة Row بمحاذاة المحتوى بداخلها في نهاية الشاشة أو بدايتها أو منتصفها.

يمكننا أيضًا تحديد ترتيب أفقي (horizontalArrangement) وهو الترتيب الأفقي للدوال داخل هذه الدالة، والمحاذاة الرأسية (verticalAlignment) وهي المحاذاة الرأسية للدوال داخل هذه الدالة، لنجعل المحاذاة والترتيب تكون في المنتصف جميعها. سيكون الكود بالشكل التالي:

LazyColumn(
   modifier = Modifier.fillMaxWidth(),
   contentPadding = PaddingValues(16.dp)
) {
   item {
       Row(
           modifier = Modifier.fillMaxWidth()
               .wrapContentHeight()
               .padding(vertical = 25.dp),
           horizontalArrangement = Arrangement.Center,
           verticalAlignment = Alignment.CenterVertically
       ) {}
   }
}

داخل Row نريد أن نحدد نصًا لعرض عنوان بسيط قبل قائمة النباتات. سيكون هذ النص نمط عنوان 3 (Heading 3) من MaterialTheme.

LazyColumn(
   modifier = Modifier.fillMaxWidth(),
   contentPadding = PaddingValues(16.dp)
) {
   item {
       Row(
           modifier = Modifier.fillMaxWidth()
               .wrapContentHeight()
               .padding(vertical = 25.dp),
           horizontalArrangement = Arrangement.Center,
           verticalAlignment = Alignment.CenterVertically
       ) {
           Text(
               "\uD83C\uDF3F  Plants in Cosmetics",
               style = MaterialTheme.typography.h3
           )
       }
   }
}

بالإضافة إلى العنصر الموجود داخل LazyColumn يمكننا تحديد العناصر (items) لتأخذ قائمة بيانات وتعرض كل عنصر من عناصر هذه القائمة في دالة قابلة للتكوين. سنقوم بتمرير قائمة النباتات القادمة من MainActivity كقائمة بيانات للـ items().

سنعرض المعلومات لكل نبتة في PlantCard وهي دالة قابلة للتكوين مخصصة وغير معرفة مسبقاً والتي سنقوم بتعريفها في الخطوة التالية.

داخل دالة PlantCard نحتاج إلى تمرير اسم النبتة ووصفها وصورتها حتى نتمكن من عرضها. بعد تطبيق ما ذكرناه ،كودك سيكون كما يلي:

@Composable
fun AllPlants(platList: List<Plant>) {
   Scaffold(
       topBar = {
           TopAppBar(
               backgroundColor = MaterialTheme.colors.primary,
               title = { Text(stringResource(R.string.app_name)) }
           )
       }
   ) {
       LazyColumn(
           modifier = Modifier.fillMaxWidth(),
           contentPadding = PaddingValues(16.dp)
       ) {
           item {
               Row(
                   modifier = Modifier.fillMaxWidth()
                       .wrapContentHeight()
                       .padding(vertical = 25.dp),
                   horizontalArrangement = Arrangement.Center,
                   verticalAlignment = Alignment.CenterVertically
               ) {
                   Text(
                       "\uD83C\uDF3F  Plants in Cosmetics",
                       style = MaterialTheme.typography.h3
                   )
               }
           }
           items(platList) { plant ->
               PlantCard(plant.name, plant.description, plant.imageRes)
           }
       }
   }
}

PlantCard.kt

قم بإضافة ملف جديد من نوع Kotlin class/file -> نختار النوع file -> نسميه PlantCard.

داخل هذا الملف عرف دالة “PlantCard” القابلة للتكوين باستخدام المعلومات التي مررناها مسبقًا في ملف AllPlant كالتالي:

@Composable
fun PlantCard(name: String, description: String, image: Int) {}

بطاقة النبتة

لإنشاء التصميم أعلاه، سنستخدم دالة Card المعرفة مسبقاً، ونعيّن مظهر هذه البطاقة باستخدام Modifier بحيث تحتوي على مسافة بادئة (padding) بقيمة 10.dp من جميع الجوانب وتأخذ أقصى عرض متبقي بعد هذه المسافة.

بالنسبة للارتفاع، سنستخدم wrapContentHeight لأن محتوى بعض البطاقات أطول من غيرها.

لتحديد شكل البطاقة، يُفضّل أن نستخدم الأشكال من MaterialTheme والتي سنخصصها لاحقًا في تدوينة أخرى، في الوقت الحالي، لنختر شكلاً متوسطًا.

يتحكم الارتفاع (elevation) في حجم الظل الموجود أسفل البطاقة، لنحدد قيمته 5.dp.

نريد أيضًا تحديد لون خلفية البطاقة لتأخذ قيمة surface من MaterialTheme والتي سنفصل فيها في تدوينة أخرى.

بعد تحويل جميع ماذكرناه إلى كود سيكون هكذا:

@Composable
fun PlantCard(name: String, description: String, image: Int) {
   Card(
       modifier = Modifier
           // The space between each card and the other
           .padding(10.dp)
           .fillMaxWidth()
           .wrapContentHeight(),
       shape = MaterialTheme.shapes.medium,
       elevation = 5.dp,
       backgroundColor = MaterialTheme.colors.surface
   ) {}
}

في التصميم أعلاه يمكننا تحويل محتوى البطاقة إلى صف (Row) مع صورة (Image) وعمود (Column). يعرض العمود نصين عموديًا -اسم ووصف النبات-.

لنبدأ بدالة الصورة Image والتي تعرض صورة على الشاشة وتأخد باراميتر لصورة بصيغة png و jpg يتم استرجاعها من ملف res بإعطاء رمز الصورة في painterResource ونحن في الأصل مررنا هذا الرمز كقيمة imageRes من AllPlants حتى نتمكن من استخدامه هنا.

براميتر contentDescription يجب أن يكون معرفاً ويستقبل نص لقراءة الصورة في إمكانيات الوصول.

يجب علينا أيضًا أن نحدد حجم الصورة للتأكد من عرضها بشكل صحيح سنعطي الحجم قيمة 130.dp ومسافة بادئة بقيمة 8.dp باستخدام Modifier.

بالنسبة لبراميتر contentScale نريد أن تكون الصورة كاملة معروضة داخل الحجم الذي عرفناه سابقاً باستخدام قيمة FIT.

كودك سيكون بهذا الشكل:

@Composable
fun PlantCard(name: String, description: String, image: Int) {
   Card(
       modifier = Modifier.padding(10.dp)
           .fillMaxWidth()
           .wrapContentHeight(),
       shape = MaterialTheme.shapes.medium,
       elevation = 5.dp,
       backgroundColor = MaterialTheme.colors.surface
   ) {
       Row(
           verticalAlignment = Alignment.CenterVertically,
       ) {
           Image(
               painter = painterResource(id = image),
               contentDescription = null,
               modifier = Modifier.size(130.dp)
                   .padding(8.dp),
               contentScale = ContentScale.Fit,
           )
       }
   }
}

بجانب الصورة، نريد تقديم نصين بشكل عمودي داخل دالة (Column) والتي نعطيها مسافة بادئة بقيمة 8.dp وقت تعريفها.

بعد ذلك، نضيف نصاً باستخدام دالة Text تأخذ اسم النبتة الذي مررناه في ملف AllPlant، ونعيّن نمط النص ليكون عنوان رقم 4 (Heading 4).

أضف نصاً آخراً باستخدام دالة Text يأخذ وصف النبتة الذي تم تمريره في ملف AllPlant ، وقم بتعيين نمط النص ليكون نصًا رقم 2 (Body 2).

سيكون كودك هكذا:

@Composable
fun PlantCard(name: String, description: String, image: Int) {
   Card(
       modifier = Modifier.padding(10.dp)
           .fillMaxWidth()
           .wrapContentHeight(),
       shape = MaterialTheme.shapes.medium,
       elevation = 5.dp,
       backgroundColor = MaterialTheme.colors.surface
   ) {
       Row(
           verticalAlignment = Alignment.CenterVertically,
       ) {
           Image(
               painter = painterResource(id = image),
               contentDescription = null,
               modifier = Modifier.size(130.dp)
                   .padding(8.dp),
               contentScale = ContentScale.Fit,
           )
           Column(Modifier.padding(8.dp)) {
               Text(
                   text = name,
                   style = MaterialTheme.typography.h4,
                   color = MaterialTheme.colors.onSurface,
               )
               Text(
                   text = description,
                   style = MaterialTheme.typography.body2,
               )
           }
       }
   }
}

النتيجة

هذه النتيجة النهائية للتطبيق في الوضع النهاري والليلي.

عنوان نصي (الثيم الليلي)

عنوان نصي (الثيم النهاري)

👩‍💻 التطبيق كاملاً على GitHub.

خاتمة

في هذه التدوينة، تعلمنا كيفية عرض قائمة باستخدام LazyColumn، وأثناء ذلك تعاملنا مع العديد من دوال Jetpack Compose الأساسية لبناء واجهة المستخدم هذه. التدوينة التالية سنشرح تخصيص الثيمات في Jetpack Compose مستخدمين هذا التطبيق.