واجهة أندرويد والأنشطة والتنقّل

الـ Fragments

18 دقيقة الدرس 6 من 12

الـ Fragments

مع نمو تطبيق Android الخاص بك إلى ما هو أبعد من شاشة واحدة، تكتشف سريعًا أن استخدام Activity واحدة لكل شاشة أمر خشن للغاية. تحتاج الأجهزة اللوحية إلى عرض قائمة رئيسية بجانب لوحة تفاصيل في آنٍ واحد، في حين تعرض الهواتف كلًّا منهما بالتتابع. إن تكرار منطق التخطيط داخل Activity منفصلتين أمر غير قابل للصيانة. حلّ Android هذه المشكلة عبر Fragment — وهو قطعة مستقلة وقابلة لإعادة الاستخدام من واجهة المستخدم والسلوك تعيش داخل Activity لكنها تدير عرضها الخاص ودورة حياتها الخاصة.

ما هو الـ Fragment

Fragment هو فئة في androidx.fragment.app تمثّل جزءًا من واجهة المستخدم أو السلوك داخل Activity. فكّر فيه كشاشة فرعية معيارية: لديه ملف XML خاص بتخطيطه، وفئة Java خاصة به، ودورة حياة خاصة تعمل متداخلة داخل دورة حياة Activity المضيفة. يمكن لـ Activity واحدة استضافة عدة Fragments، وتبديلها ذهابًا وإيابًا، وحتى إعادة استخدام نفس فئة Fragment في عدة Activities.

لماذا تُعدّ الـ Fragments مهمة: بنية Android الحديثة — Jetpack Navigation وViewPager2 وتبويبات التنقل السفلية وتخطيطات master-detail — مبنية بالكامل على الـ Fragments. حتى إن لم تُدر مكدس التراجع بنفسك مباشرةً، فالإطار يستخدم الـ Fragments تحت الغطاء في كل مكان.

إنشاء Fragment

تُجاوز فئة Fragment الدالة onCreateView() لنفخ تخطيطها، وتختياريًا onViewCreated() لإعداد العروض بعد النفخ. يجب أن لا تبحث عن عروض أو تضبط مستمعات في المنشئ — إذ لا يوجد العرض بعد وقت الإنشاء.

// ProfileFragment.java package com.example.myapp; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; public class ProfileFragment extends Fragment { @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { // نفخ التخطيط الخاص بهذا الـ Fragment return inflater.inflate(R.layout.fragment_profile, container, false); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); // يمكن البحث عن العروض وضبطها هنا بأمان TextView nameView = view.findViewById(R.id.tv_name); nameView.setText("Edrees Salih"); } }

ملف التخطيط المقابل res/layout/fragment_profile.xml هو ملف XML عادي للتخطيط بعنصر جذر — مثل ConstraintLayout — تمامًا كما تكتبه لـ Activity.

إضافة Fragment إلى Activity

تُدار الـ Fragments عبر FragmentManager. يمكنك إما تعريف Fragment بشكل ثابت في ملف XML الخاص بتخطيط Activity، أو إضافته ديناميكيًا أثناء التشغيل عبر FragmentTransaction. النهج الديناميكي هو ما ستحتاجه دائمًا تقريبًا لأنه يتيح لك استبدال الـ Fragments وتحريكها.

التعريف الثابت (XML للتخطيط) — الأبسط، لكن لا يمكن استبداله أثناء التشغيل:

<!-- res/layout/activity_main.xml --> <androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/fragment_container" android:name="com.example.myapp.ProfileFragment" android:layout_width="match_parent" android:layout_height="match_parent" />

المعاملة الديناميكية (Java) — النهج المرن:

// MainActivity.java import androidx.appcompat.app.AppCompatActivity; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; import android.os.Bundle; public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // يحتوي على FragmentContainerView فارغ // تجنّب إضافة الـ Fragment مجددًا عند تغيير الإعدادات (الدوران) if (savedInstanceState == null) { FragmentManager fm = getSupportFragmentManager(); FragmentTransaction tx = fm.beginTransaction(); tx.add(R.id.fragment_container, new ProfileFragment(), "profile"); tx.commit(); } } }
احرص دائمًا على حماية إضافة الـ Fragment بـ savedInstanceState == null. عند دوران الجهاز، يُعيد Android إنشاء Activity ويستعيد تلقائيًا أي Fragments كانت مُضافة للمكدس. إن أضفت الـ Fragment مجددًا دون هذا الشرط، ستنتهي بنسخ مكررة من الـ Fragment مُكدّسة فوق بعضها.

دورة حياة الـ Fragment

