این صفحه چندین روال مطلوب را ارائه میدهد که با مقیاسپذیرتر و آزمایشپذیرتر کردن برنامه شما هنگام استفاده از روتینهای همکار، تأثیر مثبتی دارند.
تزریق توزیعکنندهها
هنگام ایجاد روتینهای همزمان جدید یا فراخوانی 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) { /* ... */ } }
این الگوی تزریق وابستگی آزمایش را آسانتر میکند زیرا میتوانید آن توزیعکنندهها را در آزمونهای واحد و ابزار دقیق با توزیعکننده آزمون جایگزین کنید تا آزمونهایتان قطعیتر شوند.
فراخوانی توابع تعلیق باید از رشته اصلی ایمن باشد
توابع تعلیق باید main-safe باشند، یعنی فراخوانی آنها از رشته اصلی ایمن باشد. اگر کلاسی در روتین همکار عملیات مسدودکننده طولانیمدت انجام میدهد، مسئولیت دارد بااستفاده از
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) /* ... */ }
لایه داده و کسبوکار باید توابع تعلیق و «جریانها» را آشکار کند
کلاسهای موجود در لایههای داده و کسبوکار معمولاً توابعی را برای انجام تماسهای یکباره یا برای مطلع شدن از تغییرات داده در طول زمان آشکار میکنند. کلاسهای موجود در آن لایهها باید توابع تعلیق برای تماسهای یکباره و جریان برای اعلام تغییرات دادهها را آشکار کنند.
// 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, defaultDispatcher = 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) // Update UI, user logged in successfully } catch (exception: IOException) { // Update UI, login attempt failed } } } }
برای اطلاعات بیشتر، پست وبلاگ استثناها در روتینهای همکار، یا مدیریت استثناهای روتین همکار در مستندات Kotlin را بررسی کنید.
درباره زیربرنامهها بیشتر بدانید
برای منابع بیشتر درباره روتینهای همکار، به راهنمای روتینهای همکار در مستندات Kotlin مراجعه کنید.