أساسيات ودجات Flutter

ودجت النص والطباعة

45 دقيقة الدرس 3 من 18

ودجت Text

ودجت Text هي واحدة من أكثر الودجت استخداماً في Flutter. تعرض سلسلة نصية بنمط واحد. للنصوص الأكثر تعقيداً مع أنماط متعددة، يوفر Flutter RichText وText.rich.

ودجت Text أساسية

import 'package:flutter/material.dart';

class TextExamples extends StatelessWidget {
  const TextExamples({super.key});

  @override
  Widget build(BuildContext context) {
    return const Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text('نص بسيط بتنسيق افتراضي'),
        Text(
          'نص عريض وأكبر',
          style: TextStyle(
            fontSize: 24,
            fontWeight: FontWeight.bold,
          ),
        ),
        Text(
          'نص ملون ومائل',
          style: TextStyle(
            fontSize: 18,
            fontStyle: FontStyle.italic,
            color: Colors.blue,
          ),
        ),
      ],
    );
  }
}

TextStyle — تحكم كامل في التنسيق

TextStyle يمنحك تحكماً كاملاً في كيفية ظهور النص. إليك أهم الخصائص:

خصائص TextStyle

Text(
  'مثال على نص منسق',
  style: TextStyle(
    fontSize: 20,                    // الحجم بالبكسل المنطقي
    fontWeight: FontWeight.w600,     // من 100 إلى 900 (عريض = 700)
    fontStyle: FontStyle.italic,     // عادي أو مائل
    color: Colors.deepPurple,        // لون النص
    backgroundColor: Colors.yellow.shade100, // تمييز الخلفية
    letterSpacing: 2.0,              // المسافة بين الحروف
    wordSpacing: 4.0,               // المسافة بين الكلمات
    height: 1.5,                     // مُضاعف ارتفاع السطر
    decoration: TextDecoration.underline,    // خط تحتي أو وسطي أو فوقي
    decorationColor: Colors.red,     // لون خط الزخرفة
    decorationStyle: TextDecorationStyle.wavy, // صلب، مزدوج، منقط، متقطع، متموج
    decorationThickness: 2.0,        // سُمك الزخرفة
    shadows: [
      Shadow(
        color: Colors.black26,
        offset: Offset(2, 2),
        blurRadius: 4,
      ),
    ],
  ),
)

مقياس FontWeight

يوفر Flutter ثوابت مُسماة لأوزان الخطوط، تتراوح من رفيع (100) إلى أسود (900):

أمثلة على وزن الخط

Column(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    Text('رفيع (w100)', style: TextStyle(fontWeight: FontWeight.w100)),
    Text('خفيف (w300)', style: TextStyle(fontWeight: FontWeight.w300)),
    Text('عادي (w400)', style: TextStyle(fontWeight: FontWeight.w400)),
    Text('متوسط (w500)', style: TextStyle(fontWeight: FontWeight.w500)),
    Text('شبه عريض (w600)', style: TextStyle(fontWeight: FontWeight.w600)),
    Text('عريض (w700)', style: TextStyle(fontWeight: FontWeight.bold)),
    Text('عريض جداً (w800)', style: TextStyle(fontWeight: FontWeight.w800)),
    Text('أسود (w900)', style: TextStyle(fontWeight: FontWeight.w900)),
  ],
)

TextAlign واتجاه النص

تحكم في كيفية محاذاة النص داخل حاويته باستخدام textAlign. لاحظ أن المحاذاة تعمل نسبةً لعرض النص المتاح — يجب أن يكون لودجت Text عرض محدد (أو أن يكون في تخطيط ممتد).

محاذاة النص

SizedBox(
  width: double.infinity,
  child: Column(
    children: [
      Text('محاذاة لليسار', textAlign: TextAlign.left),
      Text('محاذاة للوسط', textAlign: TextAlign.center),
      Text('محاذاة لليمين', textAlign: TextAlign.right),
      Text(
        'النص المضبوط يوزع الكلمات بالتساوي عبر العرض الكامل '
        'للحاوية مما يجعل كلا الحافتين متراصتين بشكل أنيق.',
        textAlign: TextAlign.justify,
      ),
    ],
  ),
)

TextOverflow و maxLines

عندما يكون النص طويلاً جداً لحاويته، يمكنك التحكم في كيفية تجاوزه باستخدام overflow وmaxLines:

التعامل مع تجاوز النص

Column(
  children: [
    // علامة حذف في النهاية
    Text(
      'هذا نص طويل جداً سيتم اقتطاعه بعلامة حذف في النهاية عندما يتجاوز حاويته',
      maxLines: 1,
      overflow: TextOverflow.ellipsis,
    ),

    // تلاشي
    Text(
      'هذا النص يتلاشى عند الحافة عندما يتجاوز حدود حاويته',
      maxLines: 1,
      overflow: TextOverflow.fade,
      softWrap: false,
    ),

    // قص النص
    Text(
      'هذا النص يُقص عندما يتجاوز المساحة المتاحة',
      maxLines: 1,
      overflow: TextOverflow.clip,
    ),

    // متعدد الأسطر مع حد
    Text(
      'هذه فقرة أطول ستعرض حتى سطرين من النص '
      'ثم تعرض علامة حذف إذا كان هناك محتوى '
      'أكثر لعرضه بعد هذين السطرين.',
      maxLines: 2,
      overflow: TextOverflow.ellipsis,
    ),
  ],
)
نصيحة: TextOverflow.ellipsis هو أسلوب التجاوز الأكثر استخداماً في تطبيقات الإنتاج. يشير بوضوح للمستخدمين بوجود محتوى إضافي. ادمجه مع maxLines للتحكم بعدد الأسطر المعروضة بدقة.

RichText و TextSpan

عندما تحتاج لعرض نص بـأنماط متعددة في فقرة واحدة، استخدم RichText أو الاختصار Text.rich. فئة TextSpan تمثل قسماً من النص بنمطه الخاص.

RichText بأنماط متعددة

// استخدام Text.rich (الاختصار المفضل)
Text.rich(
  TextSpan(
    style: const TextStyle(fontSize: 16, color: Colors.black87),
    children: [
      const TextSpan(text: 'Flutter هو '),
      TextSpan(
        text: 'جميل',
        style: TextStyle(
          color: Colors.blue.shade700,
          fontWeight: FontWeight.bold,
        ),
      ),
      const TextSpan(text: '، '),
      TextSpan(
        text: 'سريع',
        style: TextStyle(
          color: Colors.green.shade700,
          fontWeight: FontWeight.bold,
        ),
      ),
      const TextSpan(text: '، و'),
      TextSpan(
        text: 'منتج',
        style: TextStyle(
          color: Colors.orange.shade700,
          fontWeight: FontWeight.bold,
          decoration: TextDecoration.underline,
        ),
      ),
      const TextSpan(text: '.'),
    ],
  ),
)

// استخدام RichText مباشرة
RichText(
  text: TextSpan(
    style: DefaultTextStyle.of(context).style,
    children: const [
      TextSpan(text: 'السعر: '),
      TextSpan(
        text: '\$29.99',
        style: TextStyle(
          fontWeight: FontWeight.bold,
          color: Colors.green,
        ),
      ),
      TextSpan(
        text: ' \$49.99',
        style: TextStyle(
          decoration: TextDecoration.lineThrough,
          color: Colors.grey,
        ),
      ),
    ],
  ),
)
ملاحظة: عند استخدام RichText مباشرة، فإنه لا يرث نمط النص الافتراضي من شجرة الودجت. يجب عليك تعيين النمط الأساسي يدوياً. اختصار Text.rich يرث النمط الافتراضي تلقائياً مما يجعله النهج المفضل في معظم الحالات.

SelectableText

بشكل افتراضي، النص في Flutter غير قابل للتحديد. استخدم SelectableText عندما تريد أن يتمكن المستخدمون من تحديد النص ونسخه:

ودجت نص قابل للتحديد

