بهترین روش‌ها برای کوروتین‌ها در اندروید

این صفحه چندین روش برتر را ارائه می‌کند که با مقیاس‌پذیرتر کردن و آزمایش‌پذیرتر کردن برنامه شما در هنگام استفاده از برنامه‌های مشترک، تأثیر مثبتی دارند.

دیسپچرهای تزریق

هنگام ایجاد برنامه های جدید یا تماس withContext ، Dispatchers را کدگذاری نکنید.

// 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 عمر کند، بخش Creating coroutines را در بخش لایه کسب و کار و داده بررسی کنید.

انواع قابل تغییر را در معرض نمایش قرار ندهید

ترجیح می دهند انواع تغییرناپذیر را در معرض کلاس های دیگر قرار دهند. به این ترتیب، تمام تغییرات در نوع قابل تغییر در یک کلاس متمرکز می‌شود و در صورت بروز مشکل، اشکال‌زدایی را آسان‌تر می‌کند.

// 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)

    /* ... */
}

لایه داده و کسب و کار باید توابع تعلیق و جریان ها را نشان دهد

کلاس‌ها در لایه‌های داده و کسب‌وکار عموماً عملکردهایی را برای انجام تماس‌های تک‌شات یا اطلاع‌رسانی از تغییرات داده در طول زمان نشان می‌دهند. کلاس‌های موجود در آن لایه‌ها باید توابع تعلیق را برای تماس‌های یک‌شات و Flow برای اطلاع در مورد تغییرات داده را در معرض نمایش قرار دهند.

// 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 خارجی همانطور که در Coroutines & Patterns توضیح داده شده برای کارهایی که نباید پست وبلاگ لغو شوند استفاده شود.

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 اجتناب کنید

این شبیه به بهترین روش Inject Dispatchers است. با استفاده از GlobalScope ، CoroutineScope که یک کلاس از آن استفاده می‌کند و برخی از جنبه‌های منفی آن را به همراه دارد، کدگذاری می‌کنید:

  • ارزش های کدگذاری سخت را ترویج می کند. اگر GlobalScope هاردکد می کنید، ممکن است Dispatchers کدنویسی سخت نیز باشید.

  • آزمایش را بسیار سخت می کند زیرا کد شما در یک محدوده کنترل نشده اجرا می شود، شما نمی توانید اجرای آن را کنترل کنید.

  • شما نمی توانید یک CoroutineContext مشترک برای اجرا برای همه کوروتین های تعبیه شده در خود دامنه داشته باشید.

در عوض، تزریق CoroutineScope برای کارهایی که باید از محدوده فعلی بیشتر شود، در نظر بگیرید. برای کسب اطلاعات بیشتر در مورد این موضوع ، بخش Creating coroutines در بخش کسب و کار و لایه داده را بررسی کنید.

// 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 و جایگزین های آن در Coroutines & Patterns برای کارهایی که نباید پست وبلاگ لغو شوند، بیشتر بیاموزید.

کار روتین خود را لغو کنید

لغو در کوراتین‌ها مشارکتی است، به این معنی که وقتی 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
            }
        }
    }
}

برای اطلاعات بیشتر، پست وبلاگ Exceptions in coroutines یا Exceptions Coroutine که در اسناد Kotlin مدیریت می شود را بررسی کنید.

درباره کوروتین ها بیشتر بدانید

برای منابع بیشتر کوروتین ها، به منابع اضافی برای کوروتین ها و صفحه جریان کاتلین مراجعه کنید.