التعامل مع دورات الحياة باستخدام المكوّنات التي تراعي دورة الحياة (طرق العرض)

المفاهيم والتنفيذ في Jetpack Compose

تنفّذ المكوّنات التي تراعي مراحل النشاط إجراءات استجابةً لتغيير في حالة مرحلة النشاط لمكوّن آخر، مثل الأنشطة واللقطات. تساعدك هذه المكوّنات في إنشاء رمز برمجي منظَّم بشكل أفضل، وغالبًا ما يكون حجمه أصغر، ويسهل الحفاظ عليه.

من الأنماط الشائعة تنفيذ إجراءات المكوّنات التابعة في طرق مراحل النشاط الخاصة بالأنشطة والأجزاء. ومع ذلك، يؤدي هذا النمط إلى تنظيم سيئ للرمز البرمجي وإلى انتشار الأخطاء. باستخدام المكوّنات التي تراعي مراحل النشاط، يمكنك نقل رمز المكوّنات التابعة من طرق مراحل النشاط إلى المكوّنات نفسها.

توفر حزمة androidx.lifecycle فئات وواجهات تتيح لك إنشاء مكونات مراعية لدورة الحياة، وهي مكونات يمكنها تعديل سلوكها تلقائيًا استنادًا إلى حالة دورة الحياة الحالية لنشاط أو جزء.

معظم مكوّنات التطبيق المحدّدة في إطار عمل Android لها دورات حياة مرتبطة بها. تتم إدارة دورات الحياة من خلال نظام التشغيل أو رمز إطار العمل الذي يتم تنفيذه في عمليتك. وهي أساسية لطريقة عمل Android، ويجب أن يحترمها تطبيقك. وقد يؤدي عدم إجراء ذلك إلى حدوث تسرّبات في الذاكرة أو حتى تعطُّل التطبيق.

لنفترض أنّ لدينا نشاطًا يعرض الموقع الجغرافي للجهاز على الشاشة. قد يكون التنفيذ الشائع على النحو التالي:

Kotlin

internal class MyLocationListener(
        private val context: Context,
        private val callback: (Location) -> Unit
) {

    fun start() {
        // connect to system location service
    }

    fun stop() {
        // disconnect from system location service
    }
}

class MyActivity : AppCompatActivity() {
    private lateinit var myLocationListener: MyLocationListener

    override fun onCreate(...) {
        myLocationListener = MyLocationListener(this) { location ->
            // update UI
        }
    }

    public override fun onStart() {
        super.onStart()
        myLocationListener.start()
        // manage other components that need to respond
        // to the activity lifecycle
    }

    public override fun onStop() {
        super.onStop()
        myLocationListener.stop()
        // manage other components that need to respond
        // to the activity lifecycle
    }
}

Java

class MyLocationListener {
    public MyLocationListener(Context context, Callback callback) {
        // ...
    }

    void start() {
        // connect to system location service
    }

    void stop() {
        // disconnect from system location service
    }
}

class MyActivity extends AppCompatActivity {
    private MyLocationListener myLocationListener;

    @Override
    public void onCreate(...) {
        myLocationListener = new MyLocationListener(this, (location) -> {
            // update UI
        });
    }

    @Override
    public void onStart() {
        super.onStart();
        myLocationListener.start();
        // manage other components that need to respond
        // to the activity lifecycle
    }

    @Override
    public void onStop() {
        super.onStop();
        myLocationListener.stop();
        // manage other components that need to respond
        // to the activity lifecycle
    }
}

على الرغم من أنّ هذا النموذج يبدو جيدًا، إلا أنّه في تطبيق حقيقي، سينتهي بك الأمر إلى إجراء عدد كبير جدًا من عمليات الاستدعاء التي تدير واجهة المستخدم والمكوّنات الأخرى استجابةً للحالة الحالية لدورة الحياة. تؤدي إدارة عدة مكوّنات إلى وضع قدر كبير من الرموز في طرق مراحل النشاط، مثل onStart() وonStop، ما يجعل صيانتها أمرًا صعبًا.

بالإضافة إلى ذلك، ليس هناك ما يضمن بدء تشغيل المكوّن قبل إيقاف النشاط أو الجزء. وينطبق ذلك بشكل خاص إذا كنا بحاجة إلى تنفيذ عملية تستغرق وقتًا طويلاً، مثل بعض عمليات التحقّق من الإعدادات في onStart. قد يؤدي ذلك إلى حدوث حالة تزامن حيث تنتهي الطريقة onStop() قبل onStart، ما يؤدي إلى إبقاء المكوّن نشطًا لفترة أطول من اللازم.

