توفر أنماط "كوروتين" في Kotlin واجهة برمجة تطبيقات تتيح لك كتابة رمز غير متزامن. باستخدام أنماط "كوروتين" في Kotlin، يمكنك تحديد CoroutineScope، ما
يساعدك في إدارة وقت تشغيل أنماط "كوروتين". يتم تشغيل كل عملية غير متزامنة ضمن نطاق معيّن.
توفر المكونات التي تراعي مراحل النشاط دعمًا أساسيًا لأنماط "كوروتين" للنطاقات المنطقية في تطبيقك، بالإضافة إلى طبقة قابلية التشغيل التفاعلي مع LiveData. يوضّح هذا الموضوع كيفية استخدام أنماط "كوروتين" بفعالية مع المكوّنات التي تراعي مراحل النشاط.
إضافة تبعيات KTX
إنّ نطاقات "كوروتين" المضمّنة الموضّحة في هذا الموضوع مضمّنة في KTX extensions لكل مكوّن مطابق. احرِص على إضافة التبعيات المناسبة عند استخدام هذه النطاقات.
- بالنسبة إلى
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
يتم تحديد 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
يتم تحديد 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 أخرى. على سبيل المثال، قد تريد جمع تدفق عندما تكون حالة 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.
}
}
}
}
}
Lifecycle هي STOPPED، تظل التدفقات الأولية نشطة في الخلفية، ما قد يؤدي إلى إرسال عناصر جديدة وإهدار الموارد.
استخدام أنماط "كوروتين" مع 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)
}
)
}
}
}
لمزيد من المعلومات ذات الصلة بأنماط "كوروتين"، يُرجى الاطّلاع على الروابط التالية:
- تحسين أداء التطبيق باستخدام أنماط "كوروتين" في Kotlin
- نظرة عامة على أنماط "كوروتين"
- التعامل مع سلاسل التعليمات في CoroutineWorker
مراجع إضافية
لمزيد من المعلومات حول استخدام أنماط "كوروتين" مع المكوّنات التي تراعي مراحل النشاط، يُرجى الاطّلاع على المراجع الإضافية التالية.
نماذج
المدوّنات
- أنماط "كوروتين" على Android: أنماط التطبيقات
- أنماط "كوروتين" سهلة الاستخدام في Android: نطاق ViewModel
- اختبار عمليتَي إرسال متتاليتَين لـ LiveData في أنماط "كوروتين"