קורוטינה היא תבנית עיצוב של פעולות מקבילות שאפשר להשתמש בה ב-Android כדי לפשט קוד שמופעל באופן אסינכרוני. Coroutines נוספו ל-Kotlin בגרסה 1.3 ומבוססים על מושגים מוכרים משפות אחרות.
ב-Android, קורוטינות עוזרות לנהל משימות ארוכות טווח שאחרת עלולות לחסום את השרשור הראשי ולגרום לאפליקציה להפסיק להגיב. יותר מ-50% מהמפתחים המקצועיים שמשתמשים ב-coroutines דיווחו על עלייה בפרודוקטיביות. בנושא הזה מוסבר איך אפשר להשתמש ב-Kotlin coroutines כדי לפתור את הבעיות האלה, וכך לכתוב קוד אפליקציה נקי ותמציתי יותר.
תכונות
שגרות המשך (coroutines) הן הפתרון המומלץ שלנו לתכנות אסינכרוני ב-Android. התכונות הבולטות כוללות את הדברים הבאים:
- קל משקל: אפשר להריץ הרבה קורוטינות בשרשור יחיד בזכות התמיכה בהשהיה, שלא חוסמת את השרשור שבו הקורוטינה פועלת. השהיה חוסכת זיכרון יותר מחסימה, ותומכת בהרבה פעולות בו-זמניות.
- פחות דליפות זיכרון: אפשר להשתמש במקביליות מובנית כדי להריץ פעולות בהיקף מסוים.
- תמיכה מובנית בביטול: ביטול מופץ באופן אוטומטי בהיררכיית הקורוטינות הפועלות.
- שילוב עם Jetpack: הרבה ספריות של Jetpack כוללות תוספים שמספקים תמיכה מלאה בשגרות משנה. חלק מהספריות מספקות גם היקף משתני קורוטינה משלהן, שאפשר להשתמש בהן כדי ליצור מקביליות מובנית.
סקירה כללית של דוגמאות
על סמך המדריך לארכיטקטורת אפליקציות, הדוגמאות בנושא הזה שולחות בקשה לרשת ומחזירות את התוצאה לשרשור הראשי, שבו האפליקציה יכולה להציג את התוצאה למשתמש.
באופן ספציפי, רכיב הארכיטקטורה ViewModel קורא לשכבת המאגר ב-thread הראשי כדי להפעיל את בקשת הרשת. במדריך הזה מוצגים פתרונות שונים שמשתמשים ב-coroutines כדי למנוע חסימה של ה-thread הראשי.
ViewModel כולל קבוצה של תוספי KTX שפועלים ישירות עם קורוטינות. התוספים האלה הם ספריית lifecycle-viewmodel-ktx ונעשה בהם שימוש במדריך הזה.
פרטי התלות
כדי להשתמש בקורוטינות בפרויקט 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") }
ביצוע בשרשור ברקע
כשמבצעים בקשת רשת בשרשור הראשי, השרשור מחכה או נחסם עד שהוא מקבל תשובה. מכיוון שהשרשור חסום, מערכת ההפעלה לא יכולה לקרוא ל-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>() } private const val loginUrl = "https://example.com/login" class LoginRepository(private val responseParser: LoginResponseParser) { // 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 היא סינכרונית וחוסמת את ה-thread שקורא לה. כדי לדמות את התגובה של בקשת הרשת, יש לנו מחלקה משלנו בשם 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 חוסמת את ה-UI thread כשמתבצעת בקשת הרשת. הפתרון הפשוט ביותר להעברת הביצוע מה-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. חשוב לזכור שכל הקורוטינות חייבות לפעול בהיקף. CoroutineScopeמנהל קורוטינה אחת או יותר שקשורות זו לזו. -
launchהיא פונקציה שיוצרת קורוטינה ושולחת את ההפעלה של גוף הפונקציה שלה למפיץ המתאים. -
Dispatchers.IOמציין שצריך להריץ את הקורוטינה הזו ב-thread ששמור לפעולות קלט/פלט.
הפונקציה login מופעלת באופן הבא:
- האפליקציה קוראת לפונקציה
loginמהשכבהViewב-thread הראשי. -
launchיוצרת קורוטינה חדשה, ובקשת הרשת מתבצעת באופן עצמאי בשרשור ששמור לפעולות קלט/פלט. - בזמן שהקורוטינה פועלת, הפונקציה
loginממשיכה לפעול ומחזירה ערך, יכול להיות שלפני שהבקשה לרשת מסתיימת. שימו לב: בשלב הזה אנחנו מתעלמים מתגובת הרשת כדי לפשט את התהליך.
מכיוון שהקורוטינה הזו מופעלת באמצעות viewModelScope, היא מבוצעת בהיקף של ViewModel. אם ViewModel נהרס כי המשתמש עובר למסך אחר, viewModelScope מבוטל אוטומטית וגם כל הקורוטינות שפועלות מבוטלות.
בעיה אחת בדוגמה הקודמת היא שכל מה שקורא ל-makeLoginRequest צריך לזכור להעביר במפורש את הביצוע מה-thread הראשי. נבדוק איך אפשר לשנות את Repository כדי לפתור את הבעיה.
שימוש בקורוטינות כדי להבטיח בטיחות בשרשור הראשי
פונקציה נחשבת בטוחה לשימוש בשרשור הראשי אם היא לא חוסמת עדכונים בממשק המשתמש בשרשור הראשי. הפונקציה makeLoginRequest לא בטוחה לשימוש ב-thread הראשי, כי קריאה ל-makeLoginRequest מה-thread הראשי חוסמת את ממשק המשתמש. אפשר להשתמש בפונקציה withContext() מהספרייה של קורוטינות כדי להעביר את ההפעלה של קורוטינה לשרשור אחר:
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) מעבירה את ההפעלה של הקורוטינה ל-thread של קלט/פלט, וכך הפונקציה שקוראת הופכת לבטוחה לשימוש ב-thread הראשי ומאפשרת לעדכן את ממשק המשתמש לפי הצורך.
התג makeLoginRequest מסומן גם במילת המפתח suspend. מילת המפתח הזו היא הדרך של Kotlin לאכוף קריאה לפונקציה מתוך קורוטינה.
בדוגמה הבאה, הקורוטינה נוצרת ב-LoginViewModel.
הפונקציה makeLoginRequest מעבירה את הביצוע מה-thread הראשי, ולכן אפשר להריץ את ה-Coroutine בפונקציה login ב-thread הראשי:
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פועלות ב-thread הראשי. - התוצאה של בקשת הרשת מטופלת עכשיו כדי להציג את ממשק המשתמש של ההצלחה או הכישלון.
פונקציית הכניסה פועלת עכשיו באופן הבא:
- האפליקציה קוראת לפונקציה
login()מהשכבהViewב-thread הראשי. -
launchיוצרת קורוטינה חדשה בשרשור הראשי, והקורוטינה מתחילה את הביצוע. - בתוך הקורוטינה, הקריאה ל-
loginRepository.makeLoginRequest()now משעה את ההרצה הנוספת של הקורוטינה עד שהבלוק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.
מקורות מידע נוספים על קורוטינות:
- סקירה כללית של קורוטינות (JetBrains)
- מדריך לקורוטינות (JetBrains)
- מקורות מידע נוספים על שגרות משנה (coroutines) ועל Flow ב-Kotlin