استخدام كوروتين Kotlin مع المكوّنات المستندة إلى مراحل الحياة

توفِّر كوروتيني كوتلين واجهة برمجة تطبيقات تتيح لك كتابة رمز غير متزامن. باستخدام الكوروتينات في لغة Kotlin، يمكنك تحديد CoroutineScope الذي يساعدك في إدارة وقت تشغيل الكوروتينات. وتعمل كل عملية غير متزامنة ضمن نطاق معين.

توفّر المكوّنات الواعية لدورة الحياة دعمًا من الدرجة الأولى للكورروتينات للنطاقات المنطقية في تطبيقك، بالإضافة إلى طبقة إمكانية التشغيل التفاعلي مع LiveData. يشرح هذا الموضوع كيفية استخدام الكوروتين بشكل فعّال مع المكوّنات التي تراعي مراحل النشاط.

إضافة تبعيات KTX

إنّ نطاقات الكوروتين المدمجة الموضّحة في هذا الموضوع مضمّنة في إضافات KTX لكل مكوّن مقابل. تأكد من إضافة التبعيات المناسبة عند استخدام هذه النطاقات.

  • بالنسبة إلى ViewModelScope، استخدِم androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0 أو إصدارًا أحدث.
  • بالنسبة إلى LifecycleScope، استخدِم androidx.lifecycle:lifecycle-runtime-ktx:2.4.0 أو إصدارًا أحدث.
  • بالنسبة إلى liveData، استخدِم androidx.lifecycle:lifecycle-livedata-ktx:2.4.0 أو إصدارًا أحدث.

نطاقات الكوروتين الواعية لمراحل الحياة

تحدِّد المكونات الواعية لدورة الحياة النطاقات المضمَّنة التالية التي يمكنك استخدامها في تطبيقك.

نطاق نموذج العرض

يتم تحديد ViewModelScope لكل ViewModel في تطبيقك. ويتم تلقائيًا إلغاء أي كورروتين تم إطلاقه في هذا النطاق في حال محو ViewModel. ويمكن الاستفادة من الكوروتين هنا عندما يكون لديك عمل يجب إنجازه فقط إذا كان ViewModel مفعَّلاً. على سبيل المثال، إذا كنت تحتسب بعض البيانات لتنسيق، يجب تحديد نطاق العمل على ViewModel بحيث إذا تم محو ViewModel، يتم إلغاء العمل تلقائيًا لتجنّب استهلاك الموارد.

يمكنك الوصول إلى CoroutineScope لـ ViewModel من خلال السمة viewModelScope في ViewModel، كما هو موضّح في المثال التالي:

class MyViewModel: ViewModel() {
    init {
        viewModelScope.launch {
            // Coroutine that will be canceled when the ViewModel is cleared.
        }
    }
}

نطاق مراحل النشاط

يتم تحديد LifecycleScope لكل عنصر من عناصر Lifecycle. يتم إلغاء أي الكوروتين تم إطلاقه في هذا النطاق عند تدمير Lifecycle. ويمكنك الوصول إلى CoroutineScope من Lifecycle إما من خلال lifecycle.coroutineScope أو lifecycleOwner.lifecycleScope.

يوضح المثال أدناه كيفية استخدام lifecycleOwner.lifecycleScope لإنشاء نص محسوب مسبقًا بشكل غير متزامن:

class MyFragment: Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewLifecycleOwner.lifecycleScope.launch {
            val params = TextViewCompat.getTextMetricsParams(textView)
            val precomputedText = withContext(Dispatchers.Default) {
                PrecomputedTextCompat.create(longTextContent, params)
            }
            TextViewCompat.setPrecomputedText(textView, precomputedText)
        }
    }
}

فيديوهات كورروتينية قابلة لإعادة التشغيل مراعية لمراحل الحياة

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

