שיטות מומלצות בנוגע לקורוטינים ב-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) { /* ... */ }
}

הדפוס הזה של החדרת התלות מקל על הבדיקה, כי אפשר להחליף אותם משלחים בבדיקות יחידה ומכשירים עם מרכז בדיקות כדי שהבדיקות יהיו דטרמיניסטיות יותר.

צריך לאפשר קריאה לפונקציות של השעיה מה-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 הדף הזה.