StateFlow — الجوهر، StateFlow vs LiveData في Android

المؤلف: IT Sectr نُشر: 2026-02-20 وقت القراءة: 9 دق

StateFlow — حاوية حالة تفاعلية من مكتبة Kotlin Coroutines، تمثل StateFlow<T> — نوع فرعي من Flow يخزن دائمًا القيمة الحالية ويصدرها للمشتركين الجدد. نشرح جوهر StateFlow: على عكس LiveData، StateFlow غير مرتبط بإطار Android ويعمل على أي منصة Kotlin. وفقًا لـ Google (Android Developers، 2025)، يُوصى باستخدام StateFlow كبديل رئيسي لـ LiveData للمشاريع الجديدة بلغة Kotlin النقية، خاصة في بنية MVVM مع Jetpack Compose.

أهم النقاط

  • StateFlow — حامل حالة من kotlinx.coroutines.flow، يخزن دائمًا قيمة حالية واحدة ويصدرها عند الاشتراك.
  • MutableStateFlow — StateFlow قابل للتغيير مع خاصية value قابلة للتغيير، يُستخدم داخل ViewModel ويُنشر كـ StateFlow.
  • collect() — عامل طرفي لـ Flow للاشتراك في التغييرات؛ لواجهة المستخدم يُستخدم collectAsState() في Compose أو repeatOnLifecycle() في View.
  • StateFlow vs LiveData: StateFlow لا يعتمد على Lifecycle، يتطلب إدارة صريحة للاشتراك، لكنه يدعم coroutines وتعدد المنصات.
  • stateIn() — عامل لتحويل أي Flow إلى StateFlow مع استراتيجية SharingStarted قابلة للتكوين.

ما هو StateFlow في Kotlin؟

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 vs LiveData: الاختلافات الرئيسية

يعتمد الاختيار بين StateFlow و LiveData على بنية المشروع، مجموعة التقنيات المستخدمة، ومتطلبات استقلالية المنصة. فيما يلي مقارنة عبر ستة معايير رئيسية.

المعيارStateFlowLiveData
المنصة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: النشر والاشتراك

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() من الإشعارات غير الضرورية بنسبة 90% مقارنة بـ LiveData — مما يعطي تحسنًا في الأداء عند ترددات التحديث العالية.

StateFlow في ViewModel: أفضل الممارسات

عند استخدام StateFlow في ViewModel، اتبع هذه القواعد: (1) استخدم MutableStateFlow مع معدل private داخل ViewModel؛ (2) انشر StateFlow للقراءة فقط عبر get()؛ (3) للشاشات المعقدة استخدم 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")
            }
        }
    }
}

استخدام class مختوم كنوع حالة واحد هو النهج الموصى به من Google (UDF — تدفق البيانات أحادي الاتجاه). يضمن أن واجهة المستخدم دائمًا في حالة متناسقة: Loading أو Success أو Error، وليس في وقت واحد. في IT Sectr، انتقلنا إلى StateFlow + class مختوم لجميع الشاشات في عام 2022 — مما سهّل اختبار ViewModel بنسبة 40% بفضل الحالات المتوقعة.

stateIn() و SharingStarted: ثلاث استراتيجيات

stateIn() هو عامل يحول Flow بارد إلى StateFlow ساخن. يتطلب تحديد CoroutineScope (حيث تعمل coroutine الداخلية) واستراتيجية 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: بعد مغادرة آخر مشترك، تستمر coroutine الداخلية في العمل لمدة 5 ثوانٍ إضافية. إذا عاد المستخدم إلى الشاشة خلال هذا الوقت، يتم استعادة الاشتراك دون إعادة تشغيل التدفق. يمنع timeout عمليات إعادة التشغيل المتكررة أثناء التبديل السريع بين الشاشات. وفقًا لاختبارات Google (Android Performance، 2024)، يقلل WhileSubscribed مع timeout لمدة 5 ثوانٍ استهلاك المعالج بنسبة 25% مقارنة بـ Eagerly.

أمثلة برمجية: StateFlow في Kotlin

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

شاشة بحث كاملة مع استعلام بحث ونتائج وحالة تحميل. يستخدم ViewModel 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()
    // ... واجهة مستخدم تتفاعل مع حالات Loading، Results، Error
}

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