Kotlin

class MyActivity : AppCompatActivity() {
    private lateinit var myLocationListener: MyLocationListener

    override fun onCreate(...) {
        myLocationListener = MyLocationListener(this) { location ->
            // update UI
        }
    }

    public override fun onStart() {
        super.onStart()
        Util.checkUserStatus { result ->
            // what if this callback is invoked AFTER activity is stopped?
            if (result) {
                myLocationListener.start()
            }
        }
    }

    public override fun onStop() {
        super.onStop()
        myLocationListener.stop()
    }

}

Java

class MyActivity extends AppCompatActivity {
    private MyLocationListener myLocationListener;

    public void onCreate(...) {
        myLocationListener = new MyLocationListener(this, location -> {
            // update UI
        });
    }

    @Override
    public void onStart() {
        super.onStart();
        Util.checkUserStatus(result -> {
            // what if this callback is invoked AFTER activity is stopped?
            if (result) {
                myLocationListener.start();
            }
        });
    }

    @Override
    public void onStop() {
        super.onStop();
        myLocationListener.stop();
    }
}

توفر حزمة androidx.lifecycle فئات وواجهات تساعدك في معالجة هذه المشاكل بطريقة مرنة ومعزولة.

مراحل النشاط

Lifecycle هي فئة تحتوي على معلومات حول حالة دورة حياة أحد المكوّنات (مثل نشاط أو جزء) وتسمح لعناصر أخرى بمراقبة هذه الحالة.

يستخدم Lifecycle تعدادَين رئيسيَّين لتتبُّع حالة مراحل النشاط للمكوّن المرتبط به:

حدث

أحداث مراحل النشاط التي يتم إرسالها من إطار العمل والفئة Lifecycle تتطابق هذه الأحداث مع أحداث عمليات الاستدعاء في الأنشطة واللقطات.

الولاية

الحالة الحالية للمكوّن الذي يتتبّعه العنصر Lifecycle

الشكل 1. حالات وأحداث مراحل نشاط Android

يمكنك اعتبار الحالات كعُقد في رسم بياني والأحداث كحواف بين هذه العُقد.

يمكن لفئة مراقبة حالة مراحل نشاط المكوّن من خلال تنفيذ DefaultLifecycleObserver وتجاوز الطرق المقابلة، مثل onCreate, onStart وما إلى ذلك. بعد ذلك، يمكنك إضافة مراقب من خلال استدعاء طريقة addObserver() للفئة Lifecycle وتمرير مثيل للمراقب، كما هو موضّح في المثال التالي:

Kotlin

class MyObserver : DefaultLifecycleObserver {
    override fun onResume(owner: LifecycleOwner) {
        connect()
    }

    override fun onPause(owner: LifecycleOwner) {
        disconnect()
    }
}

myLifecycleOwner.getLifecycle().addObserver(MyObserver())

Java

public class MyObserver implements DefaultLifecycleObserver {
    @Override
    public void onResume(LifecycleOwner owner) {
        connect()
    }

    @Override
    public void onPause(LifecycleOwner owner) {
        disconnect()
    }
}

myLifecycleOwner.getLifecycle().addObserver(new MyObserver());

في المثال أعلاه، ينفّذ العنصر myLifecycleOwner الواجهة LifecycleOwner، والتي سيتم توضيحها في القسم التالي.

LifecycleOwner

LifecycleOwner هي واجهة طريقة واحدة تشير إلى أنّ الفئة تتضمّن Lifecycle. تحتوي هذه الواجهة على طريقة واحدة، وهي getLifecycle، ويجب أن تنفّذها الفئة. إذا كنت تحاول إدارة مراحل نشاط عملية تطبيق كاملة بدلاً من ذلك، يمكنك الاطّلاع على ProcessLifecycleOwner.

تجرّد هذه الواجهة ملكية Lifecycle من الفئات الفردية، مثل Fragment وAppCompatActivity، وتتيح كتابة مكوّنات تعمل معها. يمكن لأي فئة تطبيقات مخصّصة تنفيذ الواجهة LifecycleOwner.

تعمل المكوّنات التي تنفّذ DefaultLifecycleObserver بسلاسة مع المكوّنات التي تنفّذ LifecycleOwner لأنّ المالك يمكنه توفير دورة حياة يمكن للمراقب تسجيلها لمراقبتها.

