StateFlow — حاوية حالة تفاعلية من مكتبة Kotlin Coroutines، تمثل StateFlow<T> — نوع فرعي من Flow يخزن دائمًا القيمة الحالية ويصدرها للمشتركين الجدد. نشرح جوهر StateFlow: على عكس LiveData، StateFlow غير مرتبط بإطار Android ويعمل على أي منصة Kotlin. وفقًا لـ Google (Android Developers، 2025)، يُوصى باستخدام StateFlow كبديل رئيسي لـ LiveData للمشاريع الجديدة بلغة Kotlin النقية، خاصة في بنية MVVM مع Jetpack Compose.
أهم النقاط
collectAsState() في Compose أو repeatOnLifecycle() في View.StateFlow هو واجهة من مكتبة kotlinx.coroutines.flow، توسع MutableSharedFlow بمعامل replay ثابت يساوي 1. هذا يعني أن StateFlow يتذكر دائمًا آخر قيمة مرسلة ويعيد تشغيلها فورًا لكل مشترك جديد. على عكس LiveData، StateFlow جزء من مكتبة Kotlin Coroutines القياسية وليس له تبعيات لنظام Android.
من الناحية المفاهيمية، StateFlow هو خاصية تفاعلية: تقرأ قيمته الحالية عبر .value وتشترك في التغييرات عبر .collect(). يُسمى هذا النموذج "التدفق الساخن" (hot flow) — مصدر البيانات نشط بغض النظر عن المشتركين، على عكس التدفقات "الباردة" (cold) التي تُنشأ عبر flow { }، والتي تبدأ عند ظهور مشترك.
تم تثبيت StateFlow في kotlinx.coroutines 1.3.7 (ديسمبر 2020) وأوصت به Google كبديل لـ LiveData بدءًا من Google I/O 2021. بحلول يناير 2025، وفقًا لاستطلاع JetBrains، 56% من مشاريع Android الجديدة بلغة Kotlin تستخدم StateFlow كحاوية تفاعلية رئيسية.
يعتمد الاختيار بين StateFlow و LiveData على بنية المشروع، مجموعة التقنيات المستخدمة، ومتطلبات استقلالية المنصة. فيما يلي مقارنة عبر ستة معايير رئيسية.
| المعيار | StateFlow | LiveData |
|---|---|---|
| المنصة | Kotlin Multiplatform (Android، iOS، خادم) | Android فقط |
| دورة الحياة | لا — يتطلب repeatOnLifecycle() | نعم — ربط مدمج |
| Coroutines | دعم كامل (map، filter، combine) | عبر builder liveData { } |
| السلامة من null | نعم — قابل للتسلسل عبر kotlinx.serialization | نعم — عبر LiveData<String?> القابل للnull |
| الدمج | مدمج — يتخطى القيم الوسيطة | فقط عبر postValue() |
| الاختبار | runTest + Turbine أو عوامل مدمجة | InstantTaskExecutorRule + observeForever |
StateFlow يتطلب إدارة صريحة للاشتراك في طبقة View: في Fragment/Activity، يتم الاشتراك عبر repeatOnLifecycle(STATE.STARTED) { viewModel.uiState.collect { ... } }. هذا يعطي تحكمًا أكثر من الاشتراك التلقائي لـ LiveData، لكنه يضيف كودًا نمطيًا. في Jetpack Compose، يتم تبسيط الاشتراك إلى val state by viewModel.uiState.collectAsState().
توصية Google (Android Developers، 2025): للمشاريع الجديدة بلغة Kotlin استخدم StateFlow، خاصة عند العمل مع Compose. أبقِ LiveData من أجل: (1) كود Java، (2) المكتبات التي تتطلب توافقًا مع Java، (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() من الإشعارات غير الضرورية بنسبة 90% مقارنة بـ LiveData — مما يعطي تحسنًا في الأداء عند ترددات التحديث العالية.
عند استخدام StateFlow في ViewModel، اتبع هذه القواعد: (1) استخدم MutableStateFlow مع معدل private داخل ViewModel؛ (2) انشر StateFlow للقراءة فقط عبر get()؛ (3) للشاشات المعقدة استخدم 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")
}
}
}
}
استخدام class مختوم كنوع حالة واحد هو النهج الموصى به من Google (UDF — تدفق البيانات أحادي الاتجاه). يضمن أن واجهة المستخدم دائمًا في حالة متناسقة: Loading أو Success أو Error، وليس في وقت واحد. في IT Sectr، انتقلنا إلى StateFlow + class مختوم لجميع الشاشات في عام 2022 — مما سهّل اختبار ViewModel بنسبة 40% بفضل الحالات المتوقعة.
stateIn() هو عامل يحول Flow بارد إلى StateFlow ساخن. يتطلب تحديد CoroutineScope (حيث تعمل coroutine الداخلية) واستراتيجية 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: بعد مغادرة آخر مشترك، تستمر coroutine الداخلية في العمل لمدة 5 ثوانٍ إضافية. إذا عاد المستخدم إلى الشاشة خلال هذا الوقت، يتم استعادة الاشتراك دون إعادة تشغيل التدفق. يمنع timeout عمليات إعادة التشغيل المتكررة أثناء التبديل السريع بين الشاشات. وفقًا لاختبارات Google (Android Performance، 2024)، يقلل WhileSubscribed مع timeout لمدة 5 ثوانٍ استهلاك المعالج بنسبة 25% مقارنة بـ Eagerly.
شاشة بحث كاملة مع استعلام بحث ونتائج وحالة تحميل. يستخدم ViewModel 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()
// ... واجهة مستخدم تتفاعل مع حالات Loading، Results، Error
}
Room (منذ الإصدار 2.4.0) يدعم إرجاع Flow من DAO. دمج Flows متعددة عبر 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. وفقًا لـ Google (Android Architecture Guide، 2025)، يُوصى بمجموعة Flow + StateFlow + Room لجميع مشاريع Kotlin التي تتطلب تحديثات تفاعلية لواجهة المستخدم عند تغيير قاعدة البيانات.
الأسئلة الشائعة
الدمج هو آلية يحتفظ بها StateFlow فقط بآخر قيمة مرسلة. إذا تم إرسال قيمة جديدة قبل أن يعالج المشترك القيمة السابقة، تفقد القيمة الوسيطة. هذا مهم لواجهة المستخدم: إذا تغيرت الحالة من Loading → Success → Error، ولم تعرض واجهة المستخدم Success، فإنها تنتقل مباشرة إلى Error دون عرض إضافي. الدمج هو تحسين رئيسي في Android يمنع إعادة التركيب المفرطة في Compose.
استخدم دالة الامتداد liveData.asFlow() من مكتبة lifecycle-livedata-ktx، ثم .stateIn() للتحويل إلى StateFlow. التحويل العكسي هو stateFlow.asLiveData(). التحويل مفيد عند الترحيل من LiveData إلى StateFlow: يمكنك تحويل ViewModels تدريجيًا إلى StateFlow مع ترك View القديم مشتركًا عبر LiveData.
يجب أن يكون لـ StateFlow دائمًا قيمة — هذا هو عقد الواجهة: أي مشترك متصل حديثًا يتلقى فورًا الحالة الحالية دون انتظار. تُمرر القيمة الأولية إلى مُنشئ MutableStateFlow(initialValue) أو إلى عامل stateIn(initialValue). إذا كانت الحالة قد تكون غائبة، استخدم MutableStateFlow<T?>(null) مع نوع nullable وتعامل مع null في واجهة المستخدم.
نعم، StateFlow آمن للخيوط: قراءة وكتابة value تستخدمان عمليات ذرية (CAS). ومع ذلك، collect() هي دالة suspend ويجب تشغيلها في coroutine. إذا حدث الإصدار والتجميع على خيوط مختلفة، يضمن StateFlow happens-before لجميع العمليات على value. لتجميع StateFlow في View، استخدم lifecycleScope.launch { repeatOnLifecycle(STATE.STARTED) { stateFlow.collect { ... } } }.
لا يوجد حد صارم، لكن يُوصى باستخدام ما لا يزيد عن 3-5 StateFlows منفصلة لكل شاشة. إذا كانت هناك حاجة لمزيد من الحالات المختلفة، ادمجها في حالة واحدة عبر class مختوم أو data class. يتطلب كل StateFlow تخصيص كائن Continuation أثناء التجميع — مائة StateFlow يمكن أن تخلق ضغطًا ملحوظًا على GC. وفقًا لتوصية Google، class مختوم UIState واحد لكل شاشة هو التوازن الأمثل بين readability والأداء.
الخلاصة
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.
اقرأ أيضًا