تشغيل Fragment للحصول على نتائج في Android
أضافت Google مؤخرًا قدرة جديدة إلى FragmentManager تجعله يعمل كمخزن مركزي لنتائج الـ fragments.

بصفتنا مطوّري Android، لا بد أننا جميعًا نعرف الدالة startActivityForResult. نستخدم هذه الدالة لتشغيل Activity جديدة واستلام النتائج في الـ Fragment أو الـ Activity الحالية، ما يسهّل التواصل بين الـ fragment والـ activity. لكن عندما تكون شاشات تطبيقي ممثّلة بـ Fragments، لا أستطيع استخدام startActivityForResult لتمرير النتائج بينها.
حتى وقت قريب، لم يكن هناك ما يشبه startActivityForResult عندما يتعلق الأمر بالتواصل بين الـ Fragments. لذلك كنت مضطرًا لإنشاء ViewModel مشترك بين الـ Fragments لأتمكن من تمرير البيانات ذهابًا وإيابًا بينها. أما القدرة الجديدة فتتيح لنا التعامل مع النتائج بين الـ fragments بطريقة مشابهة لتعاملنا معها بين الـ activities باستخدام startActivityForResult، لكنها مصمَّمة خصيصًا لسياق الـ fragment.
لحسن الحظ، أضافت Google مؤخرًا قدرة جديدة إلى عائلة الـ Fragment. وهناك طريقتان لتمرير البيانات ذهابًا وإيابًا بين الـ Fragments: الأولى باستخدام FragmentManager، والثانية باستخدام مكتبة Navigation component.
لنرَ إذن كيف نستخدم هذه القدرة الجديدة عمليًا. سأبني تطبيقًا بسيطًا لقائمة ملاحظات يتكوّن من Fragmentين باستخدام مكتبة Navigation component: أحدهما لعرض قائمة الملاحظات، والآخر لكتابة ملاحظة جديدة.
لنبدأ بإضافة التبعيات المهمة. وقت كتابة هذا المقال، كان أحدث إصدار من Fragment هو 1.3.0-alpha05.
def nav_version = "2.3.0-beta01"
def fragment_version = "1.3.0-alpha05"
implementation "androidx.fragment:fragment:$fragment_version"
implementation "androidx.fragment:fragment-ktx:$fragment_version"
implementation "androidx.navigation:navigation-fragment:$nav_version"
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
وهذا هو الـ navigation graph الخاص بهذا المشروع.
الكود المصدري كاملًا متاح على https://github.com/BenBoonya/fragment-result
استخدام FragmentManager
الكود المصدري: https://github.com/BenBoonya/fragment-result/tree/using-fragment-manager
القدرة الجديدة التي أُضيفت إلى FragmentManager جعلت FragmentManager قادرًا على العمل كمخزن مركزي لنتائج الـ Fragments. ورغم أن الدالة لا تحمل اسم startFragmentForResult، فإنها تعمل بطريقة مشابهة لـ startActivityForResult. وبالمقتطف التالي يمكننا تمرير البيانات ذهابًا وإيابًا بين الـ Fragments بسهولة. وقد أُضيفت هذه القدرة منذ Fragment 1.3.0-alpha04.
لكي يستلم NoteListFragment النتيجة من AddNoteFragment، علينا تعيين result listener. لنلقِ نظرة على NoteListFragment.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
<em>setFragmentResultListener</em>(
ADD_NOTE
) <strong>{ </strong>_, result <strong>->
</strong>result.getString(NOTE)?.<em>let </em><strong>{ </strong>note <strong>->
</strong>adapter.addItem(note)
<strong>}
}
</strong>}
كما ترون، تتوفر extension function باسم setFragmentResultListener، ونستلم النتيجة على شكل bundle. لننتقل إلى AddNoteFragment. نحتاج هنا إلى تعيين النتيجة، ويمكن فعل ذلك بسهولة كما يلي.
private fun navigateBack() {
<em>setFragmentResult</em>(
NoteListFragment.ADD_NOTE, <em>bundleOf</em>(
NoteListFragment.NOTE <em>to </em>binding.editText.<em>text</em>.toString()
)
)
<em>findNavController</em>().popBackStack()
}
الأمر الوحيد الذي يجب تذكّره هو أن عليكم تعيين النتيجة على FragmentManager نفسه باستخدام الـ requestKey نفسه. وإذا نظرنا عن قرب إلى الـ extension functions الخاصة بـ Fragment، سنرى أن كلًا من setFragmentResult وsetFragmentResultListener تستدعيان parentFragmentManager، كما في المقتطفين التاليين.
inline fun Fragment.setFragmentResultListener(
requestKey: String,
crossinline listener: ((resultKey: String, bundle: Bundle) -> Unit)
) {
<em>parentFragmentManager</em>.<em>setFragmentResultListener</em>(requestKey, this, listener)
}
fun Fragment.setFragmentResult(
requestKey: String,
result: Bundle
) = <em>parentFragmentManager</em>.setFragmentResult(requestKey, result)
ومن الأمور الجديرة بالذكر أيضًا أنه لا يمكن أن يوجد إلا listener واحد ونتيجة واحدة لكل مفتاح. ففي مثالنا، إذا استدعينا setFragmentResult في AddNoteFragment أكثر من مرة، فسيرسل النظام أحدث نتيجة إلى NoteListFragment قبل إزالة AddNoteFragment من الـ back stack. وإذا كان AddNoteFragment child Fragment تابعًا لـ NoteListFragment، فكل ما نحتاج إليه هو تعيين الـ listener والنتيجة على childFragmentManager بدلًا من parentFragmentManager.
استخدام مكتبة Navigation component
الكود المصدري: https://github.com/BenBoonya/fragment-result/tree/using-navigation-component
لإرجاع النتائج إلى الوجهات السابقة، تستخدم مكتبة Navigation component SavedStateHandle، وهو key-value map يمكن استخدامه لتخزين البيانات واسترجاعها بشكل دائم حتى مع تغييرات الإعدادات (configuration changes) في NoteListFragment:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
<em>findNavController</em>().<em>currentBackStackEntry</em>?.<em>savedStateHandle</em>?.getLiveData<String>(
ADD_NOTE
)?.observe(<em>viewLifecycleOwner</em>, <em>Observer </em><strong>{ </strong>note <strong>->
</strong>adapter.addItem(note)
<strong>}</strong>)
}
يوفّر savedStateHandle LiveData يمكننا وضع observer عليها للحصول على القيمة. في AddNoteFragment
<em>findNavController</em>().<em>previousBackStackEntry</em>?.<em>savedStateHandle</em>?.set(
NoteListFragment.ADD_NOTE,
binding.editText.<em>text</em>.toString()
)
هنا نعيّن القيمة في savedStateHandle الخاص بالـ back stack entry السابق، والذي يعود إلى NoteListFragment
الخلاصة
أتطلع بحماس إلى الإصدار المستقر من هذه الميزات. ففي رأيي، جعلت القدرات الجديدة في عائلة الـ Fragment العمل بمعمارية Single activity، التي تُمثَّل فيها كل الوجهات بـ Fragments، أسهل بكثير وأكثر جاذبية.


