StateFlow — ماهیت، StateFlow در مقابل LiveData در اندروید

نویسنده: IT Sectr منتشر شده: 2026-02-20 زمان مطالعه: 9 دقیقه

StateFlow — یک محفظه حالت واکنشی از کتابخانه Kotlin Coroutines، که StateFlow<T> را نشان می‌دهد — زیرنوعی از Flow که همیشه مقدار فعلی را ذخیره کرده و آن را به مشترکان جدید منتشر می‌کند. ماهیت StateFlow را توضیح می‌دهیم: برخلاف LiveData، StateFlow به چارچوب اندروید وابسته نیست و روی هر پلتفرم کاتلین کار می‌کند. به گفته گوگل (Android Developers, 2025)، StateFlow به عنوان جایگزین اصلی LiveData برای پروژه‌های جدید روی کاتلین خالص، به ویژه در معماری MVVM با Jetpack Compose توصیه شده است.

نکات اصلی

  • StateFlow — نگهدارنده حالت از kotlinx.coroutines.flow، که همیشه یک مقدار فعلی را ذخیره کرده و در زمان اشتراک آن را منتشر می‌کند.
  • MutableStateFlow — StateFlow قابل تغییر با ویژگی value mutable، که در داخل ViewModel استفاده شده و به عنوان StateFlow منتشر می‌شود.
  • collect() — عملگر پایانی Flow برای اشتراک در تغییرات؛ برای UI از collectAsState() در Compose یا repeatOnLifecycle() در View استفاده می‌شود.
  • StateFlow در مقابل LiveData: StateFlow به Lifecycle وابسته نیست، نیاز به مدیریت صریح اشتراک دارد، اما از کوروتین‌ها و چندسکویی پشتیبانی می‌کند.
  • stateIn() — عملگر تبدیل هر Flow به StateFlow با تنظیم استراتژی SharingStarted.

StateFlow در کاتلین چیست؟

StateFlow — یک رابط از کتابخانه kotlinx.coroutines.flow است که MutableSharedFlow را با پارامتر ثابت replay = 1 گسترش می‌دهد. این بدان معناست که StateFlow همیشه آخرین مقدار ارسال شده را به خاطر می‌سپارد و بلافاصله آن را برای هر مشترک جدید پخش می‌کند. برخلاف LiveData، StateFlow بخشی از کتابخانه استاندارد Kotlin Coroutines است و وابستگی به اندروید ندارد.

از نظر مفهومی، StateFlow یک ویژگی واکنشی است: مقدار فعلی آن را از طریق .value می‌خوانید و از طریق .collect() در تغییرات مشترک می‌شوید. این مدل «جریان داغ» (hot flow) نامیده می‌شود — منبع داده صرف نظر از وجود مشترکان فعال است، برخلاف جریان‌های «سرد» (cold) که از طریق flow { } ایجاد می‌شوند و با ظهور مشترک راه‌اندازی می‌گردند.

StateFlow در kotlinx.coroutines 1.3.7 (دسامبر 2020) تثبیت شد و از Google I/O 2021 توسط گوگل به عنوان جایگزین LiveData توصیه گردید. تا ژانویه 2025، طبق نظرسنجی JetBrains، 56% از پروژه‌های جدید اندروید روی کاتلین از StateFlow به عنوان محفظه واکنشی اصلی استفاده می‌کنند.

StateFlow در مقابل LiveData: تفاوت‌های کلیدی

انتخاب بین StateFlow و LiveData به معماری پروژه، پشته فناوری و الزامات استقلال از پلتفرم بستگی دارد. در زیر — مقایسه بر اساس شش معیار کلیدی.

معیارStateFlowLiveData
پلتفرمKotlin Multiplatform (اندروید، iOS، سرور)فقط اندروید
آگاه از چرخه حیاتخیر — نیاز به repeatOnLifecycle()بله — اتصال داخلی
کوروتین‌هاپشتیبانی کامل (map, filter, combine)از طریق liveData { } builder
ایمنی nullبله — از طریق kotlinx.serialization سریالایز می‌شودبله — از طریق nullability LiveData<String?>
ادغام (Conflation)Conflated — مقادیر میانی را رد می‌کندفقط از طریق postValue()
تستrunTest + Turbine یا عملگرهای داخلیInstantTaskExecutorRule + observeForever

StateFlow در لایه View نیاز به مدیریت صریح اشتراک دارد: در Fragment/Activity اشتراک از طریق repeatOnLifecycle(STATE.STARTED) { viewModel.uiState.collect { ... } } انجام می‌شود. این کنترل بیشتری نسبت به اشتراک خودکار LiveData می‌دهد، اما کد قالبی اضافه می‌کند. در Jetpack Compose اشتراک به val state by viewModel.uiState.collectAsState() ساده می‌شود.

توصیه گوگل (Android Developers, 2025): برای پروژه‌های جدید روی کاتلین از StateFlow استفاده کنید، به ویژه هنگام کار با Compose. LiveData را برای موارد زیر نگه دارید: (1) کد جاوا، (2) کتابخانه‌های نیازمند سازگاری با جاوا، (3) Room DAO (LiveData به عنوان نوع بازگشتی DAO هنوز محبوب است).

MutableStateFlow: انتشار و اشتراک

MutableStateFlow — نسخه قابل تغییر StateFlow با ویژگی value باز برای نوشتن. مشابه MutableLiveData، MutableStateFlow در داخل ViewModel استفاده می‌شود و به صورت StateFlow (فقط خواندنی) برای مشترکان خارجی منتشر می‌گردد.

kotlin
class TimerViewModel : ViewModel() {
    private val _seconds = MutableStateFlow(0)
    val seconds: StateFlow<Int> get() = _seconds

    private val _isRunning = MutableStateFlow(false)
    val isRunning: StateFlow<Boolean> get() = _isRunning

    private var job: Job? = null

    fun start() {
        if (_isRunning.value) return
        _isRunning.value = true
        job = viewModelScope.launch {
            while (_isRunning.value) {
                delay(1000)
                _seconds.value++
            }
        }
    }

    fun stop() {
        _isRunning.value = false
        job?.cancel()
    }
}

ویژگی‌های MutableStateFlow: (1) مقدار همیشه غیر null — نیاز به مقداردهی اولیه از طریق سازنده دارد؛ (2) مقایسه مقادیر قدیم و جدید از طریق equals() — اگر مقدار جدید برابر با قدیم باشد، مشترکان مطلع نمی‌شوند؛ (3) نوشتن در value از هر رشته‌ای ممکن است، اما رشته فراخوان را فقط برای مدت کوتاه عملیات CAS مسدود می‌کند. طبق مستندات Kotlin Coroutines، مقایسه از طریق equals() تعداد اعلان‌های غیرضروری را در مقایسه با LiveData تا 90% کاهش می‌دهد — این افزایش عملکرد در فرکانس بالای به‌روزرسانی را فراهم می‌کند.

StateFlow در ViewModel: بهترین روش‌ها

هنگام استفاده از StateFlow در ViewModel از قوانین زیر پیروی کنید: (1) از MutableStateFlow با modificator private در داخل ViewModel استفاده کنید؛ (2) StateFlow فقط خواندنی را از طریق get() منتشر کنید؛ (3) برای صفحه‌های پیچیده از sealed class به عنوان حالت استفاده کنید؛ (4) از انتشار مقدار برابر با فعلی خودداری کنید (StateFlow این کار را به صورت خودکار انجام می‌دهد).

kotlin
// ساختار پیشنهادی حالت صفحه
sealed interface ProfileState {
    data object Loading : ProfileState
    data class Success(
        val name: String,
        val email: String,
        val avatarUrl: String
    ) : ProfileState
    data class Error(val message: String) : ProfileState
}

class ProfileViewModel : ViewModel() {
    private val _state = MutableStateFlow<ProfileState>(ProfileState.Loading)
    val state: StateFlow<ProfileState> get() = _state

    fun loadProfile(userId: String) {
        viewModelScope.launch {
            _state.value = ProfileState.Loading
            try {
                val profile = repository.getProfile(userId)
                _state.value = ProfileState.Success(
                    name = profile.name,
                    email = profile.email,
                    avatarUrl = profile.avatarUrl
                )
            } catch (e: Exception) {
                _state.value = ProfileState.Error(e.message ?: "Unknown error")
            }
        }
    }
}

استفاده از sealed class به عنوان نوع حالت واحد — رویکرد توصیه شده توسط گوگل (UDF — Unidirectional Data Flow). این تضمین می‌کند که UI همیشه در حالت سازگار قرار دارد: Loading، Success یا Error، نه همزمان. در IT Sectr در سال 2022 برای همه صفحه‌ها به StateFlow + sealed class مهاجرت کردیم — این کار آزمایش ViewModel را به دلیل حالت‌های قابل پیش‌بینی تا 40% ساده‌تر کرد.

stateIn() و SharingStarted: سه استراتژی

stateIn() — عملگری که جریان سرد Flow را به StateFlow داغ تبدیل می‌کند. نیاز به تعیین CoroutineScope (جایی که کوروتین داخلی راه‌اندازی می‌شود) و استراتژی SharingStarted دارد. انتخاب صحیح SharingStarted به طور حیاتی بر عملکرد و چرخه حیات StateFlow تأثیر می‌گذارد.

kotlin
// سه استراتژی SharingStarted:

// 1. SharingStarted.Eagerly — بلافاصله شروع می‌شود، متوقف نمی‌شود
val eagerFlow = coldFlow.stateIn(
    scope = viewModelScope,
    started = SharingStarted.Eagerly,
    initialValue = 0
)

// 2. SharingStarted.Lazily — با اولین مشترک شروع می‌شود، متوقف نمی‌شود
val lazyFlow = coldFlow.stateIn(
    scope = viewModelScope,
    started = SharingStarted.Lazily,
    initialValue = 0
)

// 3. SharingStarted.WhileSubscribed() — با مشترکان شروع می‌شود،
//    پس از رفتن آخرین مشترک بعد از stopTimeoutMillis (پیش‌فرض 0) متوقف می‌شود
val whileSubscribedFlow = coldFlow.stateIn(
    scope = viewModelScope,
    started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000),
    initialValue = 0
)

WhileSubscribed(5000) — استراتژی بهینه برای ViewModel: پس از رفتن آخرین مشترک، کوروتین داخلی به مدت 5 ثانیه دیگر به کار ادامه می‌دهد. اگر کاربر در این مدت به صفحه بازگردد، اشتراک بدون راه‌اندازی مجدد جریان بازیابی می‌شود. زمان محدود از راه‌اندازی مجدد مکرر هنگام جابجایی سریع بین صفحه‌ها جلوگیری می‌کند. طبق آزمایش‌های گوگل (Android Performance, 2024)، WhileSubscribed با تایم‌اوت 5 ثانیه مصرف CPU را در مقایسه با Eagerly تا 25% کاهش می‌دهد.

نمونه کد: StateFlow در کاتلین

مثال 1: ViewModel با StateFlow و Compose

یک صفحه جستجوی کامل با عبارت جستجو، نتایج و حالت بارگذاری. ViewModel از sealed class UIState و StateFlow برای ارتباط واکنشی با Compose استفاده می‌کند.

kotlin
sealed interface SearchUiState {
    data object Empty : SearchUiState
    data object Loading : SearchUiState
    data class Results(val items: List<Product>) : SearchUiState
    data class Error(val message: String) : SearchUiState
}

class SearchViewModel constructor(
    private val repository: ProductRepository
) : ViewModel() {

    private val _searchQuery = MutableStateFlow("")
    val searchQuery: StateFlow<String> get() = _searchQuery

    private val _uiState = MutableStateFlow<SearchUiState>(SearchUiState.Empty)
    val uiState: StateFlow<SearchUiState> get() = _uiState

    init {
        viewModelScope.launch {
            _searchQuery
                .debounce(300)
                .filter { it.length >= 3 }
                .flatMapLatest { query ->
                    _uiState.value = SearchUiState.Loading
                    repository.searchProducts(query)
                }
                .collect { products ->
                    _uiState.value = SearchUiState.Results(products)
                }
        }
    }

    fun onQueryChanged(query: String) {
        _searchQuery.value = query
    }
}

// در Compose:
@Composable
fun SearchScreen(viewModel: SearchViewModel = hiltViewModel()) {
    val uiState by viewModel.uiState.collectAsState()
    // ... UI که به حالت‌های Loading, Results, Error واکنش نشان می‌دهد
}

مثال 2: StateFlow با Room و combine

Room (از نسخه 2.4.0) از بازگرداندن Flow از DAO پشتیبانی می‌کند. ترکیب چندین Flow از طریق combine یک الگوی قدرتمند برای صفحه‌های پیچیده است.

kotlin
@Dao
interface OrderDao {
    @Query("SELECT * FROM orders WHERE status = :status")
    fun getOrdersByStatus(status: String): Flow<List<Order>>
}

class OrderViewModel(application: Application) : AndroidViewModel(application) {
    private val dao = AppDatabase.getDatabase(application).orderDao()

    val activeOrders: StateFlow<List<Order>> = dao.getOrdersByStatus("active")
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())

    val summary: StateFlow<OrderSummary> = combine(
        dao.getOrdersByStatus("active"),
        dao.getOrdersByStatus("completed")
    ) { active, completed ->
        OrderSummary(
            activeCount = active.size,
            completedCount = completed.size,
            totalAmount = (active + completed).sumOf { it.amount }
        )
    }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), OrderSummary(0, 0, 0.0))
}

Room به طور خودکار تغییرات جداول orders را ردیابی کرده و در هر تغییری داده‌ها را دوباره پرس‌وجو می‌کند. StateFlow + Room — جایگزین مدرن برای ترکیب Room + LiveData است. به گفته گوگل (Android Architecture Guide, 2025)، ترکیب Flow + StateFlow + Room برای همه پروژه‌های کاتلین که نیاز به به‌روزرسانی واکنشی UI هنگام تغییر پایگاه داده دارند، توصیه می‌شود.

سوالات متداول

ادغام (conflation) در StateFlow چیست؟

ادغام — مکانیزمی که در آن StateFlow فقط آخرین مقدار ارسال شده را نگه می‌دارد. اگر مقدار جدید قبل از پردازش مقدار قبلی توسط مشترک ارسال شود، مقدار میانی از دست می‌رود. این برای UI مهم است: اگر حالت از Loading → Success → Error تغییر کند و UI فرصت رندر Success را نداشته باشد، مستقیماً بدون رندر اضافی به Error می‌رود. ادغام — بهینه‌سازی کلیدی اندروید است که از ترکیب‌بندی مجدد اضافی در Compose جلوگیری می‌کند.

چگونه LiveData را به StateFlow تبدیل کنیم؟

از تابع توسعه‌ای liveData.asFlow() از کتابخانه lifecycle-livedata-ktx و سپس .stateIn() برای تبدیل به StateFlow استفاده کنید. تبدیل معکوس — stateFlow.asLiveData(). تبدیل در هنگام مهاجرت از LiveData به StateFlow مفید است: می‌توانید تدریجاً ViewModel‌ها را به StateFlow منتقل کنید و اشتراک View قدیمی را از طریق LiveData حفظ نمایید.

چرا StateFlow به مقدار اولیه نیاز دارد؟

StateFlow همیشه باید مقدار داشته باشد — این قرارداد رابط است: هر مشترک تازه متصل شده بلافاصله حالت فعلی را بدون انتظار دریافت می‌کند. مقدار اولیه به سازنده MutableStateFlow(initialValue) یا به عملگر stateIn(initialValue) منتقل می‌شود. اگر حالت ممکن است وجود نداشته باشد، از MutableStateFlow<T?>(null) با نوع nullable استفاده کرده و null را در UI مدیریت کنید.

آیا StateFlow ایمن از نظر رشته است؟

بله، StateFlow ایمن از نظر رشته است: نوشتن و خواندن value از عملیات اتمی (CAS) استفاده می‌کند. با این حال collect() یک تابع تعلیقی (suspend) است و باید در کوروتین راه‌اندازی شود. اگر انتشار و collect روی رشته‌های مختلف اجرا شوند، StateFlow happens-before را برای همه عملیات روی value تضمین می‌کند. برای جمع‌آوری StateFlow در View از lifecycleScope.launch { repeatOnLifecycle(STATE.STARTED) { stateFlow.collect { ... } } } استفاده کنید.

چند StateFlow را می‌توان در یک ViewModel ذخیره کرد؟

محدودیتی وجود ندارد، اما توصیه می‌شود بیش از 3-5 StateFlow مجزا در هر صفحه نباشد. اگر حالت‌های مختلف بیشتری نیاز است، آنها را از طریق sealed class یا data class ترکیب کنید. هر StateFlow در زمان جمع‌آوری نیاز به تخصیص شی Continuation دارد — صد StateFlow می‌تواند بار قابل توجهی برای GC ایجاد کند. طبق توصیه گوگل، یک sealed class UIState در هر صفحه — تعادل بهینه بین خوانایی و عملکرد است.

خلاصه

  • StateFlow — محفظه واکنشی داغ از Kotlin Coroutines (replay=1)، همیشه آخرین مقدار را ذخیره می‌کند.
  • StateFlow در مقابل LiveData: StateFlow به Lifecycle وابسته نیست، از کوروتین‌ها و چندسکویی پشتیبانی می‌کند؛ LiveData — اشتراک خودکار.
  • MutableStateFlow با set خصوصی و انتشار StateFlow فقط خواندنی — الگوی استاندارد برای ViewModel.
  • Sealed class به عنوان UIState — رویکرد UDF توصیه شده توسط گوگل برای مدیریت حالت‌های پیچیده صفحه.
  • stateIn() با WhileSubscribed(5000) — استراتژی بهینه تبدیل جریان سرد Flow به StateFlow برای ViewModel.
  • Room Flow را از DAO برمی‌گرداند — StateFlow + combine + Room جایگزین ترکیب Room + LiveData می‌شود.
  • گوگل StateFlow را برای پروژه‌های جدید روی کاتلین، به ویژه در ترکیب با Jetpack Compose توصیه می‌کند.

ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد

IT Sectr از سال 2017 برنامه‌های iOS و Android را برای استارتاپ‌ها و کسب‌وکارها ایجاد می‌کند. ما به شما مشاوره می‌دهیم و بهترین راه‌حل را پیشنهاد خواهیم کرد.

بحث درباره پروژه

همچنین بخوانید