StateFlow — یک محفظه حالت واکنشی از کتابخانه Kotlin Coroutines، که StateFlow<T> را نشان میدهد — زیرنوعی از Flow که همیشه مقدار فعلی را ذخیره کرده و آن را به مشترکان جدید منتشر میکند. ماهیت StateFlow را توضیح میدهیم: برخلاف LiveData، StateFlow به چارچوب اندروید وابسته نیست و روی هر پلتفرم کاتلین کار میکند. به گفته گوگل (Android Developers, 2025)، StateFlow به عنوان جایگزین اصلی LiveData برای پروژههای جدید روی کاتلین خالص، به ویژه در معماری MVVM با Jetpack Compose توصیه شده است.
نکات اصلی
collectAsState() در Compose یا repeatOnLifecycle() در View استفاده میشود.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 |
|---|---|---|
| پلتفرم | 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 — نسخه قابل تغییر StateFlow با ویژگی value باز برای نوشتن. مشابه MutableLiveData، MutableStateFlow در داخل ViewModel استفاده میشود و به صورت StateFlow (فقط خواندنی) برای مشترکان خارجی منتشر میگردد.
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 از قوانین زیر پیروی کنید: (1) از MutableStateFlow با modificator private در داخل ViewModel استفاده کنید؛ (2) StateFlow فقط خواندنی را از طریق get() منتشر کنید؛ (3) برای صفحههای پیچیده از sealed class به عنوان حالت استفاده کنید؛ (4) از انتشار مقدار برابر با فعلی خودداری کنید (StateFlow این کار را به صورت خودکار انجام میدهد).
// ساختار پیشنهادی حالت صفحه
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() — عملگری که جریان سرد Flow را به StateFlow داغ تبدیل میکند. نیاز به تعیین CoroutineScope (جایی که کوروتین داخلی راهاندازی میشود) و استراتژی SharingStarted دارد. انتخاب صحیح SharingStarted به طور حیاتی بر عملکرد و چرخه حیات StateFlow تأثیر میگذارد.
// سه استراتژی 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% کاهش میدهد.
یک صفحه جستجوی کامل با عبارت جستجو، نتایج و حالت بارگذاری. ViewModel از sealed class UIState و StateFlow برای ارتباط واکنشی با Compose استفاده میکند.
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 واکنش نشان میدهد
}
Room (از نسخه 2.4.0) از بازگرداندن Flow از DAO پشتیبانی میکند. ترکیب چندین Flow از طریق combine یک الگوی قدرتمند برای صفحههای پیچیده است.
@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 هنگام تغییر پایگاه داده دارند، توصیه میشود.
سوالات متداول
ادغام — مکانیزمی که در آن StateFlow فقط آخرین مقدار ارسال شده را نگه میدارد. اگر مقدار جدید قبل از پردازش مقدار قبلی توسط مشترک ارسال شود، مقدار میانی از دست میرود. این برای UI مهم است: اگر حالت از Loading → Success → Error تغییر کند و UI فرصت رندر Success را نداشته باشد، مستقیماً بدون رندر اضافی به Error میرود. ادغام — بهینهسازی کلیدی اندروید است که از ترکیببندی مجدد اضافی در Compose جلوگیری میکند.
از تابع توسعهای liveData.asFlow() از کتابخانه lifecycle-livedata-ktx و سپس .stateIn() برای تبدیل به StateFlow استفاده کنید. تبدیل معکوس — stateFlow.asLiveData(). تبدیل در هنگام مهاجرت از LiveData به StateFlow مفید است: میتوانید تدریجاً ViewModelها را به StateFlow منتقل کنید و اشتراک View قدیمی را از طریق LiveData حفظ نمایید.
StateFlow همیشه باید مقدار داشته باشد — این قرارداد رابط است: هر مشترک تازه متصل شده بلافاصله حالت فعلی را بدون انتظار دریافت میکند. مقدار اولیه به سازنده MutableStateFlow(initialValue) یا به عملگر stateIn(initialValue) منتقل میشود. اگر حالت ممکن است وجود نداشته باشد، از MutableStateFlow<T?>(null) با نوع nullable استفاده کرده و null را در UI مدیریت کنید.
بله، StateFlow ایمن از نظر رشته است: نوشتن و خواندن value از عملیات اتمی (CAS) استفاده میکند. با این حال collect() یک تابع تعلیقی (suspend) است و باید در کوروتین راهاندازی شود. اگر انتشار و collect روی رشتههای مختلف اجرا شوند، StateFlow happens-before را برای همه عملیات روی value تضمین میکند. برای جمعآوری StateFlow در View از lifecycleScope.launch { repeatOnLifecycle(STATE.STARTED) { stateFlow.collect { ... } } } استفاده کنید.
محدودیتی وجود ندارد، اما توصیه میشود بیش از 3-5 StateFlow مجزا در هر صفحه نباشد. اگر حالتهای مختلف بیشتری نیاز است، آنها را از طریق sealed class یا data class ترکیب کنید. هر StateFlow در زمان جمعآوری نیاز به تخصیص شی Continuation دارد — صد StateFlow میتواند بار قابل توجهی برای GC ایجاد کند. طبق توصیه گوگل، یک sealed class UIState در هر صفحه — تعادل بهینه بین خوانایی و عملکرد است.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید