withContext: ما هو، تبديل السياق والعمل في Coroutines

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

withContext — هي دالة تبديل سياق التنفيذ داخل الروتين (coroutine) والتي تغير مؤقتاً الخيط أو الموزع (dispatcher) لكتلة الكود المحددة وتعيد النتيجة إلى السياق الأصلي. وفقاً JetBrains, 2025، يعتبر withContext من أكثر أدوات coroutines استخداماً للتعامل مع طلبات الشبكة وعمليات القرص. تضمن الدالة أنه بعد اكتمال الكتلة، يستمر coroutine في التنفيذ على الموزع الأصلي، مما يمنع أخطاء أمان الخيوط العرضية.

الملامح الرئيسية

  • withContext — دالة تعليق (suspending) تغير CoroutineContext لكتلة الكود المُمررة وتعيد النتيجة
  • Dispatchers.IO — الوسيط النموذجي للتبديل إلى خيط خلفي لعمليات الشبكة والقرص
  • Dispatchers.Main — السياق الأصلي الذي يعيد withContext التنفيذ إليه تلقائياً بعد اكتمال الكتلة
  • الاستدعاءات المتسلسلة — ينفذ withContext الكود بشكل تسلسلي، على عكس launch و async، مما يبسط التحكم في ترتيب العمليات
  • نتيجة Val — يعيد withContext قيمة مباشرة عبر return في السطر الأخير من lambda، دون await أو join

ما هو withContext في Kotlin؟

withContext هي دالة تعليق من حزمة kotlinx.coroutines التي تنفذ كتلة الكود المُمررة في CoroutineContext محدد وتعيد النتيجة إلى السياق الأصلي. توقيع الدالة يبدو كالتالي:

kotlin
suspend fun  withContext(
    context: CoroutineContext,
    block: suspend CoroutineScope.() -> T
): T

المعامل context يقبل أي CoroutineContext — الأكثر شيوعاً هو أحد Dispatchers.IO أو Dispatchers.Default أو Dispatchers.Main القياسية. يتم تنفيذ الكتلة في هذا السياق، وتُعاد النتيجة إلى المكان الذي تم فيه استدعاء withContext.

الميزة الرئيسية: الإرجاع التلقائي

بعد اكتمال lambda، يضمن withContext إعادة التنفيذ إلى الموزع الأصلي. هذا يعني أن المطور لا يحتاج إلى استدعاء withContext(Dispatchers.Main) يدوياً بعد عملية خلفية — الإرجاع يحدث تلقائياً. هذا السلوك موثق في مواصفات Kotlin Coroutines منذ الإصدار 1.3.

أين يُستخدم withContext

تطوير Android هو المجال الرئيسي لاستخدام withContext. السيناريو النموذجي: ViewModel يبدأ coroutine على الخيط الرئيسي، داخله يستدعي withContext(Dispatchers.IO) لطلب شبكة، والنتيجة بعد الإرجاع التلقائي إلى Main تُستخدم لتحديث واجهة المستخدم. هذا النهج هو أساس بنية MVVM وتوصي به Google في الدليل الرسمي لـ coroutines.

كيف يعمل withContext: تبديل الموزعين

لفهم withContext، تحتاج إلى فهم CoroutineContext ومكونه الرئيسي — الموزع (Dispatcher). كل coroutine لديه مجموعة من عناصر السياق، من بينها يحدد الموزع أي خيط أو مجموعة خيوط ينفذ عليها الكود.

الموزعون القياسيون لـ withContext

الموزعالغرضحجم المجموعة
Dispatchers.Mainالخيط الرئيسي لواجهة المستخدم (Android, JavaFX, Swing)1 (الخيط الرئيسي)
Dispatchers.IOعمليات القرص والشبكة64 خيطاً (الحد ينمو)
Dispatchers.Defaultالحسابات المكثفة للمعالجmax(2, عدد النوى)
Dispatchers.Unconfinedبدون خيط ثابتغير محدود

من المهم فهم أن withContext لا ينشئ coroutine جديداً —他只是 يبدل السياق للموجود. هذا هو الفرق الرئيسي عن launch و async اللذان يولدان coroutines جديدة. التنفيذ الداخلي لـ withContext محسّن: إذا تطابق السياق المطلوب مع السياق الحالي، لا يحدث تبديل — تنفذ الدالة على نفس الموزع.

متى لا يبدل withContext الخيط

Dispatchers.Main داخل withContext(Dispatchers.Main) لا يسبب تبديلاً — تتعرف Kotlin Coroutines على تطابق السياقات وتتخطى العملية غير الضرورية. وبالمثل، withContext(Dispatchers.Default) داخل coroutine يعمل بالفعل على Default لا يخلق حملاً زائداً. هذا التحسين مُنفذ في ContinuationInterceptor.

withContext مقابل launch و async: متى تختار ماذا

غالباً ما يخلط المبتدئون بين withContext و launch و async، حيث أن جميع الدوال الثلاث تعمل مع coroutines والسياق. لكن الغرض منها مختلف جوهرياً.

مقارنة الدوال الثلاث

الخاصيةwithContextlaunchasync
ينشئ coroutine جديداًلانعمنعم
يعيد نتيجةنعم (T مباشرة)لا (Job)نعم (Deferred<T>)
التنفيذتسلسليمتوازيمتوازي
انتظار النتيجةتلقائيjoin()await()
حالة الاستخدام النموذجيةتبديل الموزعإطلاق ونسيانحسابات متوازية

قاعدة الاختيار

إذا كنت بحاجة لتنفيذ عملية واحدة في خيط خلفي والحصول على نتيجة — استخدم withContext. إذا كنت بحاجة لتشغيل عدة عمليات مستقلة بالتوازي — استخدم async مع await. إذا لم تكن بحاجة للنتيجة (تسجيل، كتابة ذاكرة تخزين مؤقت) — استخدم launch. توصي Google باستخدام withContext كأداة مفضلة لـ طبقة Repository في بنية Android.

أمثلة كود مع withContext

دعنا نستعرض ثلاثة سيناريوهات عملية لاستخدام withContext في تطبيقات Android بلغة Kotlin. كل مثال يوضح مهمة محددة والنمط الصحيح.

مثال 1: طلب شبكة في Repository

ViewModel يستدعي طريقة من المستودع من coroutine على Main. داخلياً، withContext(Dispatchers.IO) ينفذ طلب HTTP، وتُعاد النتيجة تلقائياً:

kotlin
class UserRepository(
    private val api: UserApi
) {
    suspend fun getUser(id: String): User {
        return withContext(Dispatchers.IO) {
            api.fetchUser(id)
        }
    }
}

الـ Coroutine في ViewModel يستدعي getUser مثل أي دالة تعليق عادية — دون تحديد الموزع صراحةً. withContext يخفي تفاصيل تبديل الخيوط.

مثال 2: عمليتان خلفيتان متسلسلتان

عندما تحتاج لتنفيذ عدة عمليات IO واحدة تلو الأخرى، يجمعها withContext في كتلة واحدة. هذا أكثر كفاءة من لف كل عملية في withContext منفصل:

kotlin
suspend fun loadUserProfile(id: String): Profile {
    return withContext(Dispatchers.IO) {
        val user = api.fetchUser(id)
        val posts = api.fetchPosts(id)
        Profile(user, posts)
    }
}

كلا العمليتين تنفذان على Dispatchers.IO، ويتم إنشاء نتيجة Profile وإعادتها دون تبديل سياق غير ضروري. إذا كانت العمليات مستقلة، فمن الأفضل استخدام async للتنفيذ المتوازي.

مثال 3: سياق مختلط مع NonCancellable

في بعض السيناريوهات، تحتاج لتنفيذ كود لا يمكن إلغاؤه — مثلاً، حفظ الحالة عند إغلاق شاشة. مزيج withContext + NonCancellable يحل هذه المهمة:

kotlin
withContext(Dispatchers.IO + NonCancellable) {
    cache.saveState(state)
    analytics.logEvent("state_saved")
}

العامل + يدمج عنصرين من السياق: موزع IO وعلامة NonCancellable. يتم تنفيذ الكتلة حتى لو تم إلغاء coroutine الأب — مفيد لعمليات الإنهاء.

ماذا يحدث تحت الغطاء: Continuation والتحسينات

التنفيذ الداخلي لـ withContext يعتمد على آلية Continuation — التجريد المركزي لـ coroutines في Kotlin. كل نقطة تعليق تحفظ حالة التنفيذ في كائن Continuation، و withContext ليس استثناءً.

كيف يبدل withContext السياق على مستوى البايت كود

مترجم Kotlin يترجم withContext إلى استدعاء للطريقة withContext من kotlinx.coroutines، والتي داخلياً تنشئ مثيلاً جديداً من DispatchedContinuation. هذا الكائن يلف Continuation الأصلي ويستبدل موزعه. إذا كان الموزع الجديد يختلف عن الحالي، يتوقف التنفيذ، وتُرسل الكتلة إلى مجموعة الخيوط المناسبة، وبعد الاكتمال — تستأنف مع السياق الأصلي.

التحسين: المسار السريع عند تطابق السياقات

عند استدعاء withContext مع نفس الموزع الذي يعمل عليه coroutine بالفعل، ينشط Kotlin المسار السريع (fast-path): تنفذ الكتلة بشكل متزامن، دون إنشاء DispatchedContinuation ودون إرسالها إلى مجموعة الخيوط. هذا يجعل withContext مجانياً تقريباً للاستدعاءات المتكررة بنفس السياق. وفقاً لمعايير JetBrains (kotlinx.coroutines 1.8)، يكتمل المسار السريع في أقل من 0.1 µs.

اعتبارات الأداء

كل استدعاء لـ withContext مع موزع مختلف ينشئ DispatchedContinuation جديداً ويتطلب تبديل خيوط — وهذا يستغرق من 1 إلى 5 µs حسب الحمل. بالنسبة لمعظم التطبيقات، هذا التأخير غير ملحوظ، ولكن داخل الحلقات بآلاف التكرارات، من الأفضل تجميع العمليات في كتلة withContext واحدة.

