מושגים ויישום ב-Jetpack פיתוח נייטיב
שגרות ההמשך של Kotlin מספקות API שמאפשר לכם לכתוב קוד אסינכרוני. בעזרת Kotlin coroutines, אפשר להגדיר CoroutineScope, וכך לנהל את המועדים שבהם ה-coroutines צריכים לפעול. כל פעולה אסינכרונית פועלת בהיקף מסוים.
רכיבים שמודעים למחזור החיים מספקים תמיכה ברמה גבוהה בקורוטינות עבור היקפים לוגיים באפליקציה, יחד עם שכבת יכולת פעולה הדדית עם LiveData. במאמר הזה מוסבר איך להשתמש ביעילות ב-שגרות המשך (coroutines) עם רכיבים שמודעים למחזור החיים.
הוספת יחסי תלות של KTX
היקפי ה-Coroutine המובנים שמתוארים בנושא הזה כלולים בתוספים של KTX לכל רכיב תואם. כשמשתמשים בהיקפים האלה, חשוב להוסיף את התלויות המתאימות.
- ל-
ViewModelScope, משתמשים ב-androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0ומעלה. - ל-
LifecycleScope, משתמשים ב-androidx.lifecycle:lifecycle-runtime-ktx:2.4.0ומעלה. - ל-
liveData, משתמשים ב-androidx.lifecycle:lifecycle-livedata-ktx:2.4.0ומעלה.
היקפי coroutine עם מודעות למחזור החיים
רכיבים שמודעים למחזור החיים מגדירים את ההיקפים המובנים הבאים שאפשר להשתמש בהם באפליקציה.
ViewModelScope
ה-ViewModelScope מוגדר לכל ViewModel באפליקציה. כל שגרת המשך (coroutine) שמופעלת בהיקף הזה מבוטלת באופן אוטומטי אם ה-ViewModel מנוקה. קורוטינות שימושיות כאן כשצריך לבצע עבודה רק אם ViewModel פעיל. לדוגמה, אם אתם מחשבים נתונים מסוימים עבור פריסה, כדאי להגדיר את היקף העבודה ל-ViewModel, כך שאם ViewModel ינוקה, העבודה תבוטל אוטומטית כדי למנוע צריכת משאבים.
אפשר לגשת אל CoroutineScope של ViewModel דרך המאפיין viewModelScope של ViewModel, כמו בדוגמה הבאה:
class MyViewModel: ViewModel() {
init {
viewModelScope.launch {
// Coroutine that will be canceled when the ViewModel is cleared.
}
}
}
LifecycleScope
מוגדר LifecycleScope לכל אובייקט Lifecycle. כל קורוטינה
שמופעלת בהיקף הזה מבוטלת כשהאובייקט Lifecycle מושמד. אפשר לגשת ל-CoroutineScope של Lifecycle דרך נכסי lifecycle.coroutineScope או lifecycleOwner.lifecycleScope.
בדוגמה הבאה אפשר לראות איך משתמשים ב-lifecycleOwner.lifecycleScope כדי ליצור טקסט מחושב מראש באופן אסינכרוני:
class MyFragment: Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
val params = TextViewCompat.getTextMetricsParams(textView)
val precomputedText = withContext(Dispatchers.Default) {
PrecomputedTextCompat.create(longTextContent, params)
}
TextViewCompat.setPrecomputedText(textView, precomputedText)
}
}
}
קורוטינות עם מודעות למחזור החיים שאפשר להפעיל מחדש
למרות ש-lifecycleScope מספק דרך מתאימה לבטל באופן אוטומטי פעולות ארוכות טווח כש-Lifecycle הוא DESTROYED, יכול להיות שיהיו לכם מקרים אחרים שבהם תרצו להתחיל את ההרצה של בלוק קוד כש-Lifecycle נמצא במצב מסוים, ולבטל אותה כשהוא נמצא במצב אחר. לדוגמה, יכול להיות שתרצו לאסוף נתונים של זרימה כשהערך של Lifecycle הוא STARTED ולבטל את האיסוף כשהערך הוא STOPPED. בגישה הזו, העיבוד של פליטות הזרימה מתבצע רק כשהממשק גלוי על המסך, וכך נחסכים משאבים וגם נמנעות קריסות של האפליקציה.
במקרים כאלה, ממשקי ה-API Lifecycle ו-LifecycleOwner מספקים את ה-API repeatOnLifecycle suspend, שמבצע בדיוק את הפעולה הזו. בדוגמה הבאה יש בלוק קוד שמופעל בכל פעם שהערך של Lifecycle המשויך הוא לפחות STARTED, ומבוטל כשהערך של Lifecycle הוא STOPPED:
class MyFragment : Fragment() {
val viewModel: MyViewModel by viewModel()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Create a new coroutine in the lifecycleScope
viewLifecycleOwner.lifecycleScope.launch {
// repeatOnLifecycle launches the block in a new coroutine every time the
// lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED.
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
// Trigger the flow and start listening for values.
// This happens when lifecycle is STARTED and stops
// collecting when the lifecycle is STOPPED
viewModel.someDataFlow.collect {
// Process item
}
}
}
}
}
איסוף נתונים בתהליך שמתחשב במחזור החיים
אם אתם צריכים לבצע איסוף עם מודעות למחזור החיים רק בתהליך אחד, אתם יכולים להשתמש בשיטה Flow.flowWithLifecycle() כדי לפשט את הקוד:
viewLifecycleOwner.lifecycleScope.launch {
exampleProvider.exampleFlow()
.flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
.collect {
// Process the value.
}
}
עם זאת, אם אתם צריכים לבצע איסוף שמודע למחזור החיים בכמה זרמים במקביל, אתם צריכים לאסוף כל זרם ברוטינה משותפת אחרת. במקרה כזה, יעיל יותר להשתמש ישירות בפונקציה repeatOnLifecycle():
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
// Because collect is a suspend function, if you want to
// collect multiple flows in parallel, you need to do so in
// different coroutines.
launch {
flow1.collect { /* Process the value. */ }
}
launch {
flow2.collect { /* Process the value. */ }
}
}
}
השעיית קורוטינות עם מודעות למחזור החיים
למרות שהשיטה CoroutineScope מספקת דרך נכונה לבטל באופן אוטומטי פעולות שפועלות במשך זמן רב, יכול להיות שיהיו מקרים אחרים שבהם תרצו להשהות את הביצוע של בלוק קוד, אלא אם Lifecycle נמצא במצב מסוים. לדוגמה, כדי להפעיל FragmentTransaction, צריך להמתין עד שLifecycle יהיה לפחות STARTED. במקרים כאלה, Lifecycle מספק שיטות נוספות:
lifecycle.whenCreated, lifecycle.whenStarted ו-lifecycle.whenResumed. כל שגרת המשך שמופעלת בתוך הבלוקים האלה מושהית אם Lifecycle לא נמצא לפחות במצב המינימלי הרצוי.
בדוגמה הבאה יש בלוק קוד שמופעל רק כשהרכיב המשויך Lifecycle נמצא לפחות במצב STARTED:
class MyFragment: Fragment {
init { // Notice that we can safely launch in the constructor of the Fragment.
lifecycleScope.launch {
whenStarted {
// The block inside will run only when Lifecycle is at least STARTED.
// It will start executing when fragment is started and
// can call other suspend methods.
loadingView.visibility = View.VISIBLE
val canAccess = withContext(Dispatchers.IO) {
checkUserAccess()
}
// When checkUserAccess returns, the next line is automatically
// suspended if the Lifecycle is not *at least* STARTED.
// We could safely run fragment transactions because we know the
// code won't run unless the lifecycle is at least STARTED.
loadingView.visibility = View.GONE
if (canAccess == false) {
findNavController().popBackStack()
} else {
showContent()
}
}
// This line runs only after the whenStarted block above has completed.
}
}
}
אם Lifecycle מושמד בזמן שקורוטינה פעילה באמצעות אחת מהשיטות של when, הקורוטינה מבוטלת באופן אוטומטי. בדוגמה שלמטה, הבלוק finally מופעל ברגע שהמצב Lifecycle הוא DESTROYED:
class MyFragment: Fragment {
init {
lifecycleScope.launchWhenStarted {
try {
// Call some suspend functions.
} finally {
// This line might execute after Lifecycle is DESTROYED.
if (lifecycle.state >= STARTED) {
// Here, since we've checked, it is safe to run any
// Fragment transactions.
}
}
}
}
}
שימוש בשגרות משנה עם LiveData
כשמשתמשים בפונקציה LiveData, יכול להיות שיהיה צורך לחשב ערכים באופן אסינכרוני.
לדוגמה, יכול להיות שתרצו לאחזר את ההעדפות של משתמש ולהציג אותן בממשק המשתמש שלכם. במקרים כאלה, אפשר להשתמש בפונקציית ה-builder liveData כדי לקרוא לפונקציה suspend ולהציג את התוצאה כאובייקט LiveData.
בדוגמה שלמטה, loadUser() היא פונקציית השהיה שהוגדרה במקום אחר. משתמשים בפונקציית ה-builder liveData כדי להפעיל את loadUser() באופן אסינכרוני, ואז משתמשים ב-emit() כדי להציג את התוצאה:
val user: LiveData<User> = liveData {
val data = database.loadUser() // loadUser is a suspend function.
emit(data)
}
אבן הבניין liveData משמשת כפרימיטיב של מקביליות מובנית בין שגרות המשנה לבין LiveData. בלוק הקוד מתחיל לפעול כש-LiveData הופך לפעיל, ומבוטל באופן אוטומטי אחרי זמן קצוב לתפוגה שניתן להגדרה, כש-LiveData הופך ללא פעיל. אם הוא מבוטל לפני שהסתיים, הוא מופעל מחדש אם LiveData חוזר להיות פעיל. אם הפעולה הושלמה בהצלחה בהרצה קודמת, היא לא תופעל מחדש. הערה:
ההפעלה מחדש מתבצעת רק אם המינוי בוטל אוטומטית. אם החסימה מבוטלת מסיבה אחרת (למשל, אם מוצג CancellationException), היא לא מופעלת מחדש.
אפשר גם להפיק כמה ערכים מהבלוק. כל קריאה ל-emit() משעה את הביצוע של הבלוק עד שהערך של LiveData מוגדר בשרשור הראשי.
val user: LiveData<Result> = liveData {
emit(Result.loading())
try {
emit(Result.success(fetchUser()))
} catch(ioException: Exception) {
emit(Result.error(ioException))
}
}
אפשר גם לשלב liveData עם Transformations, כמו בדוגמה הבאה:
class MyViewModel: ViewModel() {
private val userId: LiveData<String> = MutableLiveData()
val user = userId.switchMap { id ->
liveData(context = viewModelScope.coroutineContext + Dispatchers.IO) {
emit(database.loadUserById(id))
}
}
}
אפשר להשתמש בפונקציה LiveData כדי להחזיר כמה ערכים על ידי קריאה לפונקציה emitSource()
בכל פעם שרוצים להחזיר ערך חדש. שימו לב: כל קריאה ל-emit()
או ל-emitSource() מסירה את המקור שנוסף קודם.
class UserDao: Dao {
@Query("SELECT * FROM User WHERE id = :id")
fun getUser(id: String): LiveData<User>
}
class MyRepository {
fun getUser(id: String) = liveData<User> {
val disposable = emitSource(
userDao.getUser(id).map {
Result.loading(it)
}
)
try {
val user = webservice.fetchUser(id)
// Stop the previous emission to avoid dispatching the updated user
// as `loading`.
disposable.dispose()
// Update the database.
userDao.insert(user)
// Re-establish the emission with success type.
emitSource(
userDao.getUser(id).map {
Result.success(it)
}
)
} catch(exception: IOException) {
// Any call to `emit` disposes the previous one automatically so we don't
// need to dispose it here as we didn't get an updated value.
emitSource(
userDao.getUser(id).map {
Result.error(exception, it)
}
)
}
}
}
מידע נוסף על קורוטינות זמין בקישורים הבאים:
- שיפור ביצועי האפליקציה באמצעות שגרות המשך (coroutines) ב-Kotlin
- סקירה כללית בנושא קורוטינות
- Threading in CoroutineWorker
מקורות מידע נוספים
למידע נוסף על שימוש בקורוטינות עם רכיבים שמודעים למחזור החיים, אפשר לעיין במקורות המידע הנוספים הבאים.
דוגמאות
בלוגים
- שגרות משנה (coroutines) ב-Android: דפוסי שימוש באפליקציות
- Easy coroutines in Android: viewModelScope
- בדיקת שתי פליטות רצופות של LiveData בשגרות משנה