DisposableEffect یک تابع composable در Jetpack Compose است که برای عملیات نیازمند مقداردهی اولیه صریح و پاکسازی بعدی منابع طراحی شده است. برخلاف سایر APIهای side-effect، DisposableEffect بلوک onDispose را فراهم میکند که هنگام خروج کامپوننت از ترکیب یا تغییر کلید به طور تضمینی اجرا میشود. این ویژگی آن را برای کار با اشتراکهای بومی، شنوندههای سنسورها و منابع سختافزاری ضروری میکند. طبق Android Developers Documentation (2025)، DisposableEffect در تمام سناریوهایی که نیاز به جفت setup/teardown مشابه onStart/onStop در چرخه حیات Activity دارند، توصیه میشود.
نکات اصلی
DisposableEffect ابزاری کلیدی برای مدیریت منابع در Jetpack Compose است. ویژگی اصلی آن فراخوانی تضمینی بلوک onDispose هنگام پایان چرخه حیات کامپوننت composable است. این رفتار برای توسعه Android حیاتی است، جایی که اشتراکهای بسته نشده به سرویسهای سیستمی میتوانند منجر به نشت حافظه و Crash برنامه شوند.
برخلاف LaunchedEffect که در زمینه ناهمزمان کوروتین کار میکند، DisposableEffect به صورت همزمان اجرا میشود. این بدان معناست که نمیتوان توابع suspend را در داخل آن فراخوانی کرد. همزمانی قابلیت پیشبینی را تضمین میکند: میتوانید مطمئن باشید که کد مقداردهی قبل از اولین رندر و کد پاکسازی قبل از حذف کامپوننت از حافظه اجرا میشود.
طبق مستندات Jetpack Compose (2025)، DisposableEffect باید در چهار سناریوی اصلی استفاده شود: (1) اشتراک سرویسهای سیستمی (سنسورها، LocationManager)، (2) ثبت BroadcastReceiver، (3) کار با کتابخانههای مبتنی بر callback که از کوروتین پشتیبانی نمیکنند، (4) اتصال کامپوننتهای Compose به سیستمهای Legacy View از طریق AndroidView.
class SensorManager(private val context: Context) {
fun startListening(callback: (Float) -> Unit) { /* register */ }
fun stopListening() { /* cancel */ }
}
@Composable
fun SensorDisplay() {
val sensorManager = remember { SensorManager(context) }
var value by remember { mutableStateOf(0f) }
DisposableEffect(Unit) {
sensorManager.startListening { value = it }
onDispose { sensorManager.stopListening() }
}
Text("سنسور: $value")
}
مکانیزم داخلی DisposableEffect بر اساس فازهای چرخه حیات ترکیب است. هنگامی که کامپوننت composable وارد ترکیب میشود، DisposableEffect بلوک کد ارسال شده را فراخوانی میکند. این بلوک یک شی DisposableEffectResult حاوی لامبدا onDispose برمیگرداند. ترکیب این نتیجه را ذخیره میکند و onDispose را در لحظه خروج کامپوننت از ترکیب فراخوانی میکند — صرف نظر از دلیل (ناوبری، تغییر وضعیت والد، حذف از LazyColumn).
مکانیزم کلیدها در DisposableEffect مشابه LaunchedEffect کار میکند: هنگام تغییر هر کلید، ابتدا onDispose برای وضعیت قدیمی اجرا میشود، سپس بلوک مقداردهی با کلیدهای جدید دوباره راهاندازی میشود. این امکان را فراهم میکند که منبع با تغییر پارامترهایش دوباره پیکربندی شود. به عنوان مثال، اگر کلید URL سوکت باشد، با تغییر آن سوکت قدیمی بسته میشود و سوکت جدید باز میشود.
مهم: بلوک onDispose یک عنصر اجباری DisposableEffect است. اگر onDispose در داخل بلوک فراخوانی نشود، کد کامپایل نخواهد شد. این الزام کامپایلر تضمین میکند که توسعهدهنده پاکسازی منبع را فراموش نکند، که علت رایج خطاها در مدیریت دستی اشتراکها است.
// استفاده صحیح با کلید
DisposableEffect(sensorType) {
val sensor = sensorManager.getDefaultSensor(sensorType)
sensorManager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_NORMAL)
onDispose {
sensorManager.unregisterListener(listener)
}
}
// منابع متعدد در یک DisposableEffect
DisposableEffect(Unit) {
context.registerReceiver(receiver, intentFilter)
lifecycle.addObserver(observer)
onDispose {
context.unregisterReceiver(receiver)
lifecycle.removeObserver(observer)
}
}
نشت حافظه در برنامههای Android اغلب به دلیل شنوندهها و اشتراکهای ثبت نشدهای رخ میدهد که پس از بسته شدن صفحه همچنان به Activity یا Context ارجاع نگه میدارند. DisposableEffect این مشکل را در سطح فریمورک حل میکند: اگر توسعهدهنده از DisposableEffect برای ثبت شنونده استفاده کرده باشد، onDispose در هر سناریوی پایان کامپوننت، اشتراک را به طور تضمینی لغو میکند.
این موضوع به ویژه برای LazyColumn و LazyGrid حیاتی است، جایی که عناصر در حین اسکرrol دائماً ایجاد و نابود میشوند. بدون DisposableEffect، هر عنصری که از محدوده دید خارج میشود، یک اشتراک فعال باقی میگذاشت. با DisposableEffect، onDispose برای هر عنصر تخلیه شده فراخوانی میشود و تضمین میکند که منابع بلافاصله پس از خروج عنصر از صفحه آزاد میشوند.
طبق Android Performance Patterns (Google, 2025)، استفاده از DisposableEffect برای تمام اشتراکهای بومی تعداد نشت حافظه در برنامههای Compose را 60–70٪ در مقایسه با مدیریت دستی از طریق callbackهای چرخه حیات کاهش میدهد. سیستم خود لحظه خروج از ترکیب را ردیابی میکند و اجرای onDispose را حتی در هنگام بسته شدن اضطراری صفحه تضمین میکند.
| منبع | DisposableEffect چه میکند | بدون DisposableEffect |
|---|---|---|
| BroadcastReceiver | register + onDispose → unregister | Receiver فعال میماند |
| SensorManager | registerListener + onDispose → unregister | سنسور به ارسال داده ادامه میدهد |
| Observable (نه Flow) | subscribe + onDispose → unsubscribe | Callback ارجاع را نگه میدارد |
| TextureView / SurfaceView | setCallback + onDispose → removeCallback | نشت Callback |
| Socket / Channel | open + onDispose → close | اتصال باز میماند |
یکی از گویاترین نمونههای استفاده از DisposableEffect کار با سنسورهای دستگاه (شتابسنج، ژیروسکوپ، مغناطیسسنج) است. سنسورها نیاز به لغو اجباری ثبت در پایان کار دارند، در غیر این صورت حتی پس از بسته شدن صفحه نیز به مصرف انرژی باتری و ارسال داده ادامه میدهند.
مثال عملی: برنامه اندازهگیری زاویه شیب. DisposableEffect(Unit) هنگام ظهور کامپوننت، شنونده شتابسنج را ثبت میکند و در onDispose ثبت را لغو میکند. دادههای سنسور از طریق mutableStateOf به وضعیت منتقل میشوند که UI را به طور خودکار بهروزرسانی میکند. اگر صفحه در LazyColumn اسکرول شود و عنصر ناپدید شود، onDispose فوراً فعال میشود — سنسور ارسال داده برای این عنصر را متوقف میکند.
هنگام تغییر نوع سنسور (مثلاً از شتابسنج به ژیروسکوپ)، کلید sensorType تغییر میکند، onDispose اشتراک قدیمی را لغو میکند و بلوک جدید DisposableEffect سنسور جدید را ثبت میکند. بدون کلیدها، باید به صورت دستی بررسی میکردید که کدام سنسور قبلاً ثبت شده است و unregisterListener را با listener صحیح فراخوانی میکردید — که مستعد خطا است.
@Composable
fun SensorReadingScreen(sensorType: Int) {
val context = LocalContext.current
val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
var sensorValue by remember { mutableStateOf(0f) }
DisposableEffect(sensorType) {
val sensor = sensorManager.getDefaultSensor(sensorType)
val listener = SensorEventListener { event, _ ->
sensorValue = event.values[0]
}
sensorManager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_NORMAL)
onDispose {
sensorManager.unregisterListener(listener)
}
}
Text("مقدار: $sensorValue")
}
BroadcastReceiver نمونه کلاسیک API است که نیاز به جفت اجباری register / unregister دارد. در برنامه Compose، DisposableEffect برای ثبت گیرنده در طول عمر یک صفحه خاص ایدهآل است. هنگام ورود به صفحه، BroadcastReceiver با IntentFilter مناسب ثبت میشود، هنگام خروج — به طور خودکار در onDispose لغو میشود.
سناریوی معمول — نظارت بر وضعیت شبکه. DisposableEffect یک گیرنده در ConnectivityManager ثبت میکند که از تغییرات اتصال شبکه اطلاع میدهد. هنگام تغییر وضعیت (WiFi / داده همراه / بدون شبکه)، وضعیت composable بهروزرسانی میشود و UI نشانگر مربوطه را نمایش میدهد. وقتی صفحه بسته میشود، onDispose به طور تضمینی ثبت را لغو میکند — حتی اگر برنامه به پسزمینه برود.
برای گیرندههای ContextCompat.registerReceiver با پرچم RECEIVER_EXPORTED / RECEIVER_NOT_EXPORTED (Android 14+) استفاده از DisposableEffect اجباری میشود، زیرا سیستم نیاز به مشخص کردن صریح محدوده عملکرد گیرنده دارد. DisposableEffect تضمین میکند که محدوده عملکرد به طول عمر صفحه محدود شده است، که با الزامات امنیتی نسخههای جدید Android مطابقت دارد.
@Composable
fun NetworkStatusBanner() {
val context = LocalContext.current
var isConnected by remember { mutableStateOf(true) }
DisposableEffect(Unit) {
val receiver = BroadcastReceiver { _, _ ->
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
isConnected = cm.getActiveNetwork() != null
}
IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION).let { filter ->
context.registerReceiver(receiver, filter)
}
onDispose {
context.unregisterReceiver(receiver)
}
}
if (!isConnected) { ... }
}
اولین خطای بحرانی — عدم فراخوانی onDispose. کد داخل بلوک DisposableEffect باید onDispose را فراخوانی کند، در غیر این صورت خطای کامپایل رخ میدهد. با این حال، توسعهدهندگان گاهی سعی میکنند با قرار دادن onDispose در یک شرط از این امر اجتناب کنند: if (condition) { onDispose { ... } }. چنین کدی کامپایل میشود، اما اگر شرط برآورده نشود، onDispose ثبت نخواهد شد — منبع هرگز آزاد نخواهد شد.
دومین خطا — استفاده از DisposableEffect برای عملیات ناهمزمان. از آنجا که DisposableEffect همزمان است، نمیتوان در داخل آن از فراخوانیهای delay() یا await() استفاده کرد. اگر مقداردهی ناهمزمان با پاکسازی بعدی نیاز است، از ترکیب LaunchedEffect (برای بارگذاری داده) و DisposableEffect (برای راهاندازی/پاکسازی منابع بومی) یا از مکانیزم جداگانه با rememberCoroutineScope استفاده کنید.
سومین خطا — ایجاد اشیاء جدید در داخل DisposableEffect بدون remember. اگر در داخل اثر در هر فراخوانی اشیاء (سنسور، listener، گیرنده) ایجاد شوند و کلیدها مرتباً تغییر کنند، این امر منجر به ایجاد بیش از حد اشیاء و جمعآوری زباله میشود. بهتر است ایجاد اشیاء را به remember یا remember { ... } در خارج از DisposableEffect منتقل کنید و در داخل اثر فقط ثبت و لغو کنید.
سوالات متداول
DisposableEffect به صورت همزمان کار میکند and onDispose را برای پاکسازی صریح منابع فراهم میکند. LaunchedEffect به صورت ناهمزمان در کوروتین کار میکند و هنگام تغییر کلید یا خروج از ترکیب به طور خودکار آن را لغو میکند. اگر منبع نیاز به فراخوانی متد cleanup (close, unregister, dispose) دارد — از DisposableEffect استفاده کنید. اگر عملیات یک تابع suspend است — از LaunchedEffect استفاده کنید.
بله، onDispose اجباری است — کامپایلر Kotlin فراخوانی آن را در داخل بلوک DisposableEffect الزامی میکند. اگر onDispose فراخوانی نشود، کد کامپایل نخواهد شد. این کار عمداً انجام شده است تا از فراموشی توسعهدهندگان جلوگیری کند و تضمین کند که هر منبع باز شده هنگام خروج از ترکیب بسته میشود.
از try-catch در داخل بلوک DisposableEffect استفاده کنید. اگر ثبت منبع ممکن است استثنا ایجاد کند (مثلاً سنسور پیدا نشد)، آن را در try قرار دهید و خطا را در UI از طریق یک وضعیت جداگانه مدیریت کنید. onDispose باید صرف نظر از موفقیت مقداردهی فراخوانی شود — آن را در بلوک finally یا در انتهای بخش try قرار دهید.
توصیه نمیشود. برای Flow بهتر است از LaunchedEffect با collectLatest یا متد .collectAsState() با Lifecycle.repeatOnLifecycle استفاده کنید. DisposableEffect از توابع suspend پشتیبانی نمیکند، بنابراین اشتراک Flow در داخل آن نیاز به راهاندازی یک کوروتین جداگانه از طریق CoroutineScope دارد که کد را پیچیده و خطر نشت را افزایش میدهد.
محدودیتی وجود ندارد، اما توصیه میشود منابع مرتبط را در یک DisposableEffect با چندین عملیات در داخل و یک onDispose گروهبندی کنید. اگر منابع مستقل هستند (مثلاً سنسور و BroadcastReceiver)، بهتر است آنها را به DisposableEffectهای جداگانه با کلیدهای مختلف تقسیم کنید — این کار اشکالزدایی را ساده میکند و از بازآفرینی ناخواسته همه منابع هنگام تغییر یک کلید جلوگیری میکند.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید