
تُظهر الصورة الفكرة الأولى للقارئ الذي يتساءل عما يمكن كتابته عن مهمة بسيطة مثل عرض الحوار. يعتقد المدير بالمثل: "لا يوجد شيء معقد هنا ، ففاسيا ستفعل في 5 دقائق." بالطبع ، أنا أبالغ ، لكن في الواقع ليس كل شيء بسيطًا كما يبدو للوهلة الأولى. خاصة إذا كنا نتحدث عن Android.
لذا ، كان 2019 في الفناء ، وما زلنا لا نعرف كيفية إظهار مربعات الحوار بشكل صحيح .
دعونا نفعل ذلك بالترتيب ونبدأ ببيان المشكلة:
يجب إظهار حوار بسيط مع النص لتأكيد الإجراء وأزرار "التأكيد / الإلغاء". بالنقر فوق الزر "تأكيد" - تنفيذ إجراء ، عن طريق الزر "إلغاء" - أغلق مربع الحوار.
حل الجبهة
أود أن أسمي هذا الأسلوب المبتدئين ، لأن هذه ليست المرة الأولى التي أواجه فيها سوء فهم لماذا لا يمكنك استخدام AlertDialog فقط ، كما هو موضح أدناه:
AlertDialog.Builder(this) .setMessage("Please, confirm the action") .setPositiveButton("Confirm") { dialog, which ->
طريقة شائعة إلى حد ما لمطور مبتدئ ، فمن الواضح وبديهية. ولكن ، كما هو الحال في العديد من الحالات عند العمل مع Android ، فهذه الطريقة خاطئة تمامًا. من اللون الأزرق ، نحصل على تسرب للذاكرة ، فقط قم بتشغيل الجهاز ، وسوف ترى الخطأ التالي في السجلات:
stacktrace E/WindowManager: android.view.WindowLeaked: Activity com.example.testdialog.MainActivity has leaked window DecorView@71b5789[MainActivity] that was originally added here at android.view.ViewRootImpl.<init>(ViewRootImpl.java:511) at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:346) at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:93) at android.app.Dialog.show(Dialog.java:329) at com.example.testdialog.MainActivity.onCreate(MainActivity.kt:27) at android.app.Activity.performCreate(Activity.java:7144) at android.app.Activity.performCreate(Activity.java:7135) at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1271) at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2931) at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3086) at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:78) at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:108) at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:68) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1816) at android.os.Handler.dispatchMessage(Handler.java:106) at android.os.Looper.loop(Looper.java:193) at android.app.ActivityThread.main(ActivityThread.java:6718) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
في Stackoverflow ، يعد السؤال المتعلق بهذه المشكلة أحد أكثر الأسئلة شيوعًا. باختصار ، المشكلة هي أننا إما نعرض مربع الحوار أو لا نغلقه بعد اكتمال التنشيط.
يمكنك ، بالطبع ، استدعاء رفض في مربع الحوار في نشاط onPause أو onDestroy ، كما هو موصوف في الإجابة حسب المرجع . ولكن هذا ليس بالضبط ما نحتاجه. نريد استعادة الحوار بعد تشغيل الجهاز.
طريقة عفا عليها الزمن
قبل ظهور الأجزاء في Android ، كان يجب عرض مربعات الحوار من خلال مكالمة إلى طريقة تنشيط showDialog . في هذه الحالة ، يقوم النشاط بإدارة دورة حياة الحوار بشكل صحيح واستعادتها بعد فترة توقف. تم إنشاء الحوار نفسه ليتم تنفيذه في رد الاتصال onCreateDialog:
public class MainActivity extends Activity { private static final int CONFIRMATION_DIALOG_ID = 1;
ليس من السهل للغاية أن تضطر إلى بدء تشغيل معرّف حوار وتمرير المعلمات من خلال Bundle. ولا يزال بإمكاننا الحصول على مشكلة "النافذة المتسربة" إذا حاولنا عرض مربع حوار بعد استدعاء onDestroy على النشاط. هذا ممكن ، على سبيل المثال ، عند محاولة إظهار خطأ بعد عملية غير متزامنة.
بشكل عام ، تعد هذه المشكلة أمرًا معتادًا بالنسبة إلى Android ، عندما تحتاج إلى القيام بشيء ما بعد إجراء عملية غير متزامنة ، ويتم تدمير النشاط أو الجزء بالفعل في تلك اللحظة. هذا هو السبب في أن أنماط MV * أكثر شيوعًا في مجتمع Android مقارنة بمطوري iOS.
طريقة من الوثائق
ظهرت الشظايا في Android Honeycomb ، وتم إهمال الطريقة الموضحة أعلاه ، وتم تمييز طريقة showDialog للنشاط على أنها مهملة. لا ، لا يعد AlertDialog قديمًا ، حيث أن الكثير من الناس مخطئون. يوجد الآن DialogFragment ، الذي يلتف كائن الحوار ويتحكم في دورة حياته.
يتم إهمال المقتطفات الأصلية أيضًا منذ واجهة برمجة تطبيقات 28. الآن يجب عليك استخدام التطبيق فقط من مكتبة الدعم (AndroidX).
دعونا ننجز مهمتنا ، على النحو المنصوص عليه في الوثائق الرسمية :
- تحتاج أولاً إلى أن ترث من DialogFragment وتطبيق إنشاء مربع حوار في طريقة onCreateDialog.
- قم بوصف واجهة حدث الحوار وانشئ مستمع في أسلوب onAttach.
- قم بتطبيق واجهة حدث حوار في نشاط أو جزء.
إذا لم يكن القارئ واضحًا تمامًا لما لا يمكن للمستمع تمريره عبر المنشئ ، فيمكنه قراءة المزيد حول هذا الموضوع هنا
كود جزء الحوار:
class ConfirmationDialogFragment : DialogFragment() { interface ConfirmationListener { fun confirmButtonClicked() fun cancelButtonClicked() } private lateinit var listener: ConfirmationListener override fun onAttach(context: Context?) { super.onAttach(context) try {
رمز التفعيل:
class MainActivity : AppCompatActivity(), ConfirmationListener { private fun showConfirmationDialog() { ConfirmationDialogFragment() .show(supportFragmentManager, "ConfirmationDialogFragmentTag") } override fun confirmButtonClicked() {
تحولت رمز بما فيه الكفاية ، أليس كذلك؟
كقاعدة عامة ، هناك نوع من MVP في المشروع ، لكنني قررت أن مكالمات مقدم العرض يمكن حذفها في هذه الحالة. في المثال أعلاه ، يجدر إضافة الطريقة الثابتة لإنشاء مربع حوار newInstance وتمرير المعلمات إلى وسيطات التجزئة ، كما هو متوقع.
وهذا كل ما يخفيه الحوار في الوقت المناسب ويتم استعادته بشكل صحيح. ليس من المستغرب أن تنشأ مثل هذه الأسئلة على Stackoverflow: واحد واثنان .
إيجاد الحل الأمثل
الوضع الحالي لم يناسبنا ، وبدأنا في البحث عن طريقة لجعل العمل مع الحوارات أكثر راحة. كان هناك شعور بأنه يمكنك جعله أسهل ، كما في الطريقة الأولى تقريبًا.
فيما يلي الاعتبارات التي وجهتنا:
- هل أحتاج إلى حفظ واستعادة الحوار بعد إنهاء عملية التطبيق؟
في معظم الحالات ، هذا غير مطلوب ، كما في مثالنا ، عندما تحتاج إلى إظهار رسالة بسيطة أو طلب شيء ما. مثل هذا الحوار مناسب حتى يتم فقد انتباه المستخدم. إذا قمت باستعادته بعد غياب طويل في التطبيق ، فسوف يفقد المستخدم السياق من خلال الإجراء المخطط له. لذلك ، تحتاج فقط إلى دعم المنعطفات للجهاز والتعامل بشكل صحيح مع دورة حياة الحوار. خلاف ذلك ، من الحركة المحرجة للجهاز ، فقد يفقد المستخدم الرسالة التي تم فتحها للتو دون قراءتها. - عند استخدام DialogFragment ، يظهر الكثير من التعليمات البرمجية لـ boilerplate ، يتم فقد البساطة. لذلك ، سيكون من الجيد التخلص من الجزء باعتباره غلاف واستخدام مربع حوار مباشرة . للقيام بذلك ، يجب عليك تخزين حالة مربع الحوار لإظهاره مرة أخرى بعد إعادة إنشاء طريقة العرض وإخفائه عند وفاة العرض.
- الجميع معتادون على إدراك عرض الحوار كفريق ، خاصة إذا كنت تعمل فقط مع MVP. يفترض FragmentManager مهمة الاستعادة اللاحقة للدولة. لكن يمكنك النظر إلى هذا الموقف بشكل مختلف والبدء في إدراك الحوار كدولة . هذا هو أكثر ملاءمة عند العمل مع أنماط PM أو MVVM.
- بالنظر إلى أن معظم التطبيقات تستخدم الآن أساليب تفاعلية ، هناك حاجة لأن تكون مربعات الحوار تفاعلية . المهمة الرئيسية هي عدم كسر السلسلة التي تبدأ عرض الحوار ، وإرفاق دفق تفاعلي من الأحداث للحصول على نتيجة منه. هذا ملائم للغاية على جانب PresentationModel / ViewModel عندما تقوم بمعالجة تدفقات بيانات متعددة.
لقد أخذنا في الاعتبار جميع المتطلبات المذكورة أعلاه وتوصلنا إلى طريقة لعرض الحوارات بشكل تفاعلي ، والتي قمنا بتنفيذها بنجاح في مكتبة RxPM لدينا (هناك مقال منفصل عنها).
الحل نفسه لا يتطلب مكتبة ويمكن القيام به بشكل منفصل. تسترشد فكرة "الحوار كدولة" ، يمكنك محاولة بناء حل يعتمد على ViewModel العصرية و LiveData. لكنني سأترك هذا الحق للقارئ ، وبعد ذلك سنتحدث عن حل جاهز من المكتبة.
طريقة رد الفعل
سأوضح كيف تم حل المهمة الأولية في RxPM ، ولكن أولاً بضع كلمات حول المفاهيم الأساسية من المكتبة:
- PresentationModel - يخزن حالة التفاعل ، ويحتوي على منطق واجهة المستخدم ، وينجو من المنعطفات.
- الدولة هي حالة رد الفعل. يمكنك التفكير في الأمر على أنه التفاف على BehaviorRelay.
- الإجراء - التفاف على PublishRelay ، يعمل على نقل الأحداث من طريقة العرض إلى PresentationModel.
- الدولة والعمل لها يمكن ملاحظتها والمستهلك.
فئة DialogControl مسؤولة عن حالة مربع الحوار. يحتوي على معلمتين: الأول لنوع البيانات التي يجب عرضها في مربع الحوار ، والثاني لنوع النتيجة. في مثالنا ، سيكون نوع البيانات وحدة ، ولكن يمكن أن يكون رسالة إلى المستخدم أو أي نوع آخر.
DialogControl لديه الطرق التالية:
show(data: T)
- فقط يعطي أمرًا لعرضه.showForResult(data: T): Maybe<R>
- يعرض مربع حوار ويفتح الدفق للحصول على النتيجة.sendResult(result: R)
- يرسل النتيجة ، ويسمى من الجانب عرض.dismiss()
- فقط يخفي الحوار.
تقوم DialogControl بتخزين الحالة - هل يوجد حوار على الشاشة أم لا (معروض / غائب). هذه هي الطريقة التي يظهر بها رمز الفصل:
class DialogControl<T, R> internal constructor(pm: PresentationModel) { val displayed = pm.State<Display>(Absent) private val result = pm.Action<R>() sealed class Display { data class Displayed<T>(val data: T) : Display() object Absent : Display() }
إنشاء عرض تقديمي بسيط:
class SamplePresentationModel : PresentationModel() { enum class ConfirmationDialogResult { CONFIRMED, CANCELED }
يرجى ملاحظة أنه يتم تنفيذ معالجة النقرات وتأكيد التأكيد ومعالجة الإجراء في نفس السلسلة. يسمح لك هذا بتركيز الكود وعدم تشتيت المنطق عبر عمليات الاسترجاعات المتعددة.
بعد ذلك ، نربط DialogControl ببساطة بالعرض باستخدام ملحق bindTo.
نقوم بجمع AlertDialog المعتاد ، وإرسال النتيجة عبر sendResult:
class SampleActivity : PmSupportActivity<SamplePresentationModel>() { override fun providePresentationModel() = SamplePresentationModel()
في سيناريو نموذجي ، يحدث شيء مثل هذا تحت الغطاء:
- نضغط على الزر ، الحدث من خلال Action "buttonClicks" يحصل على PresentationModel.
- لهذا الحدث ، نطلق عرض مربع الحوار من خلال الدعوة إلى showForResult.
- نتيجة لذلك ، تتغير الحالة في DialogControl من الغائب إلى المعروض.
- عند تلقي الحدث المعروض ، يتم استدعاء lambda التي مررنا بها في الربط bindTo. يتم إنشاء كائن حوار فيه ، والذي يتم عرضه بعد ذلك.
- يقوم المستخدم بالضغط على زر التأكيد ، وإشعال المستمع ، ويتم إرسال نتيجة النقر إلى DialogControl عن طريق استدعاء sendResult.
- بعد ذلك ، تقع النتيجة في "نتيجة" الإجراء الداخلي ، وتتغير الحالة من المعروض إلى الغائب.
- عند تلقي حدث غائب ، يتم إغلاق مربع الحوار الحالي.
- يقع الحدث من Action "result" في الدفق الذي تم فتحه بواسطة استدعاء showForResult وتتم معالجته بواسطة السلسلة في PresentationModel.
تجدر الإشارة إلى أن مربع الحوار يغلق حتى عندما يكون العرض غير مرتبط من PresentationModel. في هذه الحالة ، تظل الحالة معروضة. سيتم استلامه في الرابط التالي وسيتم استعادة الحوار.
كما ترون ، ذهبت الحاجة إلى DialogFragment. يظهر مربع الحوار عندما يكون العرض مرتبطًا بـ PresentationModel ويتم إخفاؤه عندما يكون العرض غير مرتبط. نظرًا لحقيقة أن الحالة مخزنة في DialogControl ، والتي يتم تخزينها بدورها في PresentationModel ، تتم استعادة مربع الحوار بعد تدوير الجهاز.
اكتب الحوارات بشكل صحيح
لقد درسنا عدة طرق لعرض مربعات الحوار. إذا كنت لا تزال تظهر في الطريقة الأولى ، فأنا أطلب منك ، لا تفعل ذلك بعد الآن. لمحبي MVP لم يتبق شيء سوى استخدام الطريقة القياسية ، الموضحة في الوثائق الرسمية. لسوء الحظ ، فإن الميل إلى استنباط هذا النمط لا يسمح بالقيام بخلاف ذلك. حسنًا ، أوصي عشاق RxJava بإلقاء نظرة فاحصة على الطريقة التفاعلية ومكتبتنا RxPM .