קורוטינים ב-Kotlin ב-Android

קורוטין היא תבנית עיצוב בו-זמניות שאפשר להשתמש בה. Android כדי לפשט את הקוד שמופעל באופן אסינכרוני. קורוטין נוספו ל-Kotlin בגרסה 1.3 ומבוססים על משפות אחרות.

ב-Android, קורוטין עוזרים בניהול משימות ממושכות, אחרת, חוסמים את ה-thread הראשי וגורמים לאפליקציה להפסיק להגיב. יותר מ-50% מהמפתחים המקצועיים שמשתמשים בקורוטינים דיווחו שהם ראו פרודוקטיביות מוגברת. בנושא הזה נסביר איך להשתמש בקורוטינים של Kotlin כדי לטפל וכך תוכלו לכתוב קוד אפליקציה נקי ותמציתי יותר.

תכונות

Coroutines הוא הפתרון המומלץ שלנו לתכנות אסינכרוני Android. התכונות הבאות כוללות את התכונות הבאות:

  • משקל קל: אתם יכולים להריץ הרבה קורוטינים בשרשור אחד בגלל תמיכה עבור השעיה, כך שהיא לא חוסמת את השרשור שבו פועלת הקורוטיין. מושעה חוסכת זיכרון מפני חסימה, תוך תמיכה בפעולות רבות בו-זמנית.
  • פחות דליפות זיכרון: כדאי להשתמש בו-זמניות מובנית כדי להריץ פעולות במסגרת היקף.
  • תמיכה מובנית בביטולים: ביטול מופצת באופן אוטומטי דרך ההיררכיה הרצה של ה-coroutine.
  • שילוב Jetpack: ספריות רבות של Jetpack כוללות תוספים שמספקים תמיכה מלאה בקורוטינים. במידה מסוימת מספקות גם coroutine scope, שאפשר להשתמש בו לשימוש בו-זמניות מובנה.

סקירה כללית של דוגמאות

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

באופן ספציפי, ViewModel הרכיב הארכיטקטורה קורא לשכבת המאגר ב-thread הראשי כדי תפעיל את בקשת הרשת. המדריך הזה חוזר על עצמו באמצעות פתרונות שונים שמשתמשים בקורוטינים כדי למנוע את החסימה של ה-thread הראשי.

ViewModel כולל קבוצה של תוספי KTX שפועלים ישירות עם וקורוטינים. התוספים האלה lifecycle-viewmodel-ktx Library והם נמצאים בשימוש במדריך הזה.

פרטי תלות

כדי להשתמש בקורוטינים בפרויקט Android, צריך להוסיף את התלות הבאה לקובץ build.gradle של האפליקציה:

מגניב

dependencies {
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
}

Kotlin

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9")
}

הפעולה מתבצעת בשרשור ברקע

יצירת בקשת רשת ב-thread הראשי גורמת להמתנה או חסימה של, עד שהוא מקבל תשובה. מכיוון שה-thread חסום, מערכת ההפעלה לא חסומה יכול להתקשר אל onDraw(), דבר שגורם לאפליקציה לקפוא מובילה לתיבת הדו-שיח 'האפליקציה לא מגיבה' (ANR). למשתמש טוב יותר נריץ את הפעולה הזו על שרשור ברקע.

קודם כול, נבחן את הקורס Repository ונראה איך הוא ששולחים את בקשת הרשת:

sealed class Result<out R> {
    data class Success<out T>(val data: T) : Result<T>()
    data class Error(val exception: Exception) : Result<Nothing>()
}

class LoginRepository(private val responseParser: LoginResponseParser) {
    private const val loginUrl = "https://example.com/login"

    // Function that makes the network request, blocking the current thread
    fun makeLoginRequest(
        jsonBody: String
    ): Result<LoginResponse> {
        val url = URL(loginUrl)
        (url.openConnection() as? HttpURLConnection)?.run {
            requestMethod = "POST"
            setRequestProperty("Content-Type", "application/json; utf-8")
            setRequestProperty("Accept", "application/json")
            doOutput = true
            outputStream.write(jsonBody.toByteArray())
            return Result.Success(responseParser.parse(inputStream))
        }
        return Result.Error(Exception("Cannot open HttpURLConnection"))
    }
}

התוסף makeLoginRequest סינכרוני וחוסם את השרשור של השיחה. כדי ליצור מודל את התגובה לבקשת הרשת, יש לנו מחלקה משלנו Result.

השדה ViewModel מפעיל את בקשת הרשת כשהמשתמש לוחץ, למשך לדוגמה, בלחצן:

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun login(username: String, token: String) {
        val jsonBody = "{ username: \"$username\", token: \"$token\"}"
        loginRepository.makeLoginRequest(jsonBody)
    }
}

עם הקוד הקודם, LoginViewModel חוסם את ה-thread של ממשק המשתמש כאשר ששולחים את בקשת הרשת. הפתרון הכי פשוט להעביר את ההפעלה מחוץ ל-thread הראשי הוא ליצור קורוטין חדשה ולהפעיל את הרשת בקשה בשרשור קלט/פלט:

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun login(username: String, token: String) {
        // Create a new coroutine to move the execution off the UI thread
        viewModelScope.launch(Dispatchers.IO) {
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"
            loginRepository.makeLoginRequest(jsonBody)
        }
    }
}

עכשיו ננתח את קוד הקורוטינים בפונקציה login:

  • viewModelScope הוא CoroutineScope מוגדר מראש שנכלל עם תוספי ה-KTX ViewModel. שימו לב שכל ה-coroutines חייבים לפעול היקף. CoroutineScope מנהל/ת קורוטין קשורה אחת או יותר.
  • launch היא פונקציה שיוצרת קורוטין ושולחת ביצוע של גוף הפונקציה שלו לסידור התואם.
  • Dispatchers.IO מציין שיש לבצע את הקורוטין שרשור ששמור לפעולות קלט/פלט (I/O).

