أفضل الممارسات الخاصة بالكوروتينات في Android

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

أجهزة إرسال الحقن

عدم استخدام ترميز ثابت لـ 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" لنوع العمل. هذا النمط ومسئوليتك تكمن في الفصل الذي يقوم بالعمل.

يجب أن ينشئ 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)

    /* ... */
}

من المفترض أن تكشف طبقة البيانات وطبقة الأعمال عن دوال التعليق والتدفقات

تكشف الفئات في طبقة البيانات وطبقات الأعمال بشكل عام عن دوال لتنفيذ بلقطة واحدة أو ليتم إشعارك بتغييرات البيانات بمرور الوقت. الفئات في تلك أن تعرض الطبقات دوال التعليق للطلبات التي تتم بلقطة واحدة والتدفق إلى الإشعار بشأن تغييرات البيانات.

// Classes in the data and business layer expose
// either suspend functions or Flows
class ExampleRepository {
    suspend fun makeNetworkRequest() { /* ... */ }

    fun getExamples(): Flow<Example> { /* ... */ }
}

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

إنشاء الكوروتينات في طبقة الأعمال وطبقة البيانات

للفئات في طبقة البيانات أو الأعمال التي تحتاج إلى إنشاء كوروتين لأسباب مختلفة، فهناك خيارات مختلفة.

إذا كان العمل الذي يتم إنجازه على تلك الكوروتينات ذا صلة فقط عندما يكون المستخدم على الشاشة الحالية، يجب أن يتبع دورة حياة المتصل. في معظم في هذه الحالة، سيكون المتصل هو 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,
            testDispatcher
        )
        val article = Article()
        repository.bookmarkArticle(article)
        assertThat(articlesDataSource.isBookmarked(article)).isTrue()
    }
}

يجب أن تتشارك جميع "TestDispatchers" أداة الجدولة نفسها. يتيح لك ذلك تشغيل كل رموز الكوروتين في سلسلة اختبار واحدة لإجراء اختباراتك حتميًا. سينتظر "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)
                // Notify view user logged in successfully
            } catch (exception: IOException) {
                // Notify view login attempt failed
            }
        }
    }
}

لمزيد من المعلومات، يمكنك الاطّلاع على مشاركة المدونة. الاستثناءات في الكوروتين، أو التعامل مع استثناءات Coroutine في وثائق Kotlin.

مزيد من المعلومات عن الكوروتينات

للحصول على المزيد من موارد الكوروتين، يمكنك الاطّلاع على مراجع إضافية حول الكوروتينات في لغة Kotlin وتدفق البيانات .