StateFlow ו-SharedFlow

StateFlow ו-SharedFlow הם Flow APIs שמאפשרת לתהליכי פליטה אופטימלית של עדכוני מצב ופליטת ערכים צרכנים.

StateFlow

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

ב-Android, האפליקציה StateFlow מתאימה מאוד לכיתות שצריך לתחזק מצב ניתן למדידה.

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

class LatestNewsViewModel(
    private val newsRepository: NewsRepository
) : ViewModel() {

    // Backing property to avoid state updates from other classes
    private val _uiState = MutableStateFlow(LatestNewsUiState.Success(emptyList()))
    // The UI collects from this StateFlow to get its state updates
    val uiState: StateFlow<LatestNewsUiState> = _uiState

    init {
        viewModelScope.launch {
            newsRepository.favoriteLatestNews
                // Update View with the latest favorite news
                // Writes to the value property of MutableStateFlow,
                // adding a new element to the flow and updating all
                // of its collectors
                .collect { favoriteNews ->
                    _uiState.value = LatestNewsUiState.Success(favoriteNews)
                }
        }
    }
}

// Represents different states for the LatestNews screen
sealed class LatestNewsUiState {
    data class Success(val news: List<ArticleHeadline>): LatestNewsUiState()
    data class Error(val exception: Throwable): LatestNewsUiState()
}

הכיתה שאחראית לעדכון MutableStateFlow היא המפיקה, וכל המחלקות שנאספו מה-StateFlow הם הצרכנים. ביטול הלייק תהליך קר שנוצר באמצעות ה-builder של flow, StateFlow הוא חם: האיסוף מהתהליך לא מפעיל קוד של היצרן. StateFlow תמיד פעיל ונמצא בזיכרון, והוא הופך למתאים לאשפה רק כשאין התייחסות אחרת לאשפה הרמה הבסיסית (root) של האוסף.

כשצרכן חדש מתחיל לאסוף מהתהליך, הוא מקבל את ועל המצבים הבאים: אפשר לראות את ההתנהגות הזו בסיווגים אחרים שניתנים למדידה, LiveData

ה-View מקשיב ל-StateFlow כמו בכל זרם אחר:

class LatestNewsActivity : AppCompatActivity() {
    private val latestNewsViewModel = // getViewModel()

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        // Start a coroutine in the lifecycle scope
        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.
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                // Trigger the flow and start listening for values.
                // Note that this happens when lifecycle is STARTED and stops
                // collecting when the lifecycle is STOPPED
                latestNewsViewModel.uiState.collect { uiState ->
                    // New value received
                    when (uiState) {
                        is LatestNewsUiState.Success -> showFavoriteNews(uiState.news)
                        is LatestNewsUiState.Error -> showError(uiState.exception)
                    }
                }
            }
        }
    }
}

כדי להמיר כל תהליך שהוא ל-StateFlow, משתמשים stateIn מפעיל ביניים.

StateFlow, Flow ו-LiveData

ל-StateFlow ול-LiveData יש קווי דמיון. שניהם סוגים של בעלי נתונים שגלויים לכולם, ושניהם עוקבים דפוס דומה כשמשתמשים בו בארכיטקטורת האפליקציות.

עם זאת, חשוב לשים לב שStateFlow וגם LiveData מתנהגות באופן שונה:

  • StateFlow מחייב העברה של מצב ראשוני ל-constructor, ואילו LiveData לא.
  • LiveData.observe() מבטל את רישום הצרכן באופן אוטומטי כאשר מגיע למצב STOPPED, ואילו איסוף מ-StateFlow או כל תהליך אחר לא מפסיק לאסוף באופן אוטומטי. כדי להשיג צריך לאסוף את הזרימה מ-Lifecycle.repeatOnLifecycle חסימה.

הגברת זרימת הקור באמצעות shareIn

StateFlow הוא זרם חם – הוא נשאר בזיכרון כל עוד איסוף אשפה או כל אזכור אחר שלו מתוך אוסף אשפה בסיס. אפשר להפוך זרם קר לחום באמצעות shareIn .

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

  • CoroutineScope שמשמש לשיתוף התהליך. ההיקף הזה אמור להיות פעיל יותר מכל לקוח אחר, כדי שהתהליך המשותף ימשיך לפעול כל עוד הוא נדרש.
  • מספר הפריטים שיש להפעיל מחדש לכל אספן חדש.
  • המדיניות של התנהגות ההתחלה.
class NewsRemoteDataSource(...,
    private val externalScope: CoroutineScope,
) {
    val latestNews: Flow<List<ArticleHeadline>> = flow {
        ...
    }.shareIn(
        externalScope,
        replay = 1,
        started = SharingStarted.WhileSubscribed()
    )
}

בדוגמה הזו, התהליך של latestNews מפעיל מחדש את הפריט האחרון שהופק אספן חדש ויישאר פעיל כל עוד externalScope ויש אספנים פעילים. SharingStarted.WhileSubscribed() מדיניות ההתחלה שומרת על מפיק ה-upstream פעיל כל עוד מנויים. יש עוד כללי מדיניות להתחלה, כמו SharingStarted.Eagerly כדי להפעיל את המפיק באופן מיידי, או SharingStarted.Lazily כדי להתחיל בשיתוף אחרי שהמנוי הראשון מופיע ולהשאיר את הזרימה פעילה לנצח.

SharedFlow

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

אפשר ליצור SharedFlow בלי להשתמש ב-shareIn. לדוגמה, אפשר יכול להשתמש ב-SharedFlow כדי לשלוח סימונים לשאר האפליקציה, כל התוכן מתרענן מדי פעם באותו זמן. חוץ מ- לאחזר את החדשות האחרונות, כדאי גם לרענן את המשתמש קטע מידע עם אוסף הנושאים המועדפים עליו. בתוך קטע קוד, TickHandler חושף SharedFlow כך יודעים מתי לרענן את התוכן שלהם. בדומה ל-StateFlow, יש להשתמש מאפיין גיבוי מסוג MutableSharedFlow בכיתה כדי לשלוח פריטים לזרום:

// Class that centralizes when the content of the app needs to be refreshed
class TickHandler(
    private val externalScope: CoroutineScope,
    private val tickIntervalMs: Long = 5000
) {
    // Backing property to avoid flow emissions from other classes
    private val _tickFlow = MutableSharedFlow<Unit>(replay = 0)
    val tickFlow: SharedFlow<Event<String>> = _tickFlow

    init {
        externalScope.launch {
            while(true) {
                _tickFlow.emit(Unit)
                delay(tickIntervalMs)
            }
        }
    }
}

class NewsRepository(
    ...,
    private val tickHandler: TickHandler,
    private val externalScope: CoroutineScope
) {
    init {
        externalScope.launch {
            // Listen for tick updates
            tickHandler.tickFlow.collect {
                refreshLatestNews()
            }
        }
    }

    suspend fun refreshLatestNews() { ... }
    ...
}

אפשר להתאים אישית את ההתנהגות של SharedFlow בדרכים הבאות:

  • replay מאפשר לשלוח מחדש מספר ערכים שהונפקו בעבר עבור מנויים.
  • onBufferOverflow מאפשרת להגדיר מדיניות למקרים של מאגר הנתונים הזמני מלא בפריטים לשליחה. ערך ברירת המחדל הוא BufferOverflow.SUSPEND, ולכן הוא מושעה. האפשרויות האחרות הן DROP_LATEST או DROP_OLDEST.

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

מקורות מידע נוספים בנושא תהליכי