الأخطاء الشائعة عند استخدام withContext

حتى المطورون ذوو الخبرة يرتكبون أخطاء عند العمل مع withContext. دعنا نستعرض أربع مشاكل شائعة وطرق منعها.

الخطأ 1: تداخل withContext غير ضروري

غالباً ما يلف المطورون كل سطر في withContext منفصل بدلاً من دمج العمليات في كتلة واحدة. كل استدعاء إضافي مع موزع مختلف يخلق حملاً زائداً.

الصحيح: دمج عمليات IO المتسلسلة في withContext(Dispatchers.IO) { ... } واحد. إذا كانت بعض العمليات مكثفة للمعالج — استخدم withContext(Dispatchers.Default) داخل نفس الكتلة.

الخطأ 2: استخدام withContext بدلاً من async للمهام المتوازية

ينفذ withContext الكود بشكل تسلسلي. إذا كان طلبا شبكة مستقلان ملفوفان في withContext واحد، فسيتم تنفيذهما واحداً تلو الآخر. للتوازي، استخدم async + await.

kotlin
// تسلسلي — بطيء
withContext(Dispatchers.IO) {
    val a = api.fetchA()
    val b = api.fetchB()
}

// متوازي — سريع
coroutineScope {
    val a = async { api.fetchA() }
    val b = async { api.fetchB() }
    println("${a.await()} ${b.await()}")
}

الخطأ 3: نسيان NonCancellable للعمليات الحرجة

إذا تم إلغاء coroutine أثناء withContext، فإن الكتلة على Dispatchers.IO تنقطع أيضاً. للعمليات التي يجب أن تكتمل بأي ثمن (الكتابة في قاعدة البيانات، إرسال التحليلات)، ادمج withContext مع NonCancellable.

الخطأ 4: تحديث حالة واجهة المستخدم داخل كتلة IO

لا تقم أبداً بتحديث مكونات View داخل withContext(Dispatchers.IO). لا يعود withContext إلى Main حتى اكتمال الكتلة بأكملها. انقل تحديثات واجهة المستخدم بعد قوس إغلاق withContext — عندها سيكون coroutine بالفعل على الخيط الرئيسي.

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

ما الفرق بين withContext و runBlocking؟

withContext هي دالة تعليق لا تمنع الخيط، بل تبدل السياق داخل coroutine موجود. runBlocking هو جسر بين coroutines والكود العادي الذي يمنع الخيط الحالي حتى الاكتمال. withContext آمن لخيط واجهة المستخدم، runBlocking ليس كذلك.

هل يمكن استخدام withContext بدون suspend؟

لا، withContext هي دالة suspend، لذلك يمكن استدعاؤها فقط من دالة suspend أخرى أو من coroutine (launch/async). من دالة عادية، لا يمكن استدعاء withContext — لذلك تحتاج إلى runBlocking أو CoroutineScope.

ماذا يحدث إذا مررت نفس الموزع إلى withContext؟

ينشط Kotlin المسار السريع (fast-path) — تنفذ الكتلة بشكل متزامن على نفس الخيط بدون تبديل. الحمل الزائد أقل من 0.1 µs. هذا ليس خطأ، لكن مثل هذا الاستدعاء زائد — من الأفضل تنفيذ الكود بدون withContext.

كيف يعمل withContext مع الاستثناءات؟

الاستثناءات داخل withContext تنتشر بنفس الطريقة كما في الكود العادي — عبر try-catch. إذا ألقت الكتلة استثناءً، ينتشر إلى coroutine الأب ويلغيه إذا لم تتم معالجته. استخدم try-catch داخل withContext أو حوله.

هل ينشئ withContext coroutine جديداً أم لا؟

لا، withContext لا ينشئ coroutine جديداً. إنه يستخدم coroutine الموجود لكنه يغير سياقه مؤقتاً. هذا يميزه عن launch و async اللذان يولدان coroutines فرعية. هذا السلوك مؤكد بواسطة كود مصدر kotlinx.coroutines.

الخلاصة

  • withContext — دالة تعليق لتبديل CoroutineContext داخل coroutine موجود مع إرجاع تلقائي إلى السياق الأصلي
  • Dispatchers.IO — الموزع الرئيسي لطلبات الشبكة وعمليات القرص داخل withContext
  • المسار السريع (Fast-path) — تحسين Kotlin حيث يتم تنفيذ withContext مع نفس الموزع بشكل متزامن دون حمل زائد
  • المهام المتوازية تتطلب async/await، وليس withContext — withContext ينفذ الكود بشكل تسلسلي
  • NonCancellable — علامة للعمليات الحرجة داخل withContext التي لا يجب أن تنقطع عند إلغاء coroutine
  • طبقة Repository — المكان الموصى به لـ withContext في بنية Android حسب إرشادات Google
  • Continuation — الآلية التي يقوم عليها تبديل السياق في withContext على مستوى البايت كود لـ Kotlin

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

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

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

اقرأ أيضًا