تطوير الجوّال

أول شاشات Jetpack Compose: قائمة، نموذج، وتنقّل

6 دقائق للقراءة

🔝 الدليل الرئيسي: Kotlin وJetpack Compose في 2026 · الدرس السابق: تثبيت Android Studio Otter 3

بمجرد تثبيت Android Studio وفتح مشروع Compose، الانعكاس الأول المفيد هو بناء ثلاث أو أربع شاشات ملموسة: قائمة، نموذج، تفاصيل، والتنقّل الذي يربطها. يستعرض هذا الدرس هذا البناء خطوة بخطوة مع Jetpack Compose 2026.05.00 وMaterial 3، مع التوقف عند المفاهيم التي تتكرر في كل المشاريع: composables، إدارة الحالة، قوائم مُحسَّنة، تنقّل type-safe، وthème. في النهاية، ستحصل على تطبيق صغير وظيفي يخدم كأساس قابل لإعادة الاستخدام.

المتطلبات

  • Android Studio Otter 3 (2025.2.3) مُثبَّت ووظيفي
  • مشروع Compose فارغ مُنشأ عبر template Empty Activity
  • محاكي Android 16 (API 36) أو جهاز فعلي مربوط
  • أساسيات Kotlin (دوال، أصناف، lambdas)
  • الوقت المُقدَّر: 90 دقيقة

الخطوة 1 — فهم هيكل نقطة الدخول

قبل كتابة composables، لننظر إلى ما ولّده الـ template. ملف MainActivity.kt يحوي ComponentActivity تستدعي onCreate ثم setContent { ... }. الكتلة المُمرَّرة لـ setContent هي نقطة دخول Compose: كل ما يُعرض على الشاشة ينحدر من هنا.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            HelloKotlinTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    Greeting(
                        name = "Android",
                        modifier = Modifier.padding(innerPadding)
                    )
                }
            }
        }
    }
}

خمسة عناصر للتذكّر. enableEdgeToEdge() يمد UI تحت أشرطة النظام (status + navigation)، معيار Material 3. HelloKotlinTheme هو thème الـ template: يُعرّف اللوحة، الطباعة، والأشكال. Scaffold حاوية Material 3 تُدير TopAppBar وBottomBar وFAB وpadding النظام. innerPadding يعكس المساحة المحجوزة من Scaffold لتجنّب التداخل (notch، أشرطة).

الخطوة 2 — كتابة أول composable

composable دالة Kotlin مُعلَّقة بـ @Composable. تستطيع استدعاء composables أخرى، قراءة حالات، وإصدار عناصر UI.

@Composable
fun WelcomeCard(
    title: String,
    description: String,
    modifier: Modifier = Modifier
) {
    Card(
        modifier = modifier
            .fillMaxWidth()
            .padding(16.dp)
    ) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(
                text = title,
                style = MaterialTheme.typography.titleLarge
            )
            Spacer(modifier = Modifier.height(8.dp))
            Text(
                text = description,
                style = MaterialTheme.typography.bodyMedium
            )
        }
    }
}

@Preview(showBackground = true)
@Composable
fun WelcomeCardPreview() {
    HelloKotlinTheme {
        WelcomeCard(
            title = "Mon premier composable",
            description = "Une carte Material 3 affichée par Compose."
        )
    }
}

ثلاثة اصطلاحات مهمة. أولًا، modifier: Modifier = Modifier هو المعامل الافتراضي لأي composable قابل لإعادة الاستخدام: يُتيح للمُستدعي وضع، تحديد حجم، وتنسيق. ثانيًا، نقرأ thème عبر MaterialTheme.typography وMaterialTheme.colorScheme — أبدًا ألوان أو أحجام مُشفَّرة. أخيرًا، @Preview يجعل composable مرئيًا في لوحة المعاينة دون إطلاق التطبيق.

الخطوة 3 — عرض قائمة بـ LazyColumn

لعرض قائمة عناصر، لا نستخدم Column مع حلقة for: سيُركّب كل العناصر في الذاكرة دفعة واحدة. بدلًا، LazyColumn لا يُركّب إلا العناصر المرئية، مثل RecyclerView سابقًا.

data class Article(val id: Int, val titre: String, val resume: String)

@Composable
fun ListeArticles(
    articles: List<Article>,
    onArticleClick: (Article) -> Unit,
    modifier: Modifier = Modifier
) {
    LazyColumn(
        modifier = modifier.fillMaxSize(),
        contentPadding = PaddingValues(vertical = 8.dp)
    ) {
        items(
            items = articles,
            key = { it.id }
        ) { article ->
            WelcomeCard(
                title = article.titre,
                description = article.resume,
                modifier = Modifier.clickable { onArticleClick(article) }
            )
        }
    }
}