Column(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    // نص قابل للتحديد أساسي
    const SelectableText(
      'يمكنك تحديد ونسخ هذا النص بالضغط المطول عليه.',
      style: TextStyle(fontSize: 16),
    ),

    const SizedBox(height: 16),

    // قابل للتحديد مع مؤشر وشريط أدوات مخصص
    SelectableText(
      'API Key: sk-abc123def456ghi789',
      style: const TextStyle(
        fontFamily: 'monospace',
        fontSize: 14,
        backgroundColor: Color(0xFFF5F5F5),
      ),
      cursorColor: Colors.blue,
      showCursor: true,
      onTap: () => debugPrint('تم النقر على النص'),
    ),

    const SizedBox(height: 16),

    // نص غني قابل للتحديد
    const SelectableText.rich(
      TextSpan(
        children: [
          TextSpan(text: 'الحالة: '),
          TextSpan(
            text: 'نشط',
            style: TextStyle(
              color: Colors.green,
              fontWeight: FontWeight.bold,
            ),
          ),
        ],
      ),
    ),
  ],
)

DefaultTextStyle

DefaultTextStyle يعيّن نمط نص افتراضي لجميع ودجت Text في شجرته الفرعية. هذا مفيد لتطبيق تنسيق متسق على مجموعة من ودجت النص بدون تكرار النمط على كل واحدة.

استخدام DefaultTextStyle

DefaultTextStyle(
  style: const TextStyle(
    fontSize: 16,
    color: Colors.black87,
    height: 1.6,
  ),
  child: Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      // جميعها ترث النمط الافتراضي
      const Text('الفقرة الأولى بالتنسيق الافتراضي.'),
      const SizedBox(height: 8),
      const Text('الفقرة الثانية ترث نفس النمط.'),
      const SizedBox(height: 8),
      // تجاوز خصائص محددة
      Text(
        'نص عريض يرث أيضاً النمط الأساسي.',
        style: TextStyle(
          fontWeight: FontWeight.bold,
          color: Colors.blue.shade800,
        ),
      ),
    ],
  ),
)

حزمة Google Fonts

توفر حزمة google_fonts الوصول إلى أكثر من 1,000 خط من Google Fonts. أضفها إلى pubspec.yaml واستخدمها مباشرة في أنماطك.

استخدام Google Fonts

// pubspec.yaml
// dependencies:
//   google_fonts: ^6.1.0

import 'package:google_fonts/google_fonts.dart';

class FontShowcase extends StatelessWidget {
  const FontShowcase({super.key});

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          'عنوان Roboto Slab',
          style: GoogleFonts.robotoSlab(
            fontSize: 28,
            fontWeight: FontWeight.bold,
          ),
        ),
        const SizedBox(height: 8),
        Text(
          'Lato لنص الجسم يبدو نظيفاً وعصرياً.',
          style: GoogleFonts.lato(
            fontSize: 16,
            height: 1.6,
          ),
        ),
        const SizedBox(height: 8),
        Text(
          'var code = "monospace";',
          style: GoogleFonts.firaCode(
            fontSize: 14,
            color: Colors.green.shade800,
            backgroundColor: Colors.grey.shade100,
          ),
        ),
        const SizedBox(height: 8),
        Text(
          'خط عرض أنيق',
          style: GoogleFonts.playfairDisplay(
            fontSize: 32,
            fontStyle: FontStyle.italic,
          ),
        ),
      ],
    );
  }
}

// تطبيق Google Fonts على السمة بالكامل
MaterialApp(
  theme: ThemeData(
    textTheme: GoogleFonts.interTextTheme(),
  ),
)
نصيحة: لتطبيقات الإنتاج، فكر في استخدام GoogleFonts.config.allowRuntimeFetching = false; وتضمين الخطوط كأصول. هذا يمنع تنزيل الخطوط في وقت التشغيل ويضمن توفر الخطوط دائماً بدون اتصال.

مثال عملي: مقال منسق

لندمج كل شيء لبناء ودجت مقال منسقة بشكل جميل:

ودجت مقال منسق

class StyledArticle extends StatelessWidget {
  final String title;
  final String author;
  final String date;
  final String body;
  final String category;