הפונקציה login מתבצעת באופן הבא:

  • האפליקציה מפעילה את הפונקציה login מהשכבה View ב-thread הראשי.
  • launch יוצר קורוטין חדש ובקשת הרשת מתבצעת בנפרד, בשרשור ששמור לפעולות קלט/פלט.
  • בזמן שהקורוטין פועלת, הפונקציה login ממשיכה לבצע ויחזירו, כנראה לפני שבקשת הרשת תסתיים. שימו לב כדי לשמור על פשטות, בשלב זה המערכת מתעלמת מתגובת הרשת.

מכיוון שהקורוטין הזו מתחילה ב-viewModelScope, היא מתבצעת ההיקף של ViewModel. אם ViewModel מושמד כי המשתמש מנווט אל מחוץ למסך, המשתמש viewModelScope מנווט אוטומטית בוטלו, וגם כל הקורוטינים שפעילים מבוטלים.

אחת הבעיות בדוגמה הקודמת היא שכל מה שמתקשר makeLoginRequest צריך לזכור להסיר באופן מפורש את הביצוע בשרשור הראשי. בואו נראה איך נוכל לשנות את Repository כדי לפתור את הבעיה את הבעיה הזאת עבורנו.

שימוש בקורוטינים לשמירה על הבטיחות העיקרית

פונקציה מסוימת נחשבת כבטוחה ראשית כשהיא לא חוסמת עדכונים של ממשק המשתמש ה-thread הראשי. הפונקציה makeLoginRequest אינה בטוחה הראשית, מפני שהתקשרות makeLoginRequest מה-thread הראשי חוסם את ממשק המשתמש. משתמשים ב הפונקציה withContext() מספריית Coroutines כדי להעביר את ההפעלה של קורוטין לשרשור אחר:

class LoginRepository(...) {
    ...
    suspend fun makeLoginRequest(
        jsonBody: String
    ): Result<LoginResponse> {

        // Move the execution of the coroutine to the I/O dispatcher
        return withContext(Dispatchers.IO) {
            // Blocking network request code
        }
    }
}

withContext(Dispatchers.IO) מעביר את הביצוע של הקורוטין שרשור קלט/פלט (I/O), שהופך את פונקציית הקריאה לבטוחה יותר, ומאפשר לממשק המשתמש לבצע מעדכנים לפי הצורך.

makeLoginRequest מסומן גם באמצעות מילת המפתח suspend. מילת המפתח הזו היא הדרך של Kotlin כדי לאכוף קריאה לפונקציה מתוך קורוטין.

בדוגמה הבאה, הקורוטין נוצרת ב-LoginViewModel. כש-makeLoginRequest מעביר את הביצוע מה-thread הראשי, הקורוטין אפשר להריץ את הפונקציה login עכשיו בשרשור הראשי:

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun login(username: String, token: String) {

        // Create a new coroutine on the UI thread
        viewModelScope.launch {
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"

            // Make the network call and suspend execution until it finishes
            val result = loginRepository.makeLoginRequest(jsonBody)

            // Display result of the network request to the user
            when (result) {
                is Result.Success<LoginResponse> -> // Happy path
                else -> // Show error in UI
            }
        }
    }
}

חשוב לשים לב שעדיין נדרשת כאן קורוטין, כי makeLoginRequest פונקציית suspend, וכל הפונקציות של suspend צריכות להתבצע קווטין.

הקוד הזה שונה מהדוגמה הקודמת login בכמה אופנים:

  • launch לא מקבל פרמטר Dispatchers.IO. כאשר לא עושים זאת להעביר Dispatcher אל launch, כל קורוטינים ששוגרו מ הרצה של viewModelScope בשרשור הראשי.
  • התוצאה של בקשת הרשת מטופלת עכשיו כדי להציג את הצלחה או כשל בממשק המשתמש.

פונקציית ההתחברות רצה עכשיו באופן הבא:

  • האפליקציה מפעילה את הפונקציה login() מהשכבה View ב-thread הראשי.
  • launch יוצר קורוטין חדש ב-thread הראשי, והקורוטין מתחיל להגדיר.
  • בתוך הקורוטינה, הקריאה ל-loginRepository.makeLoginRequest() מששעה את המשך הביצוע של הקורוטיין עד withContext חסימה ב-makeLoginRequest() מסיימת לפעול.
  • כשהחסימה של withContext תסתיים, הקורוטין בlogin() יתחדש ביצוע ב-thread הראשי עם התוצאה של בקשת הרשת.

טיפול בחריגים

כדי לטפל בחריגים שיכולים לגרום לשכבה Repository, אפשר להשתמש בפונקציית Kotlin תמיכה מובנית בחריגים. בדוגמה הבאה, אנחנו משתמשים בבלוק try-catch:

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun login(username: String, token: String) {
        viewModelScope.launch {
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"
            val result = try {
                loginRepository.makeLoginRequest(jsonBody)
            } catch(e: Exception) {
                Result.Error(Exception("Network request failed"))
            }
            when (result) {
                is Result.Success<LoginResponse> -> // Happy path
                else -> // Show error in UI
            }
        }
    }
}

בדוגמה הזו, כל חריגה לא צפויה שנרשמה על ידי makeLoginRequest() תטופל כשגיאה בממשק המשתמש.

משאבים נוספים בנושא קורוטינים

למבט מפורט יותר על קורוטין ב-Android, אפשר לבקר בכתובת לשפר את ביצועי האפליקציה בעזרת קורוטינים ב-Kotlin.

למשאבים נוספים בנושא קורוטינים, ראו את הקישורים הבאים: