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

التنقل بين الشاشات

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

التنقل بين الشاشات

كل تطبيق Android غير بسيط يحتوي على أكثر من شاشة واحدة. يمنحك Android أداتين متكاملتين للتنقل بينها: واجهة Intent الكلاسيكية — المتاحة منذ Android 1.0 — ومكوّن Navigation، وهو مكتبة Jetpack تفرض هيكلًا قائمًا على الرسم البياني وقابلًا للتوقع على انتقالات الـ Fragment والـ Activity. عليك فهم الاثنين: فالـ Intents لا مفر منها (تتعامل مع التنقل بين التطبيقات وإجراءات النظام)، ومكوّن Navigation هو المعيار الحالي للتنقل داخل التطبيق.

Intents: أساس التنقل في Android

الـ Intent هو كائن رسالة يصف الإجراء الذي تريد تنفيذه. حين تمرّره إلى startActivity()، يعثر نظام Android على مكوّن مناسب للتعامل معه ويبدأ تشغيله.

هناك نوعان من الـ Intent:

  • Intent صريح (Explicit) — تُسمّي فيه فئة الـ Activity المستهدفة بالاسم. يُستخدم للتنقل داخل التطبيق.
  • Intent ضمني (Implicit) — تُعلن فيه إجراءً (مثل Intent.ACTION_VIEW) وتترك للنظام مهمة إيجاد تطبيق قادر على التعامل معه. يُستخدم لفتح رابط في المتصفح، أو مشاركة محتوى، أو إجراء مكالمة، وما إلى ذلك.

Intents الصريحة: الانتقال إلى Activity أخرى

النمط الأدنى لفتح DetailActivity من MainActivity بسيط:

// داخل MainActivity.java public void openDetail(View view) { Intent intent = new Intent(this, DetailActivity.class); startActivity(intent); }

المعامل الأول لمُنشئ الـ Intent هو Context — هنا يُشير this إلى الـ Activity الحالية التي تُعدّ Context بحد ذاتها. المعامل الثاني هو كائن الفئة الخاص بالـ Activity الوجهة.

المكدس الخلفي (Back Stack): كل استدعاء لـ startActivity() يدفع الـ Activity الجديدة إلى المكدس الخلفي. حين يضغط المستخدم زر الرجوع أو يستدعي finish()، تُزال الـ Activity العلوية ويستأنف نشاط العمل السابق. يُدار هذا المكدس تلقائيًا ونادرًا ما تحتاج إلى التدخل فيه مباشرةً.

تمرير البيانات عبر Extras

تحمل الـ Intents حزمة Bundle من أزواج المفاتيح/القيم تُسمى extras. استخدمها لإرسال بيانات أساسية إلى الـ Activity الوجهة:

// في الـ Activity المُرسِلة Intent intent = new Intent(this, DetailActivity.class); intent.putExtra("EXTRA_ITEM_ID", 42); intent.putExtra("EXTRA_ITEM_NAME", "Laptop"); startActivity(intent);
// في DetailActivity.java (داخل onCreate) @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_detail); Intent incoming = getIntent(); int itemId = incoming.getIntExtra("EXTRA_ITEM_ID", -1); String name = incoming.getStringExtra("EXTRA_ITEM_NAME"); TextView title = findViewById(R.id.tvTitle); title.setText(name + " (id=" + itemId + ")"); }
عرّف مفاتيح الـ extras كثوابت. أعلنها كحقول public static final String في فئة الـ Activity الوجهة — مثلاً DetailActivity.EXTRA_ITEM_ID. يُشير المُرسِل والمُستقبِل حينها إلى نفس الثابت مما يُزيل أخطاء الكتابة.

Intents الضمنية: مغادرة تطبيقك

لفتح صفحة ويب لا تكتب متصفحًا — بل تُطلق Intent ضمنيًا ويوجّهه النظام إلى المتصفح المثبّت:

Uri page = Uri.parse("https://developer.android.com"); Intent browserIntent = new Intent(Intent.ACTION_VIEW, page); startActivity(browserIntent);

قبل استدعاء startActivity() على Intent ضمني يجب التحقق من وجود تطبيق قادر على التعامل معه، وإلا فستواجه استثناء ActivityNotFoundException:

if (browserIntent.resolveActivity(getPackageManager()) != null) { startActivity(browserIntent); } else { Toast.makeText(this, "No app can handle this action", Toast.LENGTH_SHORT).show(); }

استقبال نتيجة: ActivityResult API

أحيانًا تفتح نشاطًا ثانيًا وتحتاج نتيجة في المقابل — كمنتقي صور أو شاشة إعدادات. الطريقة الحديثة (التي تحلّ محل startActivityForResult المهجور) تستخدم ActivityResultLauncher:

