קורוטין היא תבנית עיצוב בו-זמניות שאפשר להשתמש בה. 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
של האפליקציה:
Groovy
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
מוגדר מראש שנכלל עם תוספי ה-KTXViewModel
. שימו לב שכל ה-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.
למשאבים נוספים בנושא קורוטינים, ראו את הקישורים הבאים:
- סקירה כללית של Cooroutines (JetBrains)
- מדריך Coroutines (JetBrains)
- מקורות מידע נוספים בנושא קורוטין וזרימה ב-Kotlin