في هذه الحالات، يوفّر Lifecycle وLifecycleOwner واجهة برمجة تطبيقات repeatOnLifecycle المعلّقة التي تنفِّذ هذا الإجراء بالضبط. يتضمّن المثال التالي مجموعة رموز يتم تشغيلها في كل مرة تكون فيها سمة Lifecycle المرتبطة في حالة STARTED على الأقل ويتم إلغاؤها عندما تكون قيمة Lifecycle STOPPED:

class MyFragment : Fragment() {

    val viewModel: MyViewModel by viewModel()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // Create a new coroutine in the lifecycleScope
        viewLifecycleOwner.lifecycleScope.launch {
            // repeatOnLifecycle launches the block in a new coroutine every time the
            // lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED.
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                // Trigger the flow and start listening for values.
                // This happens when lifecycle is STARTED and stops
                // collecting when the lifecycle is STOPPED
                viewModel.someDataFlow.collect {
                    // Process item
                }
            }
        }
    }
}

جمع التدفق الواعي لمراحل الحياة

إذا كنت تحتاج فقط إلى تنفيذ جمع البيانات الواعية بدورة الحياة في مسار واحد، يمكنك استخدام طريقة Flow.flowWithLifecycle() لتبسيط الرمز:

viewLifecycleOwner.lifecycleScope.launch {
    exampleProvider.exampleFlow()
        .flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
        .collect {
            // Process the value.
        }
}

ومع ذلك، إذا كنت بحاجة إلى إجراء جمع واعٍ لدورة الحياة على تدفقات متعددة بتوالي، يجب عليك جمع كل تدفق في الكوروتينات المختلفة. في هذه الحالة، يكون استخدام repeatOnLifecycle() مباشرةً أكثر فعالية:

viewLifecycleOwner.lifecycleScope.launch {
    viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
        // Because collect is a suspend function, if you want to
        // collect multiple flows in parallel, you need to do so in
        // different coroutines.
        launch {
            flow1.collect { /* Process the value. */ }
        }

        launch {
            flow2.collect { /* Process the value. */ }
        }
    }
}

تعليق الكوروتينات الواعية لمراحل الحياة

على الرغم من أنّ CoroutineScope توفّر طريقة مناسبة لإلغاء العمليات طويلة المدى تلقائيًا، قد يكون لديك حالات أخرى تريد فيها تعليق تنفيذ مجموعة الرموز ما لم يكن Lifecycle في حالة معيّنة. على سبيل المثال، لتشغيل FragmentTransaction، عليك الانتظار حتى تصبح قيمة Lifecycle STARTED على الأقل. في هذه الحالات، توفِّر السمة Lifecycle طُرقًا إضافية: lifecycle.whenCreated وlifecycle.whenStarted وlifecycle.whenResumed. ويتمّ تعليق أي تشغيل لكروتين داخل هذه الوحدات إذا لم تكن قيمة Lifecycle في الحد الأدنى المطلوب على الأقل.

يتضمّن المثال أدناه مجموعة رموز لا يتم تشغيلها إلا عندما تكون قيمة Lifecycle المرتبطة في حالة STARTED على الأقل:

class MyFragment: Fragment {
    init { // Notice that we can safely launch in the constructor of the Fragment.
        lifecycleScope.launch {
            whenStarted {
                // The block inside will run only when Lifecycle is at least STARTED.
                // It will start executing when fragment is started and
                // can call other suspend methods.
                loadingView.visibility = View.VISIBLE
                val canAccess = withContext(Dispatchers.IO) {
                    checkUserAccess()
                }

                // When checkUserAccess returns, the next line is automatically
                // suspended if the Lifecycle is not *at least* STARTED.
                // We could safely run fragment transactions because we know the
                // code won't run unless the lifecycle is at least STARTED.
                loadingView.visibility = View.GONE
                if (canAccess == false) {
                    findNavController().popBackStack()
                } else {
                    showContent()
                }
            }

            // This line runs only after the whenStarted block above has completed.

        }
    }
}

