withContext — تابع تغییردهنده زمینه اجرا در داخل همبرنامه است که به طور موقت نخ یا توزیعکننده را برای بلوک کد مشخص شده تغییر میدهد و نتیجه را به زمینه اصلی بازمیگرداند. طبق دادههای JetBrains, 2025، withContext یکی از پرکاربردترین ابزارهای کوروتین برای کار با درخواستهای شبکه و عملیات دیسک است. این تابع تضمین میکند که پس از اتمام بلوک، کوروتین اجرا را روی توزیعکننده اصلی ادامه دهد که از خطاهای تصادفی امنیت نخ جلوگیری میکند.
نکات اصلی
withContext — تابع تعلیقی از بسته kotlinx.coroutines است که بلوک کد داده شده را در CoroutineContext مشخص شده اجرا میکند و نتیجه را به زمینه اصلی بازمیگرداند. امضای تابع به این صورت است:
suspend fun withContext (
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): T
پارامتر context هر CoroutineContext را میپذیرد — اغلب یکی از Dispatchers.IO، Dispatchers.Default یا Dispatchers.Main استاندارد. بلوک دقیقاً در این زمینه اجرا میشود و نتیجه به جایی که withContext فراخوانی شده بازگردانده میشود.
پس از اتمام لامبدا، withContext به طور تضمینی اجرا را به توزیعکننده اصلی بازمیگرداند. این بدان معناست که توسعهدهنده نیازی به فراخوانی دستی withContext(Dispatchers.Main) پس از عملیات پسزمینه ندارد — بازگشت به طور خودکار انجام میشود. این رفتار در مشخصات Kotlin Coroutines از نسخه 1.3 تثبیت شده است.
توسعه اندروید — حوزه اصلی کاربرد withContext است. سناریوی معمول: ViewModel یک کوروتین روی نخ اصلی راهاندازی میکند، داخل آن withContext(Dispatchers.IO) برای درخواست شبکه فراخوانی میشود و نتیجه پس از بازگشت خودکار به Main برای بهروزرسانی UI استفاده میشود. این رویکرد اساس معماری MVVM را تشکیل میدهد و توسط Google در راهنمای رسمی کوروتینها توصیه میشود.
برای درک withContext، باید با CoroutineContext و مؤلفه کلیدی آن — توزیعکننده (Dispatcher) آشنا شوید. هر همبرنامه مجموعهای از عناصر زمینه دارد که در میان آنها توزیعکننده تعیین میکند کد روی کدام نخ یا مجموعه نخها اجرا شود.
| توزیعکننده | کاربرد | اندازه مجموعه |
|---|---|---|
| Dispatchers.Main | نخ اصلی UI (Android, JavaFX, Swing) | 1 (نخ اصلی) |
| Dispatchers.IO | عملیات دیسک و شبکه | 64 نخ (محدودیت افزایش مییابد) |
| Dispatchers.Default | محاسبات سنگین CPU | max(2, تعداد هستهها) |
| Dispatchers.Unconfined | بدون نخ ثابت | نامحدود |
درک این نکته مهم است که withContext یک کوروتین جدید ایجاد نمیکند — فقط زمینه را تغییر میدهد برای کوروتین موجود. این تفاوت کلیدی با launch و async است که همبرنامههای جدیدی ایجاد میکنند. پیادهسازی داخلی withContext بهینه شده است: اگر زمینه درخواستی با زمینه فعلی مطابقت داشته باشد، تغییری رخ نمیدهد — تابع روی همان توزیعکننده اجرا میشود.
Dispatchers.Main در داخل withContext(Dispatchers.Main) باعث تغییر نمیشود — Kotlin Coroutines یکسانی زمینهها را تشخیص میدهد و عملیات اضافی را رد میکند. به طور مشابه، withContext(Dispatchers.Default) در داخل کوروتینی که قبلاً روی Default کار میکند، هیچ سرباری ایجاد نمیکند. این بهینهسازی در ContinuationInterceptor پیادهسازی شده است.
تازهکارها اغلب withContext را با launch و async اشتباه میگیرند، زیرا هر سه تابع با کوروتینها و زمینه کار میکنند. با این حال، هدف آنها اساساً متفاوت است.
| ویژگی | withContext | launch | async |
|---|---|---|---|
| کوروتین جدید ایجاد میکند | خیر | بله | بله |
| نتیجه برمیگرداند | بله (T مستقیم) | خیر (Job) | بله (Deferred<T>) |
| اجرا | ترتیبی | موازی | موازی |
| انتظار برای نتیجه | خودکار | join() | await() |
| کاربرد معمول | تغییر توزیعکننده | Fire-and-forget | محاسبات موازی |
اگر نیاز به اجرای یک عملیات در نخ پسزمینه و دریافت نتیجه دارید — از withContext استفاده کنید. اگر نیاز به اجرای چند عملیات مستقل به صورت موازی دارید — از async با await استفاده کنید. اگر نتیجه لازم نیست (ثبت日志، نوشتن کش) — launch. Google با withContext را به عنوان ابزار ترجیحی برای لایه Repository در معماری Android توصیه میکند.
سه سناریوی عملی استفاده از withContext در برنامههای Android با Kotlin را بررسی میکنیم. هر نمونه یک وظیفه خاص و الگوی صحیح را نشان میدهد.
ViewModel متد مخزن را از کوروتین روی Main فراخوانی میکند. در داخل withContext(Dispatchers.IO) درخواست HTTP اجرا میشود و نتیجه به طور خودکار بازگردانده میشود:
class UserRepository(
private val api: UserApi
) {
suspend fun getUser(id: String): User {
return withContext(Dispatchers.IO) {
api.fetchUser(id)
}
}
}
کوروتین در ViewModel getUser را مانند یک تابع تعلیق معمولی فراخوانی میکند — بدون تعیین صریح توزیعکننده. withContext جزئیات تغییر نخ را پنهان میکند.
زمانی که نیاز به اجرای چند عملیات IO پشت سر هم است، withContext آنها را در یک بلوک واحد ترکیب میکند. این کارآمدتر از قرار دادن هر عملیات در یک withContext جداگانه است:
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 استفاده کنید.
در برخی سناریوها نیاز به اجرای کدی است که نمیتوان آن را لغو کرد — مثلاً ذخیره وضعیت هنگام بسته شدن صفحه. ترکیب withContext + NonCancellable این وظیفه را حل میکند:
withContext(Dispatchers.IO + NonCancellable) {
cache.saveState(state)
analytics.logEvent("state_saved")
}
عملگر + دو عنصر زمینه را ترکیب میکند: توزیعکننده IO و پرچم NonCancellable. بلوک حتی اگر کوروتین والد لغو شده باشد اجرا میشود — این برای عملیات نهاییسازی مفید است.
پیادهسازی داخلی withContext بر اساس مکانیزم Continuation — انتزاع مرکزی کوروتینهای Kotlin است. هر نقطه تعلیق (suspend point) وضعیت اجرا را در شیء Continuation ذخیره میکند و withContext نیز از این قاعده مستثنی نیست.
کامپایلر Kotlin withContext را به فراخوانی متد withContext از kotlinx.coroutines ترجمه میکند که در داخل یک نمونه جدید DispatchedContinuation ایجاد میکند. این شیء Continuation اصلی را میپوشاند و توزیعکننده را در آن جایگزین میکند. اگر توزیعکننده جدید با فعلی متفاوت باشد، اجرا معلق میشود، بلوک به مجموعه نخهای مربوطه ارسال میشود و پس از اتمام با زمینه اصلی از سر گرفته میشود.
زمانی که withContext با همان توزیعکنندهای فراخوانی میشود که کوروتین قبلاً روی آن در حال اجراست، Kotlin fast-path را فعال میکند: بلوک به صورت همزمان، بدون ایجاد DispatchedContinuation و بدون ارسال به مجموعه نخها اجرا میشود. این باعث میشود withContext در فراخوانیهای مکرر با همان زمینه عملاً بدون هزینه باشد. طبق بنچمارکهای JetBrains (kotlinx.coroutines 1.8)، fast-path در کمتر از 0.1 میکروثانیه اجرا میشود.
هر فراخوانی withContext با توزیعکننده متفاوت یک DispatchedContinuation جدید ایجاد میکند و نیاز به تغییر نخ دارد — این بسته به بار از 1 تا 5 میکروثانیه زمان میبرد. برای اکثر برنامهها این تأخیر نامحسوس است، اما در حلقههایی با هزاران تکرار، بهتر است عملیاتها را در یک بلوک withContext تجمیع کنید.
حتی توسعهدهندگان با تجربه هنگام کار با withContext اشتباه میکنند. چهار مشکل رایج و راههای پیشگیری از آنها را بررسی میکنیم.
توسعهدهندگان اغلب هر خط را در یک withContext جداگانه قرار میدهند، به جای اینکه عملیاتها را در یک بلوک ترکیب کنند. هر فراخوانی اضافی با توزیعکننده متفاوت سربار ایجاد میکند.
درست: عملیاتهای IO متوالی را در یک withContext(Dispatchers.IO) { ... } ترکیب کنید. اگر بخشی از عملیاتها CPU-فشرده هستند — از withContext(Dispatchers.Default) در داخل همان بلوک استفاده کنید.
withContext کد را به صورت ترتیبی اجرا میکند. اگر دو درخواست شبکه مستقل در یک withContext قرار داده شوند، یکی پس از دیگری اجرا میشوند. برای موازیسازی از async + await استفاده کنید.
// ترتیبی — کند
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()}")
}
اگر کوروتین در حین withContext لغو شود، بلوک روی Dispatchers.IO نیز قطع میشود. برای عملیاتی که باید به هر قیمتی تکمیل شوند (نوشتن در پایگاه داده، ارسال آمار)، withContext را با NonCancellable ترکیب کنید.
هرگز کامپوننتهای View را در داخل withContext(Dispatchers.IO) بهروز نکنید. withContext تا پایان بلوک به Main بازنمیگردد. بهروزرسانی UI را بعد از براکت بسته withContext انجام دهید — در آن زمان کوروتین قبلاً روی نخ اصلی خواهد بود.
سوالات متداول
withContext — تابع تعلیقی است که نخ را مسدود نمیکند، بلکه زمینه را در داخل کوروتین موجود تغییر میدهد. runBlocking — پلی بین کوروتینها و کد معمولی است که نخ فعلی را تا اتمام مسدود میکند. withContext برای نخ UI ایمن است، runBlocking — نیست.
خیر، withContext یک تابع تعلیقی است، بنابراین فقط میتوان آن را از تابع تعلیقی دیگر یا از کوروتین (launch/async) فراخوانی کرد. from یک تابع معمولی withContext فراخوانی نمیشود — برای این کار به runBlocking یا CoroutineScope نیاز است.
Kotlin fast-path را فعال میکند — بلوک به صورت همزمان روی همان نخ بدون تغییر اجرا میشود. سربار کمتر از 0.1 میکروثانیه است. این یک خطا نیست، اما چنین فراخوانی اضافی است — بهتر است کد را بدون withContext اجرا کنید.
استثناهای داخل withContext مانند کد معمولی — از طریق try-catch — منتشر میشوند. اگر بلوک استثنا پرتاب کند، به کوروتین والد منتشر میشود و در صورت عدم مدیریت، آن را لغو میکند. از try-catch در داخل withContext یا اطراف آن استفاده کنید.
خیر، withContext یک کوروتین جدید ایجاد نمیکند. از همبرنامه موجود استفاده میکند اما به طور موقت زمینه آن را تغییر میدهد. این آن را از launch و async متمایز میکند که کوروتینهای فرزند ایجاد میکنند. این رفتار توسط کد منبع kotlinx.coroutines تأیید شده است.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید