المفاهيم وتنفيذ 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.
يمكنك اعتبار الحالات كعُقد في رسم بياني والأحداث كحواف بين هذه العُقد.
يمكن للفئة مراقبة حالة دورة حياة المكوّن من خلال تنفيذ
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والإصدارات الأقدم.
مراجع إضافية
لمزيد من المعلومات حول التعامل مع مراحل النشاط باستخدام المكوّنات التي تراعي مراحل النشاط، يُرجى الاطّلاع على المراجع الإضافية التالية.