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

المفاهيم وتنفيذ 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 مخصّص

تنفّذ الأجزاء والأنشطة في Support Library 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() قبل اتخاذ قرار باستدعاء المراقب.

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

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

للأسف، يواجه هذا الحل مشكلتين رئيسيتين:

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

مراجع إضافية

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

اختبارات الرموز

المدوّنات