بالنسبة إلى مثال تتبُّع الموقع الجغرافي، يمكننا جعل الفئة MyLocationListener تنفّذ DefaultLifecycleObserver ثمّ تهيئتها باستخدام Lifecycle للنشاط في الطريقة onCreate(). يسمح ذلك لفئة MyLocationListener بأن تكون مكتفية ذاتيًا، ما يعني أنّه يتم تعريف منطق الاستجابة للتغييرات في حالة مراحل النشاط في MyLocationListener بدلاً من النشاط. يؤدي تخزين كل مكوّن لمنطقه الخاص إلى تسهيل إدارة منطق الأنشطة والقصاصات.

Kotlin

class MyActivity : AppCompatActivity() {
    private lateinit var myLocationListener: MyLocationListener

    override fun onCreate(...) {
        myLocationListener = MyLocationListener(this, lifecycle) { location ->
            // update UI
        }
        Util.checkUserStatus { result ->
            if (result) {
                myLocationListener.enable()
            }
        }
    }
}

Java

class MyActivity extends AppCompatActivity {
    private MyLocationListener myLocationListener;

    public void onCreate(...) {
        myLocationListener = new MyLocationListener(this, getLifecycle(), location -> {
            // update UI
        });
        Util.checkUserStatus(result -> {
            if (result) {
                myLocationListener.enable();
            }
        });
  }
}

من حالات الاستخدام الشائعة تجنُّب استدعاء بعض عمليات معاودة الاتصال إذا لم يكن Lifecycle في حالة جيدة في الوقت الحالي. على سبيل المثال، إذا كان برنامج معالجة البيانات يعرض جزءًا من المعاملة بعد حفظ حالة النشاط، سيؤدي ذلك إلى حدوث عطل، لذا لن نريد أبدًا استدعاء برنامج معالجة البيانات هذا.

لتسهيل حالة الاستخدام هذه، تتيح الفئة Lifecycle للكائنات الأخرى الاستعلام عن الحالة الحالية.

Kotlin

internal class MyLocationListener(
        private val context: Context,
        private val lifecycle: Lifecycle,
        private val callback: (Location) -> Unit
): DefaultLifecycleObserver {

    private var enabled = false

    override fun onStart(owner: LifecycleOwner) {
        if (enabled) {
            // connect
        }
    }

    fun enable() {
        enabled = true
        if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
            // connect if not connected
        }
    }

    override fun onStop(owner: LifecycleOwner) {
        // disconnect if connected
    }
}

Java

class MyLocationListener implements DefaultLifecycleObserver {
    private boolean enabled = false;
    public MyLocationListener(Context context, Lifecycle lifecycle, Callback callback) {
       ...
    }

    @Override
    public void onStart(LifecycleOwner owner) {
        if (enabled) {
           // connect
        }
    }

    public void enable() {
        enabled = true;
        if (lifecycle.getCurrentState().isAtLeast(STARTED)) {
            // connect if not connected
        }
    }

    @Override
    public void onStop(LifecycleOwner owner) {
        // disconnect if connected
    }
}

من خلال تنفيذ ذلك، يصبح الصف LocationListener متوافقًا تمامًا مع مراحل النشاط. إذا أردنا استخدام LocationListener من نشاط أو جزء آخر، ما علينا سوى تهيئته. تتولّى الفئة نفسها إدارة جميع عمليات الإعداد والإزالة.

إذا كانت إحدى المكتبات توفّر فئات تحتاج إلى العمل مع مراحل نشاط Android، ننصحك باستخدام المكوّنات المتوافقة مع مراحل النشاط. يمكن لعملاء مكتبتك دمج هذه المكوّنات بسهولة بدون إدارة مراحل النشاط يدويًا من جهة العميل.

تنفيذ LifecycleOwner مخصّص

تتضمّن الأجزاء والأنشطة في الإصدار 26.1.0 من "مكتبة الدعم" والإصدارات الأحدث واجهة LifecycleOwner.

إذا كان لديك فئة مخصّصة تريد تحويلها إلى LifecycleOwner، يمكنك استخدام الفئة LifecycleRegistry، ولكن عليك إعادة توجيه الأحداث إلى هذه الفئة، كما هو موضّح في مثال الرمز البرمجي التالي:

Kotlin

class MyActivity : Activity(), LifecycleOwner {

