تعرض هذه الصفحة العديد من أفضل الممارسات التي لها تأثير إيجابي من خلال جعل تطبيقك أكثر قابلية للتوسّع والاختبار عند استخدام الروتينات المشتركة.
Inject Dispatchers
لا تستخدم القيمة الثابتة Dispatchers عند إنشاء إجراءات فرعية جديدة أو استدعاء withContext.
// DO inject Dispatchers class NewsRepository( private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default ) { suspend fun loadNews() = withContext(defaultDispatcher) { /* ... */ } } // DO NOT hardcode Dispatchers class NewsRepository { // DO NOT use Dispatchers.Default directly, inject it instead suspend fun loadNews() = withContext(Dispatchers.Default) { /* ... */ } }
يسهّل نمط إدخال التبعيات إجراء الاختبارات، إذ يمكنك استبدال أدوات الإرسال هذه في اختبارات الوحدات واختبارات الأجهزة بأداة إرسال اختبار لجعل اختباراتك أكثر تحديدًا.
يجب أن يكون من الآمن استدعاء الدوال المعلقة من سلسلة التعليمات الرئيسية
يجب أن تكون الدوال المعلقة آمنة للاستخدام في سلسلة التعليمات الرئيسية، أي يمكن استدعاؤها من سلسلة التعليمات الرئيسية. إذا كان أحد الصفوف ينفّذ عمليات حظر طويلة الأمد في روتين فرعي، يكون مسؤولاً عن نقل التنفيذ من سلسلة التعليمات الرئيسية باستخدام withContext. وينطبق ذلك على جميع الفئات في تطبيقك، بغض النظر عن الجزء الذي تنتمي إليه الفئة في بنية التطبيق.
class NewsRepository(private val ioDispatcher: CoroutineDispatcher) { // As this operation is manually retrieving the news from the server // using a blocking HttpURLConnection, it needs to move the execution // to an IO dispatcher to make it main-safe suspend fun fetchLatestNews(): List<Article> { withContext(ioDispatcher) { /* ... implementation ... */ } } } // This use case fetches the latest news and the associated author. class GetLatestNewsWithAuthorsUseCase( private val newsRepository: NewsRepository, private val authorsRepository: AuthorsRepository ) { // This method doesn't need to worry about moving the execution of the // coroutine to a different thread as newsRepository is main-safe. // The work done in the coroutine is lightweight as it only creates // a list and add elements to it suspend operator fun invoke(): Result<List<ArticleWithAuthor>> { val news = newsRepository.fetchLatestNews() val response = mutableListOf<ArticleWithAuthor>() for (article in news) { val author = authorsRepository.getAuthor(article.author) response.add(ArticleWithAuthor(article, author)) } return Result.Success(response) } }
يساعد هذا النمط في توسيع نطاق تطبيقك، إذ لا تحتاج الفئات التي تستدعي دوال تعليق إلى القلق بشأن Dispatcher الذي يجب استخدامه لنوع العمل. وتقع هذه المسؤولية على عاتق الفئة التي تنفّذ العمل.
يجب أن ينشئ ViewModel إجراءات روتينية
يجب أن تفضّل فئات ViewModel إنشاء إجراءات روتينية مشتركة بدلاً من عرض دوال معلّقة لتنفيذ منطق النشاط التجاري. قد يكون تعليق الدوال في ViewModel مفيدًا إذا كان المطلوب هو إصدار قيمة واحدة فقط بدلاً من عرض الحالة باستخدام سلسلة من البيانات.
// DO create coroutines in the ViewModel class LatestNewsViewModel( private val getLatestNewsWithAuthors: GetLatestNewsWithAuthorsUseCase ) : ViewModel() { private val _uiState = MutableStateFlow<LatestNewsUiState>(LatestNewsUiState.Loading) val uiState: StateFlow<LatestNewsUiState> = _uiState fun loadNews() { viewModelScope.launch { val latestNewsWithAuthors = getLatestNewsWithAuthors() _uiState.value = LatestNewsUiState.Success(latestNewsWithAuthors) } } }
// Prefer observable state rather than suspend functions from the ViewModel class LatestNewsViewModel( private val getLatestNewsWithAuthors: GetLatestNewsWithAuthorsUseCase ) : ViewModel() { // DO NOT do this. News would probably need to be refreshed as well. // Instead of exposing a single value with a suspend function, news should // be exposed using a stream of data as in the code snippet above. suspend fun loadNews() = getLatestNewsWithAuthors() }
يجب ألا تؤدي طرق العرض إلى تشغيل أي إجراءات روتينية بشكل مباشر لتنفيذ منطق النشاط التجاري.
بدلاً من ذلك، عليك تفويض هذه المسؤولية إلى ViewModel. ويسهّل ذلك اختبار منطق نشاطك التجاري، لأنّه يمكن إجراء اختبارات الوحدة على عناصر ViewModel، بدلاً من استخدام اختبارات الأجهزة التي تكون مطلوبة لاختبار طرق العرض.
بالإضافة إلى ذلك، ستستمر إجراءاتك الروتينية حتى بعد تغييرات الإعدادات تلقائيًا إذا بدأ العمل في viewModelScope. إذا أنشأت إجراءات روتينية مشتركة باستخدام lifecycleScope بدلاً من ذلك، عليك التعامل معها يدويًا.
إذا كانت الروتين الفرعي بحاجة إلى أن يكون عمره أطول من نطاق ViewModel، يمكنك الاطّلاع على قسم إنشاء الروتينات الفرعية في طبقة النشاط التجاري وطبقة البيانات.
عدم عرض أنواع قابلة للتغيير
يُفضَّل عرض الأنواع غير القابلة للتغيير على الفئات الأخرى. بهذه الطريقة، يتم تجميع جميع التغييرات التي تطرأ على النوع القابل للتغيير في فئة واحدة، ما يسهّل عملية تصحيح الأخطاء عند حدوث مشكلة.
// DO expose immutable types class LatestNewsViewModel : ViewModel() { private val _uiState = MutableStateFlow(LatestNewsUiState.Loading) val uiState: StateFlow<LatestNewsUiState> = _uiState /* ... */ }
class LatestNewsViewModel : ViewModel() { // DO NOT expose mutable types val uiState = MutableStateFlow(LatestNewsUiState.Loading) /* ... */ }
يجب أن تعرض طبقة البيانات وطبقة النشاط التجاري دوال تعليق وFlows
توفّر الفئات في طبقتَي البيانات والنشاط التجاري بشكل عام دوالاً لتنفيذ عمليات طلب لمرة واحدة أو لتلقّي إشعارات بشأن تغييرات البيانات بمرور الوقت. يجب أن تعرض الفئات في هذه الطبقات الدوال المعلقة للمكالمات لمرة واحدة وFlow للإبلاغ عن تغييرات البيانات.
// Classes in the data and business layer expose // either suspend functions or Flows class ExampleRepository { suspend fun makeNetworkRequest() { /* ... */ } fun getExamples(): Flow<Example> { /* ... */ } }
تتيح أفضل الممارسات هذه للمتصل، وهو عادةً طبقة العرض، التحكّم في التنفيذ ودورة الحياة للعمل الذي يتم في تلك الطبقات، وإلغاء العملية عند الحاجة.
إنشاء إجراءات روتينية مشتركة في طبقة النشاط التجاري وطبقة البيانات
بالنسبة إلى الفئات في طبقة البيانات أو طبقة النشاط التجاري التي تحتاج إلى إنشاء إجراءات روتينية مشتركة لأسباب مختلفة، تتوفّر خيارات مختلفة.
إذا كان العمل المطلوب تنفيذه في إجراءات coroutines هذه لا صلة له إلا عندما يكون المستخدم على الشاشة الحالية، يجب أن يتّبع دورة حياة المتصل. في معظم الحالات، سيكون العنصر الذي يستدعي الدالة هو ViewModel، وسيتم إلغاء الاستدعاء عندما ينتقل المستخدم إلى شاشة أخرى ويتم محو ViewModel. في هذه الحالة، يجب استخدام
coroutineScope
أو supervisorScope.
class GetAllBooksAndAuthorsUseCase( private val booksRepository: BooksRepository, private val authorsRepository: AuthorsRepository, ) { suspend fun getBookAndAuthors(): BookAndAuthors { // In parallel, fetch books and authors and return when both requests // complete and the data is ready return coroutineScope { val books = async { booksRepository.getAllBooks() } val authors = async { authorsRepository.getAllAuthors() } BookAndAuthors(books.await(), authors.await()) } } }
إذا كانت المهمة المطلوب تنفيذها ذات صلة طالما أنّ التطبيق مفتوح، ولم تكن المهمة مرتبطة بشاشة معيّنة، يجب أن تستمر المهمة بعد انتهاء عمر دورة حياة المتصل. في هذا السيناريو، يجب استخدام CoroutineScope خارجي كما هو موضّح في مشاركة المدونة حول الروتينات الفرعية والأنماط الخاصة بالمهام التي لا يجب إلغاؤها.
class ArticlesRepository( private val articlesDataSource: ArticlesDataSource, private val externalScope: CoroutineScope, ) { // As we want to complete bookmarking the article even if the user moves // away from the screen, the work is done creating a new coroutine // from an external scope suspend fun bookmarkArticle(article: Article) { externalScope.launch { articlesDataSource.bookmarkArticle(article) } .join() // Wait for the coroutine to complete } }
يجب إنشاء externalScope وإدارته من خلال فئة تظل نشطة لفترة أطول من الشاشة الحالية، ويمكن إدارته من خلال الفئة Application أو ViewModel ضمن نطاق الرسم البياني للتنقّل.
إدخال TestDispatchers في الاختبارات
يجب إدخال مثيل من
TestDispatcher
في صفوفك أثناء الاختبارات. يتوفّر خياران للتنفيذ في مكتبة kotlinx-coroutines-test:
StandardTestDispatcher: يضع في قائمة الانتظار الكوروتينات التي تم تشغيلها باستخدام أداة جدولة، وينفّذها عندما لا يكون مؤشر ترابط الاختبار مشغولاً. يمكنك تعليق سلسلة الاختبار للسماح بتشغيل الروتينات الفرعية الأخرى في قائمة الانتظار باستخدام طرق مثلadvanceUntilIdle.
UnconfinedTestDispatcher: يُشغّل كوروتينات جديدة بشكل نشط وبطريقة حظر. يؤدي ذلك عادةً إلى تسهيل كتابة الاختبارات، ولكنّه يمنحك تحكّمًا أقل في كيفية تنفيذ الروتينات المشتركة أثناء الاختبار.
يمكنك الاطّلاع على مستندات كل عملية تنفيذ لبرنامج الإرسال للحصول على تفاصيل إضافية.
لاختبار الروتينات المشتركة، استخدِم أداة إنشاء الروتينات المشتركة runTest. تستخدم runTest
TestCoroutineScheduler
لتخطّي التأخيرات في الاختبارات والسماح لك بالتحكّم في الوقت الافتراضي. يمكنك أيضًا استخدام أداة الجدولة هذه لإنشاء أدوات إرسال اختبار إضافية حسب الحاجة.
class ArticlesRepositoryTest { @Test fun testBookmarkArticle() = runTest { // Pass the testScheduler provided by runTest's coroutine scope to // the test dispatcher val testDispatcher = UnconfinedTestDispatcher(testScheduler) val articlesDataSource = FakeArticlesDataSource() val repository = ArticlesRepository( articlesDataSource, defaultDispatcher = testDispatcher ) val article = Article() repository.bookmarkArticle(article) assertThat(articlesDataSource.isBookmarked(article)).isTrue() } }
يجب أن تشترك جميع TestDispatchers في أداة الجدولة نفسها. يتيح لك ذلك تشغيل كل رموز coroutine البرمجية على سلسلة الاختبار الفردية لجعل اختباراتك حتمية. ستنتظر الدالة runTest إلى أن تكتمل جميع الروتينات الفرعية التي تعمل على المجدول نفسه أو التي تندرج ضمن الروتين الفرعي للاختبار قبل أن تعرض النتيجة.
تجنُّب GlobalScope
هذا مشابه لأفضل الممارسات المتعلّقة بإدخال أدوات إرسال. باستخدام
GlobalScope،
ستعمل على ترميز CoroutineScope الذي يستخدمه أحد الصفوف، ما يؤدي إلى بعض السلبيات، وهي:
تشجّع على ترميز القيم بشكل ثابت. إذا كنت ترمّز
GlobalScopeبشكل ثابت، قد ترمّزDispatchersبشكل ثابت أيضًا.يصعّب ذلك عملية الاختبار لأنّ الرمز البرمجي يتم تنفيذه في نطاق غير خاضع للتحكّم، ولن تتمكّن من التحكّم في تنفيذه.
لا يمكنك استخدام
CoroutineContextمشترك لتنفيذه لجميع الروتينات الفرعية المضمّنة في النطاق نفسه.
بدلاً من ذلك، ننصحك بإضافة CoroutineScope إلى العمل الذي يجب أن يستمر
بعد النطاق الحالي. يمكنك الاطّلاع على قسم "إنشاء إجراءات روتينية في طبقة النشاط التجاري وطبقة البيانات" لمعرفة المزيد حول هذا الموضوع.
// DO inject an external scope instead of using GlobalScope. // GlobalScope can be used indirectly. Here as a default parameter makes sense. class ArticlesRepository( private val articlesDataSource: ArticlesDataSource, private val externalScope: CoroutineScope = GlobalScope, private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default ) { // As we want to complete bookmarking the article even if the user moves // away from the screen, the work is done creating a new coroutine // from an external scope suspend fun bookmarkArticle(article: Article) { externalScope.launch(defaultDispatcher) { articlesDataSource.bookmarkArticle(article) } .join() // Wait for the coroutine to complete } }
// DO NOT use GlobalScope directly class ArticlesRepository( private val articlesDataSource: ArticlesDataSource, ) { // As we want to complete bookmarking the article even if the user moves away // from the screen, the work is done creating a new coroutine with GlobalScope suspend fun bookmarkArticle(article: Article) { GlobalScope.launch { articlesDataSource.bookmarkArticle(article) } .join() // Wait for the coroutine to complete } }
يمكنك الاطّلاع على مزيد من المعلومات حول GlobalScope وبدائله في
منشور المدوّنة حول الروتينات المشتركة والأنماط الخاصة بالمهام التي لا يجب إلغاؤها.
إتاحة إمكانية إلغاء الروتين الفرعي
يتم إلغاء الكوروتينات بشكل تعاوني، ما يعني أنّه عند إلغاء Job لكوروتين، لن يتم إلغاء الكوروتين إلى أن يتم تعليقه أو التحقّق من حالة الإلغاء. إذا كنت تجري عمليات حظر في روتين فرعي، تأكَّد من أنّ الروتين الفرعي قابل للإلغاء.
على سبيل المثال، إذا كنت تقرأ ملفات متعددة من القرص، عليك التحقّق مما إذا تم إلغاء الروتين المشترك قبل بدء قراءة كل ملف. إحدى طرق التحقّق من حالة الإلغاء هي استدعاء الدالة ensureActive.
someScope.launch { for (file in files) { ensureActive() // Check for cancellation readFile(file) } }
يمكن إلغاء جميع وظائف التعليق من kotlinx.coroutines، مثل withContext وdelay. إذا كانت الروتينات الفرعية تستدعيها، لن تحتاج إلى اتّخاذ أي إجراء إضافي.
لمزيد من المعلومات حول الإلغاء في الكوروتينات، يمكنك الاطّلاع على مشاركة المدوّنة حول الإلغاء في الكوروتينات.
الانتباه إلى الاستثناءات
يمكن أن تتسبّب الاستثناءات التي لم تتم معالجتها في الروتينات الفرعية في تعطُّل تطبيقك. إذا كان من المحتمل حدوث استثناءات، عليك رصدها في نص أي إجراءات روتينية مشتركة تم إنشاؤها باستخدام viewModelScope أو lifecycleScope.
class LoginViewModel( private val loginRepository: LoginRepository ) : ViewModel() { fun login(username: String, token: String) { viewModelScope.launch { try { loginRepository.login(username, token) // Update UI, user logged in successfully } catch (exception: IOException) { // Update UI, login attempt failed } } } }
لمزيد من المعلومات، يمكنك الاطّلاع على منشور المدوّنة الاستثناءات في الكوروتينات أو التعامل مع استثناءات الكوروتينات في مستندات Kotlin.
مزيد من المعلومات عن الروتينات الفرعية
للحصول على مزيد من المراجع حول الكوروتينات، يمكنك الاطّلاع على دليل الكوروتينات في مستندات Kotlin.