تقدم هذه الصفحة العديد من أفضل الممارسات التي لها تأثير إيجابي من خلال جعل تطبيقك أكثر قابلية للتطور والاختبار عند استخدام الكوروتينات.
مرسلو الحقن
لا تستخدم رمزًا ثابتًا لـ 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(): List<ArticleWithAuthor> {
val news = newsRepository.fetchLatestNews()
val response: List<ArticleWithAuthor> = mutableEmptyList()
for (article in news) {
val author = authorsRepository.getAuthor(article.author)
response.add(ArticleWithAuthor(article, author))
}
return Result.Success(response)
}
}
يجعل هذا النمط تطبيقك أكثر قابلية للتطوّر، لأنّ وظائف استدعاء الصفوف الدراسية المعلّقة
لا داعي للقلق بشأن استخدام Dispatcher
لنوع العمل. وتكمن هذه المسؤولية في الفصل الذي ينفذ العمل.
يجب أن يُنشئ ViewView الكوروتينات
يجب أن تفضل فئات 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)
/* ... */
}
يجب أن تكشف طبقة البيانات وطبقة النشاط التجاري دوال التعليق والتدفقات
تكشف الفئات في طبقات البيانات والأنشطة التجارية عمومًا عن وظائف لإجراء استدعاءات من نقطة واحدة أو إخطارها بالتغييرات في البيانات بمرور الوقت. يجب أن تعرض الفئات في هذه الطبقات وظائف تعليق المكالمات التي تتم من خلال لقطة واحدة والتدفق إلى التنبيه بشأن تغييرات البيانات.
// Classes in the data and business layer expose
// either suspend functions or Flows
class ExampleRepository {
suspend fun makeNetworkRequest() { /* ... */ }
fun getExamples(): Flow<Example> { /* ... */ }
}
ومن خلال أفضل الممارسات هذه، يمكن للمتصل، بشكل عام، طبقة العرض التقديمي، التحكم في تنفيذ ودورة الحياة للعمل الذي يحدث في تلك الطبقات، وإلغاء ذلك عند الحاجة.
إنشاء كوروتين في طبقة النشاط التجاري والبيانات
بالنسبة إلى الصفوف في طبقة البيانات أو النشاط التجاري التي تحتاج إلى إنشاء الكوروتينات لأسباب مختلفة، هناك خيارات مختلفة.
إذا كان العمل الذي سيتم إنجازه في هذه الكوروتينات ملائمًا فقط عند وجود المستخدم على الشاشة الحالية، يجب أن يتبع دورة حياة المتصل. في معظم الحالات، سيكون المتصل هو نموذج العرض، وسيتم إلغاء المكالمة عندما ينتقل المستخدم بعيدًا عن الشاشة ويتم محو نموذج العرض. في هذه الحالة، يجب استخدام
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,
testDispatcher
)
val article = Article()
repository.bookmarkArticle(article)
assertThat(articlesDataSource.isBookmarked(article)).isTrue()
}
}
يجب أن يشارك جميع المستخدمين TestDispatchers
الجدول الزمني نفسه. وهذا يسمح لك بتشغيل شفرة الكورتين بالكامل في سلسلة الاختبار الفردية لتحديد اختباراتك. runTest
سينتظرون حتى تكتمل جميع الكوروتينات الموجودة على نفس أداة الجدولة أو الأطفال من الكوروتين الاختباري قبل العودة.
تجنب النطاق العام
وهذا مماثل لأفضل ممارسة إدخال المرسلين. من خلال استخدام
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
، لا يتم إلغاء الكوروتين إلى أن يتم تعليقه أو التحقّق
من إلغائه. إذا كنت تمنع العمليات في الكوروتين، فتأكد من أن الكوروتينقابل للإلغاء {0}{/0}.
على سبيل المثال، إذا كنت تقرأ عدة ملفات من القرص، قبل بدء قراءة كل ملف، تحقَّق مما إذا كان قد تم إلغاء الكوروتين. ومن طرق التحقّق من الإلغاء
طلب الوظيفة 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)
// Notify view user logged in successfully
} catch (exception: IOException) {
// Notify view login attempt failed
}
}
}
}
للحصول على المزيد من المعلومات، يُرجى الاطّلاع على مشاركة المدونة استثناءات في الكوروتينات أو معالجة استثناءات الكوروتين في مستندات Kotlin.
مزيد من المعلومات عن الكوروتينات
لمزيد من موارد الكوروتين، راجع صفحة موارد إضافية لكوروتينات Kotlin وتدفقها.