این صفحه چندین روش برتر را ارائه میکند که با مقیاسپذیرتر کردن و آزمایشپذیرتر کردن برنامه شما در هنگام استفاده از برنامههای مشترک، تأثیر مثبتی دارند.
دیسپچرهای تزریق
هنگام ایجاد برنامه های جدید یا تماس 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 مدیریت می شود را بررسی کنید.
درباره کوروتین ها بیشتر بدانید
برای منابع بیشتر کوروتین ها، به منابع اضافی برای کوروتین ها و صفحه جریان کاتلین مراجعه کنید.