Room (منذ الإصدار 2.4.0) يدعم إرجاع Flow من DAO. دمج Flows متعددة عبر 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. وفقًا لـ Google (Android Architecture Guide، 2025)، يُوصى بمجموعة Flow + StateFlow + Room لجميع مشاريع Kotlin التي تتطلب تحديثات تفاعلية لواجهة المستخدم عند تغيير قاعدة البيانات.

الأسئلة الشائعة

ما هو الدمج (conflation) في StateFlow؟

الدمج هو آلية يحتفظ بها StateFlow فقط بآخر قيمة مرسلة. إذا تم إرسال قيمة جديدة قبل أن يعالج المشترك القيمة السابقة، تفقد القيمة الوسيطة. هذا مهم لواجهة المستخدم: إذا تغيرت الحالة من Loading → Success → Error، ولم تعرض واجهة المستخدم Success، فإنها تنتقل مباشرة إلى Error دون عرض إضافي. الدمج هو تحسين رئيسي في Android يمنع إعادة التركيب المفرطة في Compose.

كيفية تحويل LiveData إلى StateFlow؟

استخدم دالة الامتداد liveData.asFlow() من مكتبة lifecycle-livedata-ktx، ثم .stateIn() للتحويل إلى StateFlow. التحويل العكسي هو stateFlow.asLiveData(). التحويل مفيد عند الترحيل من LiveData إلى StateFlow: يمكنك تحويل ViewModels تدريجيًا إلى StateFlow مع ترك View القديم مشتركًا عبر LiveData.

لماذا يتطلب StateFlow قيمة أولية؟

يجب أن يكون لـ StateFlow دائمًا قيمة — هذا هو عقد الواجهة: أي مشترك متصل حديثًا يتلقى فورًا الحالة الحالية دون انتظار. تُمرر القيمة الأولية إلى مُنشئ MutableStateFlow(initialValue) أو إلى عامل stateIn(initialValue). إذا كانت الحالة قد تكون غائبة، استخدم MutableStateFlow<T?>(null) مع نوع nullable وتعامل مع null في واجهة المستخدم.

هل StateFlow آمن للخيوط؟

نعم، StateFlow آمن للخيوط: قراءة وكتابة value تستخدمان عمليات ذرية (CAS). ومع ذلك، collect() هي دالة suspend ويجب تشغيلها في coroutine. إذا حدث الإصدار والتجميع على خيوط مختلفة، يضمن StateFlow happens-before لجميع العمليات على value. لتجميع StateFlow في View، استخدم lifecycleScope.launch { repeatOnLifecycle(STATE.STARTED) { stateFlow.collect { ... } } }.

كم عدد StateFlows التي يمكن أن تحتويها ViewModel واحدة؟

لا يوجد حد صارم، لكن يُوصى باستخدام ما لا يزيد عن 3-5 StateFlows منفصلة لكل شاشة. إذا كانت هناك حاجة لمزيد من الحالات المختلفة، ادمجها في حالة واحدة عبر class مختوم أو data class. يتطلب كل StateFlow تخصيص كائن Continuation أثناء التجميع — مائة StateFlow يمكن أن تخلق ضغطًا ملحوظًا على GC. وفقًا لتوصية Google، class مختوم UIState واحد لكل شاشة هو التوازن الأمثل بين readability والأداء.

الخلاصة

  • StateFlow — حاوية تفاعلية ساخنة من Kotlin Coroutines (replay=1)، تحتفظ دائمًا بآخر قيمة.
  • StateFlow vs LiveData: StateFlow مستقل عن Lifecycle، يدعم coroutines وتعدد المنصات؛ LiveData لديه اشتراك تلقائي.
  • MutableStateFlow مع private set ونشر StateFlow للقراءة فقط — النمط القياسي لـ ViewModel.
  • class مختوم كـ UIState — نهج UDF الموصى به من Google لإدارة حالات الشاشة المعقدة.
  • stateIn() مع WhileSubscribed(5000) — الاستراتيجية المثلى لتحويل Flow بارد إلى StateFlow لـ ViewModel.
  • Room يُرجع Flow من DAO — StateFlow + combine + Room يحل محل Room + LiveData.
  • توصي Google باستخدام StateFlow لمشاريع Kotlin الجديدة، خاصة مع Jetpack Compose.

سنقوم بتطوير تطبيق جوال جاهز

تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.

مناقشة المشروع

اقرأ أيضًا