    private lateinit var lifecycleRegistry: LifecycleRegistry

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        lifecycleRegistry = LifecycleRegistry(this)
        lifecycleRegistry.markState(Lifecycle.State.CREATED)
    }

    public override fun onStart() {
        super.onStart()
        lifecycleRegistry.markState(Lifecycle.State.STARTED)
    }

    override fun getLifecycle(): Lifecycle {
        return lifecycleRegistry
    }
}

Java

public class MyActivity extends Activity implements LifecycleOwner {
    private LifecycleRegistry lifecycleRegistry;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        lifecycleRegistry = new LifecycleRegistry(this);
        lifecycleRegistry.markState(Lifecycle.State.CREATED);
    }

    @Override
    public void onStart() {
        super.onStart();
        lifecycleRegistry.markState(Lifecycle.State.STARTED);
    }

    @NonNull
    @Override
    public Lifecycle getLifecycle() {
        return lifecycleRegistry;
    }
}

أفضل الممارسات للمكوّنات التي تراعي مراحل النشاط

  • احرص على أن تكون عناصر التحكّم في واجهة المستخدم (الأنشطة واللقطات) بسيطة قدر الإمكان. ولا يجب أن يحاولوا الحصول على بياناتهم الخاصة، بل عليهم استخدام ViewModel للقيام بذلك، ومراقبة عنصر LiveData لعكس التغييرات مرة أخرى على طرق العرض.
  • حاوِل كتابة واجهات مستخدم مستندة إلى البيانات، حيث تكون مسؤولية وحدة التحكّم في واجهة المستخدم هي تعديل طرق العرض عند تغيُّر البيانات، أو إرسال إشعارات بشأن إجراءات المستخدم إلى ViewModel.
  • ضَع منطق البيانات في فئة ViewModel. يجب أن يعمل ViewModel كأداة ربط بين وحدة التحكّم في واجهة المستخدم وبقية تطبيقك. ومع ذلك، يجب الانتباه إلى أنّ ViewModel ليس مسؤولاً عن جلب البيانات (على سبيل المثال، من شبكة). بدلاً من ذلك، يجب أن يستدعي ViewModel المكوّن المناسب لجلب البيانات، ثم يعرض النتيجة مرة أخرى لوحدة التحكّم في واجهة المستخدم.
  • استخدِم ربط البيانات للحفاظ على واجهة نظيفة بين طرق العرض ووحدة التحكّم في واجهة المستخدم. يتيح لك ذلك جعل طرق العرض أكثر تعريفية وتقليل رمز التعديل الذي عليك كتابته في الأنشطة والتقسيمات. إذا كنت تفضّل إجراء ذلك بلغة البرمجة Java، استخدِم مكتبة مثل Butter Knife لتجنُّب رمز النص النموذجي والحصول على تجريد أفضل.
  • إذا كانت واجهة المستخدم معقّدة، يمكنك إنشاء فئة مقدّم للتعامل مع تعديلات واجهة المستخدم. قد تكون هذه المهمة شاقة، ولكنّها يمكن أن تسهّل اختبار مكوّنات واجهة المستخدم.
  • تجنَّب الإشارة إلى سياق View أو Activity في ViewModel. إذا كان عمر ViewModel أطول من عمر النشاط (في حال حدوث تغييرات في الإعدادات)، سيحدث تسرّب للنشاط ولن يتم التخلص منه بشكل صحيح من خلال أداة جمع البيانات غير المستخدَمة.
  • استخدِم أنماط "كوروتين" في Kotlin لإدارة المهام الطويلة الأمد والعمليات الأخرى التي يمكن تنفيذها بشكل غير متزامن.

حالات استخدام المكوّنات التي تراعي مراحل النشاط

