בדף זה מוצגות מספר שיטות מומלצות בעלות השפעה חיובית, על ידי שהאפליקציה שלכם ניתנת להתאמה ולבדיקה כאשר משתמשים בקורוטינים.
מספרות להזרקה
אין לכתוב בתוך הקוד 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) { /* ... */ }
}
הדפוס הזה של החדרת התלות מקל על הבדיקה, כי אפשר להחליף אותם משלחים בבדיקות יחידה ומכשירים עם מרכז בדיקות כדי שהבדיקות יהיו דטרמיניסטיות יותר.
צריך לאפשר קריאה לפונקציות של השעיה מה-thread הראשי
פונקציות השעיה צריכות להיות בטוחות לשימוש הראשי, כלומר אפשר לקרוא להן בבטחה דרך
של ה-thread הראשי. אם הכיתה מבצעת פעולות חסימה ממושכות
שגרת קורוטין, היא זו שאחראית להוציא את הביצוע מה-thread הראשי באמצעות
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)
/* ... */
}
השכבה של הנתונים והעסק צריכה לחשוף פונקציות השעיה ו-Fflows
מחלקות בנתונים ובשכבות העסקיות חושפות בדרך כלל פונקציות לביצוע או קבלת התראות על שינויים בנתונים לאורך זמן. כיתות בנושאים האלה השכבות צריכות לחשוף פונקציות השעיה לשיחות חד-פעמיות ו-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
חיצוני בתור
שמוסבר בקטע קורוטין תבניות לעבודה שלא צריך לבטל את הפוסט בבלוג
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
בוטל, הקואוטין לא מבוטלת עד שהוא מושעה או בודק
לביטול. אם אתה מבצע פעולות חסימה בקורוטין, עליך לוודא
שהקורוטין ניתנת לביטול.
לדוגמה, אם קוראים מספר קבצים מהדיסק, לפני שמתחילים
לבדוק כל קובץ, ולבדוק אם הקורוטינה בוטלה. אחת הדרכים
כדי לבדוק אם יש ביטול, צריך להתקשר אל
ensureActive
מותאמת אישית.
someScope.launch {
for(file in files) {
ensureActive() // Check for cancellation
readFile(file)
}
}
כל פונקציות ההשעיה של kotlinx.coroutines
, כמו withContext
ו-
אפשר לבטל את delay
. אם העובר, אין צורך לעשות זאת
כל עבודה נוספת.
למידע נוסף על ביטולים בקורוטינים, אפשר לעיין ביטול בפוסט בבלוג coroutines.
היזהרו מחריגים
חריגים לא מטופלים שנשלפים דרך קורוטין עלולים לגרום לקריסת האפליקציה. אם יש חריגים
שעלולות לקרות, תתפסו אותם בגוף של תרופת קורוטין שנוצרו
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 הדף הזה.