  const StyledArticle({
    super.key,
    required this.title,
    required this.author,
    required this.date,
    required this.body,
    required this.category,
  });

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(20),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // شارة الفئة
          Container(
            padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
            decoration: BoxDecoration(
              color: Colors.blue.shade50,
              borderRadius: BorderRadius.circular(16),
            ),
            child: Text(
              category.toUpperCase(),
              style: TextStyle(
                fontSize: 11,
                fontWeight: FontWeight.w700,
                color: Colors.blue.shade700,
                letterSpacing: 1.2,
              ),
            ),
          ),
          const SizedBox(height: 12),
          // العنوان
          Text(
            title,
            style: const TextStyle(
              fontSize: 28,
              fontWeight: FontWeight.w800,
              height: 1.2,
              letterSpacing: -0.5,
            ),
          ),
          const SizedBox(height: 12),
          // المؤلف والتاريخ
          Text.rich(
            TextSpan(
              style: TextStyle(fontSize: 14, color: Colors.grey.shade600),
              children: [
                const TextSpan(text: 'بواسطة '),
                TextSpan(
                  text: author,
                  style: const TextStyle(fontWeight: FontWeight.w600),
                ),
                TextSpan(text: ' \u2022 \$date'),
              ],
            ),
          ),
          const SizedBox(height: 20),
          const Divider(),
          const SizedBox(height: 20),
          // نص الجسم
          Text(
            body,
            style: TextStyle(
              fontSize: 16,
              height: 1.8,
              color: Colors.grey.shade800,
            ),
            textAlign: TextAlign.justify,
          ),
        ],
      ),
    );
  }
}

مثال عملي: سيرة ذاتية بنص غني

بناء قسم سيرة ذاتية بأنماط مختلطة وروابط ونطاقات تفاعلية:

ودجت سيرة ذاتية بنص غني

class RichTextBio extends StatelessWidget {
  final String name;
  final String role;
  final String bio;
  final List<String> skills;

  const RichTextBio({
    super.key,
    required this.name,
    required this.role,
    required this.bio,
    required this.skills,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(24),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(16),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.05),
            blurRadius: 10,
            offset: const Offset(0, 4),
          ),
        ],
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            name,
            style: const TextStyle(
              fontSize: 24,
              fontWeight: FontWeight.bold,
            ),
          ),
          const SizedBox(height: 4),
          Text(
            role,
            style: TextStyle(
              fontSize: 16,
              color: Colors.blue.shade600,
              fontWeight: FontWeight.w500,
            ),
          ),
          const SizedBox(height: 16),
          Text(
            bio,
            style: TextStyle(
              fontSize: 15,
              height: 1.7,
              color: Colors.grey.shade700,
            ),
          ),
          const SizedBox(height: 16),
          Text.rich(
            TextSpan(
              style: const TextStyle(fontSize: 14),
              children: [
                const TextSpan(
                  text: 'المهارات: ',
                  style: TextStyle(fontWeight: FontWeight.w600),
                ),
                ...skills.asMap().entries.map((entry) => TextSpan(
                  children: [
                    TextSpan(
                      text: entry.value,
                      style: TextStyle(
                        color: Colors.blue.shade700,
                        fontWeight: FontWeight.w500,
                      ),
                    ),
                    if (entry.key < skills.length - 1)
                      const TextSpan(text: ' \u2022 '),
                  ],
                )),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

الملخص

في هذا الدرس، تعلمت:

  • ودجت Text تعرض نصاً بنمط واحد مع تحكم كامل في TextStyle
  • خصائص TextStyle تشمل fontSize وfontWeight وcolor وletterSpacing وheight وdecoration وshadows
  • TextAlign يتحكم في المحاذاة الأفقية؛ TextOverflow وmaxLines يتعاملان مع التجاوز
  • RichText وText.rich مع TextSpan يمكّنان النص متعدد الأنماط في ودجت واحدة
  • SelectableText يسمح للمستخدمين بتحديد ونسخ محتوى النص
  • DefaultTextStyle يطبق تنسيقاً متسقاً على جميع ودجت Text في شجرة فرعية
  • حزمة google_fonts توفر الوصول لأكثر من 1,000+ خط
ما التالي: في الدرس التالي، سنستكشف ودجت الصور والأيقونات، ونتعلم كيفية عرض الصور من مصادر مختلفة واستخدام الأيقونات بفعالية في تطبيقات Flutter الخاصة بك.