أفضل الممارسات المتعلقة بالكوروتينات في 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 لنوع العمل. وتكمن هذه المسؤولية في الفصل الذي ينفذ العمل.

يجب أن يُنشئ 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 وتدفقها.