public class MainActivity extends AppCompatActivity { private ActivityResultLauncher<Intent> pickItemLauncher; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // سجّل قبل بدء الـ Activity pickItemLauncher = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result -> { if (result.getResultCode() == RESULT_OK && result.getData() != null) { String picked = result.getData().getStringExtra("PICKED_VALUE"); // استخدام القيمة المُختارة } } ); } public void launchPicker(View view) { Intent intent = new Intent(this, PickerActivity.class); pickItemLauncher.launch(intent); } }

في PickerActivity، استدع setResult() قبل finish():

Intent result = new Intent(); result.putExtra("PICKED_VALUE", "Item A"); setResult(RESULT_OK, result); finish();
لا تستدعِ startActivityForResult() أبدًا. هذه الدالة مهجورة في API 29+. سجّل دائمًا ActivityResultLauncher في onCreate() — تسجيله في مرحلة لاحقة (بعد onStart()) يُطلق استثناء IllegalStateException.

مكوّن Navigation

حين يحتوي تطبيقك على شاشات كثيرة، تصبح إدارة شبكة من الـ Intents الصريحة وأعلام المكدس الخلفي عرضةً للأخطاء. يعالج مكوّن Jetpack Navigation هذا من خلال تمثيل تنقل تطبيقك كـرسم بياني للتنقل تصريحي — ملف XML يُدرج الوجهات (الـ Fragments) والإجراءات (الحواف) التي تربطها.

أضف التبعية إلى build.gradle (الوحدة app):

dependencies { def nav_version = "2.7.7" implementation "androidx.navigation:navigation-fragment:$nav_version" implementation "androidx.navigation:navigation-ui:$nav_version" }

إنشاء رسم بياني للتنقل

انقر بزر الماوس الأيمن على res < New < Android Resource File، اختر النوع Navigation، وسمّه nav_graph.xml. في محرر XML أعلن الوجهات والإجراءات بينها:

<?xml version="1.0" encoding="utf-8"?> <navigation xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/nav_graph" app:startDestination="@id/listFragment"> <fragment android:id="@+id/listFragment" android:name="com.example.app.ListFragment" android:label="Item List"> <action android:id="@+id/action_list_to_detail" app:destination="@id/detailFragment" /> </fragment> <fragment android:id="@+id/detailFragment" android:name="com.example.app.DetailFragment" android:label="Item Detail"> <argument android:name="itemId" app:argType="integer" android:defaultValue="-1" /> </fragment> </navigation>

استضافة الرسم البياني: NavHostFragment

يحتاج تخطيط الـ Activity الرئيسية إلى NavHostFragment — الحاوية التي تتبادل فيها الـ Fragments عند التنقل:

<!-- activity_main.xml --> <androidx.fragment.app.FragmentContainerView android:id="@+id/nav_host_fragment" android:name="androidx.navigation.fragment.NavHostFragment" android:layout_width="match_parent" android:layout_height="match_parent" app:defaultNavHost="true" app:navGraph="@navigation/nav_graph" />

تعترض app:defaultNavHost="true" زر الرجوع لنظام التشغيل، فيتولّى مكوّن Navigation إدارة إزالة عناصر المكدس الخلفي تلقائيًا.

التنقل عبر NavController

داخل أي Fragment مستضاف في الرسم البياني، احصل على NavController واستدع navigate() بمعرّف الإجراء:

// في ListFragment.java import androidx.navigation.Navigation; public class ListFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_list, container, false); Button btnGo = view.findViewById(R.id.btnGoToDetail); btnGo.setOnClickListener(v -> { // تمرير المعطيات باستخدام Bundle Bundle args = new Bundle(); args.putInt("itemId", 101); Navigation.findNavController(v) .navigate(R.id.action_list_to_detail, args); }); return view; } }

في DetailFragment، استرجع المعطى من getArguments():

// في DetailFragment.java @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); int id = getArguments() != null ? getArguments().getInt("itemId", -1) : -1; // استخدام id لتحميل البيانات }
استخدم Safe Args لأمان الأنواع. يتضمّن مكوّن Navigation مكوّن Gradle إضافيًا يُسمى Safe Args يولّد فئات معطيات مكتوبة بأنواع محددة (مثلاً ListFragmentDirections.actionListToDetail(101)). يُزيل هذا مفاتيح الـ Bundle المكتوبة كنصوص ويمنحك أخطاء وقت الترجمة عند وجود أنواع معطيات خاطئة.

Intent مقابل مكوّن Navigation: متى تستخدم كلًّا منهما

  • استخدم Intents في: بدء تطبيق آخر (متصفح، كاميرا، هاتف)، والروابط العميقة من الإشعارات، والتنقل بين الـ Activities الرئيسية (مثلاً شاشة الإعداد الأوّلي ← التطبيق الرئيسي).
  • استخدم مكوّن Navigation في: جميع انتقالات الشاشة داخل التطبيق ضمن Activity واحد يستضيف عدة Fragments. يتعامل مع المكدس الخلفي والانتقالات المتحركة وعناوين URL للروابط العميقة في مكان واحد.

الخلاصة

الـ Intents هي نظام الرسائل في Android — تُسمّي الـ Intents الصريحة فئةً بالاسم، بينما تُعلن الـ Intents الضمنية إجراءً وتترك للنظام مهمة التوجيه. تُعدّ واجهة ActivityResultLauncher الطريقة الحالية لاستقبال البيانات من شاشة أخرى. أما مكوّن Jetpack Navigation فيحلّ محل مجموعة مشتتة من استدعاءات الـ Intent بواسطة رسم بياني تنقل واحد: أعلن وجهاتك في XML، واستضفها في NavHostFragment، وتنقّل باستدعاء NavController.navigate() مع معرّف الإجراء. يُغطّي هذان الأسلوبان معًا كل سيناريو تنقل ستواجهه في تطبيق Android حقيقي.