أربع نقاط مفتاحية. items(items, key = ...) يُوفّر مفتاحًا مستقرًا لكل عنصر — لا غنى عنه ليُحدّد Compose عنصرًا عبر إعادات التركيب. contentPadding يُضيف مسافة علوية/سفلية دون تجاوز العناصر. clickable يُحوّل أي composable لمنطقة قابلة للنقر مع تأثير ripple Material 3 تلقائي. lambda onArticleClick يرفع الحدث للأب بدلًا من معالجة التنقّل محليًا (state hoisting).

الخطوة 4 — إدارة الحالة بـ remember وmutableStateOf

الحالة قلب Compose. لحالة محلية وعابرة (حقل نموذج، حالة فتح قائمة)، نستخدم remember { mutableStateOf(...) }.

@Composable
fun FormulaireRecherche(
    onRechercher: (String) -> Unit,
    modifier: Modifier = Modifier
) {
    var texte by remember { mutableStateOf("") }

    Column(modifier = modifier.padding(16.dp)) {
        OutlinedTextField(
            value = texte,
            onValueChange = { texte = it },
            label = { Text("Mot-clé") },
            modifier = Modifier.fillMaxWidth(),
            singleLine = true
        )
        Spacer(modifier = Modifier.height(12.dp))
        FilledTonalButton(
            onClick = { onRechercher(texte) },
            enabled = texte.isNotBlank(),
            modifier = Modifier.align(Alignment.End)
        ) {
            Text("Rechercher")
        }
    }
}

فكرتان مركزيتان. by remember { mutableStateOf("") } مع تفويض Kotlin: texte يتصرّف كـ var عادي، لكن أي قراءة أثناء إعادة التركيب يتتبّعها Compose، وأي كتابة تُطلق إعادة تركيب فقط لـ composables التي قرأت القيمة. enabled = texte.isNotBlank() يُوضح قوة النموذج: الزر يتعطّل تلقائيًا عندما يكون الحقل فارغًا، دون مستمع يدوي.

للملاحظة: remember ينجو من إعادات التركيب، لكن ليس من تغييرات الإعداد (دوران، موت العملية). لذلك، استخدم rememberSaveable { mutableStateOf("") }. القاعدة العملية: كل حالة يجب أن تنجو من دوران تمر عبر rememberSaveable أو ترتفع إلى ViewModel.

الخطوة 5 — التنقّل بين الشاشات بـ Navigation Compose

Navigation Compose 2.12 يُقدّم API type-safe: نُعلن routes كأصناف أو objects Kotlin قابلة للتسلسل، والمُجمِّع يتحقق من الوسيطات. لا مزيد من سلاسل سحرية للتسلسل.

@Serializable object Liste
@Serializable data class Detail(val articleId: Int)

@Composable
fun AppNavigation() {
    val navController = rememberNavController()

    NavHost(
        navController = navController,
        startDestination = Liste
    ) {
        composable<Liste> {
            ListeArticles(
                articles = articlesExemple,
                onArticleClick = { article ->
                    navController.navigate(Detail(article.id))
                }
            )
        }
        composable<Detail> { backStackEntry ->
            val detail: Detail = backStackEntry.toRoute()
            DetailArticle(
                articleId = detail.articleId,
                onRetour = { navController.popBackStack() }
            )
        }
    }
}

ثلاثة مكاسب رئيسية. الـ routes objects/data classes مُعلَّقة @Serializable: لا خطر typo. navController.navigate(Detail(id)) يُمرّر argument مُنمَّط. backStackEntry.toRoute<Detail>() يستردّ argument مُنمَّط على الوجهة. التنقّل يُدير تلقائيًا الـ stack (زر back، إيماءة العودة)، والرسم المتحرك للانتقال يُطبَّق افتراضيًا.

الخطوة 6 — تخصيص thème Material 3

@Composable
fun HelloKotlinTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    dynamicColor: Boolean = false,
    content: @Composable () -> Unit
) {
    val colorScheme = when {
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            val context = LocalContext.current
            if (darkTheme) dynamicDarkColorScheme(context)
            else dynamicLightColorScheme(context)
        }
        darkTheme -> darkColorScheme(primary = Color(0xFF1E88E5))
        else -> lightColorScheme(primary = Color(0xFF1565C0))
    }

    MaterialTheme(
        colorScheme = colorScheme,
        typography = Typography,
        content = content
    )
}