تعمل دورة حياة Fragment إلى جانب دورة حياة Activity المضيفة، لكنها تحتوي على استدعاءات راجعة إضافية خاصة بعلاقتها مع Activity وعرضها الخاص:

  1. onAttach(context) — ارتبط الـ Fragment بالـ Activity. السياق Context متاح الآن.
  2. onCreate(savedInstanceState) — أُنشئ الـ Fragment. هنا تُهيّئ الحالة غير المرئية (ViewModels، المعطيات).
  3. onCreateView() — انفخ وأعد هرمية عرض الـ Fragment.
  4. onViewCreated(view, savedInstanceState) — العرض جاهز. اضبط المستمعات، راقب LiveData، واملأ واجهة المستخدم هنا.
  5. onStart() — أصبح الـ Fragment مرئيًا.
  6. onResume() — الـ Fragment في المقدمة وتفاعلي.
  7. onPause() — فقد الـ Fragment التركيز (جاء Fragment أو Activity آخر إلى المقدمة).
  8. onStop() — لم يعد الـ Fragment مرئيًا.
  9. onDestroyView() — يجري تدمير هرمية العرض. أطلق أي مراجع للعروض هنا لتجنب تسرّب الذاكرة.
  10. onDestroy() — يُدمَّر الـ Fragment نفسه.
  11. onDetach() — فُصل الـ Fragment عن الـ Activity.
الفرق بين onDestroyView وonDestroy: يمكن تدمير عرض الـ Fragment وإعادة إنشائه (مثلًا عند وضعه في مكدس التراجع) بينما تبقى نسخة الـ Fragment نفسها حيّة. لهذا يجب إلغاء أي مراجع للعروض في onDestroyView(). يُسهّل ViewBinding ذلك بنمط: binding = null; في onDestroyView().

معطيات الـ Fragment — نمط newInstance

لا تمرّر البيانات إلى Fragment أبدًا عبر منشئ بمعاملات. سيُعيد Android إنشاء الـ Fragment عبر منشئه الافتراضي عند استعادة الحالة، مما يُفقد أي معطيات مُمرَّرة عبر المنشئ. النهج الصحيح هو نمط المصنع newInstance: احزم البيانات في Bundle وأرفقها عبر setArguments().

public class DetailFragment extends Fragment { private static final String ARG_USER_ID = "user_id"; // دالة المصنع — استدعِها بدلًا من المنشئ public static DetailFragment newInstance(int userId) { DetailFragment fragment = new DetailFragment(); Bundle args = new Bundle(); args.putInt(ARG_USER_ID, userId); fragment.setArguments(args); return fragment; } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); int userId = requireArguments().getInt(ARG_USER_ID); // تحميل البيانات لـ userId ... } }

في موضع الاستدعاء داخل Activity:

getSupportFragmentManager() .beginTransaction() .replace(R.id.fragment_container, DetailFragment.newInstance(42)) .addToBackStack(null) // الضغط على زر الرجوع سيعود لهذا الـ Fragment .commit();
استخدم replace() مع addToBackStack() عند التنقل للأمام. replace() يستبدل الـ Fragment الحالي؛ وaddToBackStack() يضمن أن زر الرجوع يعكس المعاملة ويعيد الـ Fragment السابق، مانحًا المستخدمين تجربة التنقل التي يتوقعونها.

متى تستخدم الـ Fragments

  • تخطيطات متعددة الأجزاء: عرض القائمة والتفاصيل جنبًا إلى جنب على الأجهزة اللوحية؛ جزء واحد في كل مرة على الهواتف.
  • تنقل بالتبويبات: كل تبويب هو Fragment داخل ViewPager2 أو شريط التنقل السفلي.
  • أقسام واجهة مستخدم قابلة لإعادة الاستخدام: شريط أدوات مشترك، أو عرض خريطة، أو قسم تعليقات يظهر على شاشات متعددة.
  • Jetpack Navigation: يُدير مكوّن Navigation تطبيقًا بـ Activity واحدة حيث تكون كل شاشة وجهة Fragment — وهذا هو النهج المعماري الحديث الموصى به.

الخلاصة

الـ Fragment هو مكوّن واجهة مستخدم معياري وقابل لإعادة الاستخدام يعيش داخل Activity. لديه دورة حياة خاصة (onCreateView وonViewCreated وonDestroyView هي أهم الاستدعاءات الراجعة لأعمال واجهة المستخدم)، وتخطيطه الخاص، وحالته الخاصة. تُمرَّر البيانات عبر نمط bundle في newInstance لا عبر معطيات المنشئ. معاملات Fragment — add وreplace وaddToBackStack — هي الآلية التي تُؤلّف بها Activity عدة Fragments وتتنقل بينها. في الدرس القادم ستستخدم مكوّن Jetpack Navigation الذي يبني على الـ Fragments لمنحك رسم بياني كامل للتنقل بين الشاشات.