أسباب لاعتماد بنية النشاط الواحد (Single-Activity) في Android مع Navigation Component
بدلًا من أن يمثّل كل Activity شاشة واحدة، ننظر إلى الـ Activity كحاوية كبيرة، وتمثّل الـ fragments داخلها الشاشات.

كل مطوري Android يعرفون الـ Activity، بل قد يكون أول ما تستخدمونه في عالم تطوير Android. ويعتمد عدد الـ Activities في تطبيقنا عادة على مدى تعقيد التصميم. ومنذ الإعلان عن Jetpack في Google I/O 2018، صار الحديث عن بنية النشاط الواحد (Single Activity Architecture) حاضرًا، ويبدو أن فريق Google أراد أن تصبح هذه البنية هي الخيار المفضّل.
نقدّم اليوم Navigation component كإطار عمل لهيكلة واجهة المستخدم داخل التطبيق، مع التركيز على جعل التطبيق ذي الـ Activity الواحد البنية المفضّلة.
من https://android-developers.googleblog.com/2018/05/use-android-jetpack-to-accelerate-your.html?m=1 باختصار، بنية النشاط الواحد (Single-Activity Architecture) هي البنية التي تحتوي على Activity واحد فقط، أو عدد قليل نسبيًا من الـ Activities. فبدلًا من أن يمثّل كل Activity شاشة واحدة، ننظر إلى الـ Activity كحاوية كبيرة، وتمثّل الـ fragments داخلها الشاشات. لكن لماذا قد نرغب في استخدام بنية النشاط الواحد؟ وما المشكلات في البنى التي استخدمناها سابقًا؟
1. هل سبق أن أنشأتم Activity لا يحتوي إلا على Fragment واحد لمجرد أن أحدهم قال لكم إنها أفضل ممارسة؟
في رأيي، استخدام Fragment داخل Activity هو فعلًا من أفضل الممارسات، ويمكننا إعادة استخدام هذا الـ Fragment في Activities أخرى أيضًا. لكن إذا كنا نعمل على واجهة لا تُستخدم إلا في مكان واحد، فهل ما زلنا نحتاج إلى إنشاء Activity بداخله Fragment واحد، أم ننشئ Activity بلا أي Fragment؟ مع بنية النشاط الواحد يزول هذا التساؤل، لأن تطبيقنا سيحتوي على Activity واحد فقط.
2. لطالما كانت هناك مشكلة في حركة الانتقال بين الـ Activities.
انظروا إلى شريط الحالة أثناء الانتقال بين الـ Activities، ستلاحظون أنه يومض. وحل هذه المشكلة ليس مباشرًا كما قد تظنون، إذ عليكم إضافة بعض المقتطفات غير البديهية.
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_flash_fix_a);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Fade fade = new Fade();
fade.excludeTarget(R.id.appBar, true);
fade.excludeTarget(android.R.id.statusBarBackground, true);
fade.excludeTarget(android.R.id.navigationBarBackground, true);
getWindow().setEnterTransition(fade);
getWindow().setExitTransition(fade);
}
//Button setup
}
لا يوجد شيء مكتوب عن هذا في التوثيق الرسمي، ومع ذلك تحتاجون فعلًا إلى إضافته ليبدو الانتقال سلسًا. (يمكنكم قراءة المزيد عنه هنا (https://mikescamell.com/shared-element-transitions-part-5/) أما في الانتقال بين الـ Fragments فلا تظهر هذه المشكلة.
3. مشاركة البيانات بين الـ Activities
في أغلب الأحيان، حين نريد مشاركة البيانات بين الـ Activities، نضعها في Singleton لحفظ البيانات مثل Application Class. والمشكلة أن البيانات تصبح في نطاق التطبيق (Application Scope)، وهو نطاق يستطيع الوصول إليه أيضًا كل من Service وContent Provider. ونحن نريد مشاركة البيانات بين الـ Activities فقط، لا مع العناصر الأخرى. فكيف تساعدنا بنية النشاط الواحد على حل هذه المشكلة؟ لنلقِ نظرة على الصورة أدناه.
عند استخدام بنية النشاط الواحد، تتم مشاركة البيانات على مستوى الـ Fragments، وكل الـ Fragments موجودة داخل الـ Activity. وبهذا تبقى البيانات المشتركة بعيدة عن العناصر الأخرى في نطاق التطبيق، أي لا يستطيع Service أو Content Provider الوصول إليها.
4. تمرير الوسائط إلى الـ Fragment قد يكون مرهقًا أحيانًا.
تخيلوا موقفًا تحتاجون فيه إلى تمرير معلومات إلى الـ Fragment، بعضها قد يكون null وبعضها لا. قد ينتهي بكم الأمر بكود يشبه هذا:
fun newInstance(
obj: Object1? = null,
id: Int,
minimumPrice: Int? = null,
promotionId: Int? = null,
selectionId: Int? = null,
itemPosition: Int? = null
): SomeFragment {
val fragment = PizzaOptionFragment()
fragment.<em>arguments </em>= Bundle().<em>apply </em><strong>{</strong>
<strong> </strong>putParcelable(Constants.Intent.Obj1, obj)
putInt(Constants.Intent.ID, id)
minimumPromotionPrice?.<em>let </em><strong>{ </strong>putInt(Constants.Intent.Promotion.MINIMUM_PRICE, <strong>it</strong>)<strong>}</strong>
<strong> </strong>promotionId?.<em>let </em><strong>{ </strong>putInt(Constants.Intent.Promotion.PROMOTION_ID, <strong>it</strong>) <strong>}</strong>
<strong> </strong>selectionId?.<em>let </em><strong>{ </strong>putInt(Constants.Intent.Promotion.SELECTION_ID, <strong>it</strong>) <strong>}</strong>
<strong> </strong>itemPosition?.<em>let </em><strong>{ </strong>putInt(Constants.Intent.Promotion.ITEM_POSITION, <strong>it</strong>) <strong>}</strong>
<strong> }</strong>
<strong> </strong>return fragment
}
يأتي Navigation component بميزة اسمها Safe Args Gradle Plugin.
إضافة تولّد أصنافًا بسيطة من نوع object وbuilder للوصول الآمن من حيث النوع (type-safe) إلى الوسائط المحددة للوجهات والإجراءات.
إليكم 3 خطوات بسيطة لتمرير وسيط إلى Fragment باستخدام Safe Args Gradle Plugin. لنفترض أننا نريد تمرير وسيط URL من BerryFragment إلى BerryDetailFragment
- عرّفوا الوسيط في الـ Fragment الوجهة داخل Navigation Graph.
- تمرير البيانات (المقتطف داخل BerryFragment). إذا كان هناك أكثر من وسيط، فستُضاف كمعاملات للدالة.
val direction = BerryListFragmentDirections
.actionBerryListFragmentToBerryDetailFragment(item.url)
navController.navigate(direction)
- استرجاع البيانات (المقتطف داخل BerryDetailFragment)
val args: BerryDetailFragmentArgs = <em>arguments</em>?.<em>let</em><strong>{</strong>
<strong> </strong>BerryDetailFragmentArgs.fromBundle(<strong>it</strong>)
<strong> }</strong>
val berryUrl: String = args.url
5. روابط عميقة (deep links) بسهولةتتيح لنا مكتبة Navigation component تعريف مخطط الـ deep link مباشرة في الـ Fragment الذي نريد الوصول إليه**.** ويمكن تعريف رابط الـ deep link في خطوتين بسيطتين. - عرّفوا رابط الـ deep link في Navigation Graph.
- عرّفوا الـ navigation graph في ملف Manifest.
<manifest …>
<uses-permission android:name="android.permission.INTERNET"/>
<application …>
<activity …>
<nav-graph android:value=“@navigation/main_navigation"/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
ويمكن أيضًا استخدام المخطط المعرّف للتنقل داخل التطبيق عبر NavController.
الخلاصة
إلى جانب ما ذكرناه أعلاه، تأتي مكتبة Navigation Component مع مكتبة إضافية اسمها Navigation UI Libs. وهي تساعد في إدارة التنقل عبر شريط التطبيق العلوي، والقائمة الجانبية (navigation drawer)، وشريط التنقل السفلي.**هل هناك مشكلات في استخدام بنية النشاط الواحد مع Navigation component؟**استخدمنا هذه البنية في عدة تطبيقات في بيئة الإنتاج، ولم نواجه أي مشكلات حتى الآن. ربما تتساءلون: ماذا لو أردنا تمرير البيانات ذهابًا وإيابًا بين الـ Fragments، كما في startActivityForResult؟ في السابق كنا نستخدم ViewModel مشتركًا للتواصل بين الـ fragments. لكن Google أضافت مؤخرًا قدرة جديدة إلى FragmentManager تتيح له العمل كمخزن مركزي لنتائج الـ fragments، فصار بإمكاننا تمرير البيانات بين الـ Fragments بسهولة. يمكنكم قراءة المزيد عنها هنا https://developer.android.com/training/basics/fragments/pass-data-between#kotlin إذا كنتم على وشك بدء تطبيق جديد، فأعتقد أن بنية النشاط الواحد مع Navigation component تستحق التجربة. أما إذا أردتم استخدامها في تطبيق قائم يضم Activities كثيرة، فيمكنكم البدء بتحويل مسار واحد إلى هذه البنية. ففي مسار المصادقة مثلًا، بدلًا من وجود Activity منفصل لتسجيل الدخول وآخر للتسجيل وهكذا، يمكنكم دمجها في Activity واحد، مع Fragment يمثّل كل شاشة في المسار.