القاعدة الذهبية للألوان: لا نُشفّر أبدًا لونًا في composable. نقرأه من MaterialTheme.colorScheme (primary، secondary، surface، onSurface). يضمن ذلك تماسك light/dark ويُسهّل تطوّرات العلامة التجارية المستقبلية.

الخطوة 7 — اختبار أول تفاعل كامل

@Composable
fun Compteur(modifier: Modifier = Modifier) {
    var valeur by rememberSaveable { mutableIntStateOf(0) }
    Row(
        modifier = modifier.padding(16.dp),
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.spacedBy(12.dp)
    ) {
        FilledTonalIconButton(onClick = { valeur-- }) {
            Icon(Icons.Default.Remove, contentDescription = "Décrémenter")
        }
        Text(
            text = valeur.toString(),
            style = MaterialTheme.typography.headlineMedium
        )
        FilledTonalIconButton(onClick = { valeur++ }) {
            Icon(Icons.Default.Add, contentDescription = "Incrémenter")
        }
    }
}

أطلق التطبيق، انقر الأزرار وراقب القيمة تتغيّر. أدر الشاشة: rememberSaveable وmutableIntStateOf (متغيّر مُحسَّن للأعداد الصحيحة، يتجنّب autoboxing) يحفظان الحالة. لقد صادقت كل اللبنات الأساسية: composables، حالة، إعادة تركيب، Material 3، إمكانية الوصول (contentDescription)، والنجاة من تغييرات الإعداد.

أخطاء شائعة

العَرَض السبب الحل
الشاشة لا تُحدَّث عند تغيير الحالة var x = ... بدل var x by remember { mutableStateOf(...) } دائمًا غلّف الحالة المُراقبة في mutableStateOf
قائمة بطيئة على 1000 عنصر Column + حلقة بدل LazyColumn الانتقال لـ LazyColumn مع key مستقر
وميض/تحريك عند إضافة عنصر لا key في items() قدّم key = { it.id }
حقل نص يفقد قيمته عند الدوران remember بدل rememberSaveable rememberSaveable أو ارفع لـ ViewModel
زر لا يتعطّل منطق enabled مُقيَّم خارج قراءة حالة تأكّد أن الشرط يقرأ mutableStateOf
Compose preview فارغة composable يقرأ LocalContext غير متوفر في preview غلّف بـ CompositionLocalProvider أو حقن كمعامل

التكيّف مع أحجام شاشة مختلفة

Compose responsive بالبناء. Modifier.fillMaxWidth() يتكيّف تلقائيًا مع العرض المتاح، سواء على Galaxy Fold مطوي (380 dp) أو مفتوح (840 dp). للـ layouts مختلفة فعلًا حسب صنف الشاشة، currentWindowAdaptiveInfo() يُوفّر WindowSizeClass (Compact / Medium / Expanded).

للصور، Image مع contentScale = ContentScale.Crop يُغطّي الغالبية. للصور الشبكية، أضف Coil 3: implementation("io.coil-kt.coil3:coil-compose:3.4.0") واستخدم AsyncImage(model = url, contentDescription = ...). Coil يُدير cache القرص، cache الذاكرة، وplaceholders أصلًا.

الأسئلة الشائعة

هل نحتاج معرفة XML لـ Compose؟
لا. Compose يحل محل XML لطبقة UI. سترى XML للموارد (ألوان، سلاسل، رسومات متجهية) لكن ليس للـ layouts.

متى نستخدم Column / Row / Box؟
Column يكدّس عموديًا، Row أفقيًا، Box يتراكب. للـ layouts المعقدة، ConstraintLayout Compose موجود لكن نادر الاستخدام.

كيف نُمرّر callback لـ composable ابن؟
دائمًا كمعامل، أبدًا عبر متغيّر عام. هذا نمط state hoisting.

أي حجم للأزرار؟
Material 3 يفرض 48 dp أدنى لأي منطقة لمس (توصية إمكانية الوصول).

كيف نعرض dialogue؟
AlertDialog مع var showDialog by remember { mutableStateOf(false) } نُبدّله من زر.

كم composable لكل ملف؟
لا قاعدة صارمة. ملف لكل شاشة مع sub-composables خاصة. إذا تجاوز composable 100 سطر، قسّمه.

مقالات ذات صلة

Sponsoriser ce contenu

Cet emplacement est à vous

Position premium en fin d'article — c'est l'instant où les lecteurs sont le plus engagés. Réservez cet espace pour votre marque, votre formation ou votre offre.

Recevoir nos tarifs
Publicité