يمكن أن تسهّل عليك المكوّنات التي تراعي مراحل النشاط إدارة مراحل النشاط في مجموعة متنوعة من الحالات. في ما يلي بعض الأمثلة:

  • التبديل بين تحديثات الموقع الجغرافي التقريبية والدقيقة استخدِم مكوّنات تتوافق مع مراحل النشاط لتفعيل تحديثات دقيقة للموقع الجغرافي عندما يكون تطبيقك الذي يرصد الموقع الجغرافي مرئيًا، ثم انتقِل إلى تحديثات غير دقيقة عندما يكون التطبيق يعمل في الخلفية. ‫LiveData، وهو مكوّن يراعي مراحل النشاط، يسمح لتطبيقك بتعديل واجهة المستخدم تلقائيًا عندما يغيّر المستخدم المواقع الجغرافية.
  • إيقاف تخزين الفيديو مؤقتًا وبدء تخزينه استخدِم مكوّنات تتوافق مع مراحل النشاط لبدء تخزين الفيديو مؤقتًا في أقرب وقت ممكن، ولكن أجِّل التشغيل إلى أن يتم تشغيل التطبيق بالكامل. يمكنك أيضًا استخدام المكوّنات المتوافقة مع مراحل النشاط لإيقاف التخزين المؤقت عند إيقاف تطبيقك.
  • بدء الاتصال بالشبكة وإيقافه استخدِم المكوّنات التي تراعي مراحل النشاط لإتاحة التعديل المباشر (البث) لبيانات الشبكة أثناء تشغيل التطبيق في المقدّمة، وأيضًا لإيقافها مؤقتًا تلقائيًا عند انتقال التطبيق إلى الخلفية.
  • إيقاف الرسومات القابلة للرسم المتحرّك مؤقتًا واستئنافها استخدِم مكوّنات تتوافق مع مراحل النشاط لإيقاف الرسومات المتحركة مؤقتًا عندما يكون التطبيق في الخلفية، ولاستئنافها عندما يكون التطبيق في المقدّمة.

التعامل مع أحداث الإيقاف

عندما ينتمي Lifecycle إلى AppCompatActivity أو Fragment، تتغير حالة Lifecycle إلى CREATED ويتم إرسال الحدث ON_STOP عند استدعاء onSaveInstanceState() في AppCompatActivity أو Fragment.

عند حفظ حالة Fragment أو AppCompatActivity باستخدام onSaveInstanceState، تُعتبر واجهة المستخدم غير قابلة للتغيير إلى أن يتم استدعاء ON_START. من المحتمل أن يؤدي محاولة تعديل واجهة المستخدم بعد حفظ الحالة إلى حدوث حالات عدم اتساق في حالة التنقّل في تطبيقك، ولهذا السبب يعرض FragmentManager استثناءً إذا شغّل التطبيق FragmentTransaction بعد حفظ الحالة. لمزيد من التفاصيل، يمكنك الاطّلاع على commit().

تمنع LiveData هذه الحالة الحدّية بشكل تلقائي من خلال عدم استدعاء المراقب إذا لم يكن Lifecycle المرتبط بالمراقب STARTED على الأقل. في الخلفية، يستدعي isAtLeast() قبل تحديد ما إذا كان سيتم استدعاء المراقب.

لسوء الحظ، يتم استدعاء طريقة AppCompatActivity onStop() بعد onSaveInstanceState، ما يؤدي إلى حدوث فجوة لا يُسمح فيها بتغيير حالة واجهة المستخدم، ولكن لم يتم نقل Lifecycle بعد إلى الحالة CREATED.

لمنع حدوث هذه المشكلة، تحدّد الفئة Lifecycle في الإصدار beta2 والإصدارات الأقدم الحالة على أنّها CREATED بدون إرسال الحدث، وذلك لكي يحصل أي رمز برمجي يتحقّق من الحالة الحالية على القيمة الحقيقية حتى إذا لم يتم إرسال الحدث إلى أن يستدعي النظام onStop().

مع ذلك، يتضمّن هذا الحلّ مشكلتين رئيسيتين:

  • في المستوى 23 من واجهة برمجة التطبيقات والإصدارات الأقدم، يحفظ نظام Android حالة النشاط حتى إذا كان هذا النشاط مغطّى جزئيًا بنشاط آخر. بعبارة أخرى، يستدعي نظام Android الدالة onSaveInstanceState()، ولكنّه لا يستدعي بالضرورة الدالة onStop. يؤدي ذلك إلى إنشاء فاصل زمني طويل محتمل يظل فيه المراقب يعتقد أنّ دورة الحياة نشطة على الرغم من أنّه لا يمكن تعديل حالة واجهة المستخدم.
  • يجب أن تنفّذ أي فئة تريد عرض سلوك مشابه للفئة LiveData الحل البديل الذي توفّره الإصدارات Lifecycle beta 2 والإصدارات الأقدم.

مراجع إضافية

لمزيد من المعلومات حول التعامل مع مراحل النشاط باستخدام المكوّنات التي تراعي مراحل النشاط، يُرجى الاطّلاع على المراجع الإضافية التالية.

نماذج

  • Sunflower، وهو تطبيق تجريبي يوضّح أفضل الممارسات باستخدام Architecture Components

الدروس التطبيقية حول الترميز

المدوّنات