إذا تم تدمير Lifecycle أثناء نشاط الكوروتين من خلال إحدى الطرق when، يتم إلغاء الكوروتين تلقائيًا. في المثال أدناه، يتم تشغيل مجموعة finally بعد أن تصبح حالة Lifecycle هي DESTROYED:

class MyFragment: Fragment {
    init {
        lifecycleScope.launchWhenStarted {
            try {
                // Call some suspend functions.
            } finally {
                // This line might execute after Lifecycle is DESTROYED.
                if (lifecycle.state >= STARTED) {
                    // Here, since we've checked, it is safe to run any
                    // Fragment transactions.
                }
            }
        }
    }
}

استخدام الكوروتينات مع LiveData

عند استخدام LiveData، قد تحتاج إلى حساب القيم بشكل غير متزامن. على سبيل المثال، قد ترغب في استرداد تفضيلات المستخدم وعرضها على واجهة المستخدم لديك. في هذه الحالات، يمكنك استخدام دالة إنشاء liveData لاستدعاء الدالة suspend، وتقديم النتيجة ككائن LiveData.

في المثال أدناه، loadUser() هي دالة تعليق تم تعريفها في مكان آخر. استخدم دالة أداة إنشاء liveData لاستدعاء loadUser() بشكل غير متزامن، ثم استخدم emit() لإرسال النتيجة:

val user: LiveData<User> = liveData {
    val data = database.loadUser() // loadUser is a suspend function.
    emit(data)
}

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

يمكنك أيضًا إصدار قيم متعددة من المجموعة. عند استدعاء emit()، يتم تعليق تنفيذ الحظر إلى أن يتم ضبط قيمة LiveData على سلسلة التعليمات الرئيسية.

val user: LiveData<Result> = liveData {
    emit(Result.loading())
    try {
        emit(Result.success(fetchUser()))
    } catch(ioException: Exception) {
        emit(Result.error(ioException))
    }
}

يمكنك أيضًا دمج السمة liveData مع السمة Transformations، كما هو موضّح في المثال التالي:

class MyViewModel: ViewModel() {
    private val userId: LiveData<String> = MutableLiveData()
    val user = userId.switchMap { id ->
        liveData(context = viewModelScope.coroutineContext + Dispatchers.IO) {
            emit(database.loadUserById(id))
        }
    }
}

يمكنك إصدار قيَم متعدّدة من LiveData عن طريق استدعاء الدالة emitSource() متى أردت إصدار قيمة جديدة. لاحظ أن كل استدعاء إلى emit() أو emitSource() تزيل المصدر الذي تمت إضافته مسبقًا.

class UserDao: Dao {
    @Query("SELECT * FROM User WHERE id = :id")
    fun getUser(id: String): LiveData<User>
}

class MyRepository {
    fun getUser(id: String) = liveData<User> {
        val disposable = emitSource(
            userDao.getUser(id).map {
                Result.loading(it)
            }
        )
        try {
            val user = webservice.fetchUser(id)
            // Stop the previous emission to avoid dispatching the updated user
            // as `loading`.
            disposable.dispose()
            // Update the database.
            userDao.insert(user)
            // Re-establish the emission with success type.
            emitSource(
                userDao.getUser(id).map {
                    Result.success(it)
                }
            )
        } catch(exception: IOException) {
            // Any call to `emit` disposes the previous one automatically so we don't
            // need to dispose it here as we didn't get an updated value.
            emitSource(
                userDao.getUser(id).map {
                    Result.error(exception, it)
                }
            )
        }
    }
}

لمزيد من المعلومات المتعلقة بالكوروتينات، اطّلع على الروابط التالية:

مراجع إضافية

لمعرفة المزيد حول استخدام الكوروتينات مع المكونات الواعية لدورة الحياة، راجع الموارد الإضافية التالية.

عيّنات

المدوّنات