SideEffect — چیست، همگام‌سازی وضعیت در Jetpack Compose

نویسنده: IT Sectr منتشر شده: 2026-06-30 زمان مطالعه: 9 دقیقه

SideEffect — یک تابع composable در Jetpack Compose است که بلوک کد داده شده را در هر بازترکیب موفق اجرا می‌کند. برخلاف LaunchedEffect و DisposableEffect، SideEffect به کلیدها وابسته نیست و بلوک پاک‌سازی ندارد — فقط پس از هر رندر، وضعیت Compose را با سیستم‌های خارجی همگام‌سازی می‌کند. این آن را برای به‌روزرسانی توابع callback، همگام‌سازی با ViewPager و انتقال داده به Analytics SDK ایده‌آل می‌کند. طبق Android Developers Documentation (2025)، SideEffect دقیقاً پس از تأیید بازترکیب موفق توسط Compose اجرا می‌شود و اگر بازترکیب نادیده گرفته شود، اجرا نمی‌شود.

نکات اصلی

  • SideEffect — API side-effect برای کدی که پس از هر بازترکیب موفق اجرا می‌شود.
  • همگام‌سازی — وضعیت Compose را به سیستم‌های خارجی که از Compose پشتیبانی نمی‌کنند منتقل می‌کند.
  • بدون کلید — برخلاف LaunchedEffect، SideEffect راه‌اندازی مجدد نمی‌شود، بلکه در هر بازترکیب اجرا می‌شود.
  • بدون پاک‌سازی — SideEffect onDispose ارائه نمی‌دهد، فقط برای همگام‌سازی یک‌طرفه طراحی شده است.
  • همزمانی — بلوک به صورت همزمان با فاز ترکیب Compose و بدون کروتین اجرا می‌شود.

SideEffect در Jetpack Compose چیست

SideEffect — ساده‌ترین API از مجموعه side-effect در Jetpack Compose است. این تابع بلوک کد را در هر بازترکیب موفق یک کامپوننت composable اجرا می‌کند. کلمه «موفق» در اینجا کلیدی است: اگر Compose تشخیص دهد که بازترکیب لازم نیست (مثلاً همه پارامترهای ورودی تغییر نکرده‌اند و نتیجه یکسان خواهد بود)، SideEffect اجرا نمی‌شود. این تضمین می‌کند که بلوک همگام‌سازی فقط زمانی فراخوانی می‌شود که UI واقعاً تغییر کرده باشد.

سناریوی اصلی استفاده از SideEffect — همگام‌سازی وضعیت Compose با اشیایی که بخشی از درخت Compose نیستند. مثال‌های معمول: به‌روزرسانی تابع callback در سیستم View قدیمی، انتقال وضعیت فعلی به ViewPager، ارسال رویداد به Analytics SDK هنگام تغییر داده‌های نمایش داده شده، همگام‌سازی با SDKهای نقشه که انتظار به‌روزرسانی در قالب خارجی دارند.

طبق Android Developer Blog (2025)، SideEffect اغلب در ترکیب با remember استفاده می‌شود: remember شیء (مثلاً callback) را ذخیره می‌کند و SideEffect آن را در هر تغییر وابستگی به‌روز می‌کند. این الگو به ویژه برای کتابخانه‌هایی که اشیاء listener را می‌پذیرند و در هنگام به‌روزرسانی آن‌ها را بازسازی نمی‌کنند مهم است — بدون SideEffect، listener حاوی ارجاع قدیمی به وضعیت فعلی خواهد بود.

kotlin
@Composable
fun MapScreen(zoomLevel: Int, markers: List<Marker>) {
    val mapView = remember { MapView(LocalContext.current) }
    
    SideEffect {
        mapView.setZoom(zoomLevel)
        mapView.updateMarkers(markers)
    }
    
    AndroidView(factory = { mapView })
}

SideEffect چگونه کار می‌کند و فازهای ترکیب

برای درک SideEffect، باید فازهای اجرای Jetpack Compose را بشناسید. Compose برای هر فریم سه فاز را طی می‌کند: Composition (چه چیزی نمایش داده شود)، Layout (کجا نمایش داده شود)، Drawing (چگونه نمایش داده شود). SideEffect در انتهای فاز Composition — پس از اجرای همه توابع composable اما قبل از فاز Layout — اجرا می‌شود. این تضمین می‌کند که SideEffect وضعیت نهایی همه متغیرها را پس از بازترکیب می‌بیند.

این موقعیت در چرخه حیات یک مزیت مهم می‌دهد: SideEffect نمی‌تواند باعث بازترکیب بی‌نهایت شود، حتی اگر وضعیت درون آن تغییر کند. از آنجایی که پس از ترکیب اجرا می‌شود، تغییرات انجام شده درون SideEffect فقط در فریم بعدی اعمال می‌شوند — این کار از حلقه‌هایی که مشخصه تغییرات درون بدنه تابع composable هستند (زمانی که setState درون ترکیب باعث ایجاد ترکیب جدید قبل از اتمام ترکیب فعلی می‌شود) جلوگیری می‌کند.

یکی دیگر از ویژگی‌ها — SideEffect با کلیدها بهینه نمی‌شود. صرف نظر از اینکه کدام وضعیت تغییر کرده است، در هر بازترکیب اجرا می‌شود. اگر کنترل دقیق‌تری لازم است (اجرا فقط هنگام تغییر یک پارامتر خاص)، از LaunchedEffect با کلیدها استفاده کنید یا SideEffect را در یک بررسی تغییر از طریق remember بپیچید.

kotlin
// SideEffect بهینه‌سازی شده با remember
var currentZoom by remember { mutableIntStateOf(zoomLevel) }

SideEffect {
    if (currentZoom != zoomLevel) {
        map.animateToZoom(zoomLevel)
        currentZoom = zoomLevel
    }
}

// بدون این بررسی SideEffect animateToZoom را فراخوانی می‌کرد
// در هر بازترکیب، حتی اگر zoomLevel تغییر نکرده بود

به‌روزرسانی توابع callback از طریق SideEffect

متداول‌ترین سناریوی عملی برای SideEffect — به‌روزرسانی توابع callback که وضعیت فعلی را محصور می‌کنند. در Jetpack Compose به این «مدیریت چرخه حیات callback» می‌گویند. مشکل اینجاست که عبارات lambda در Kotlin متغیرها را با مرجع می‌گیرند و اگر callback با یک مقدار متغیر ایجاد شده باشد و سپس متغیر تغییر کند — callback به استفاده از مقدار قدیمی ادامه می‌دهد.

یک مثال را در نظر بگیرید: Google Maps SDK برای اندروید شیء OnCameraMoveListener را از طریق setOnCameraMoveListener() می‌پذیرد. اگر یک lambda که isTrackingEnabled را می‌گیرد ارسال کنید، با تغییر isTrackingEnabled، lambda به‌روز نمی‌شود — Maps SDK به فراخوانی callback قدیمی با داده‌های قدیمی ادامه می‌دهد. SideEffect این مشکل را حل می‌کند: listener را در هر بازترکیب دوباره تنظیم می‌کند و تضمین می‌کند که SDK همیشه از lambda به‌روز با وضعیت فعلی استفاده می‌کند.

طبق مستندات Maps SDK برای اندروید (2025)، Google دقیقاً این الگو را برای ادغام Maps با Jetpack Compose توصیه می‌کند. رویکرد مشابهی برای WebView، VideoView، TextureView و سایر کامپوننت‌های مبتنی بر View که callbackها را از طریق متدهای set می‌پذیرند استفاده می‌شود. SideEffect به‌روزرسانی callbackها را در هر تغییر وضعیت تضمین می‌کند.

kotlin
@Composable
fun MapComposable(isTrackingEnabled: Boolean, onMarkerClick: (Marker) -> Unit) {
    val mapView = remember { MapView(LocalContext.current) }
    
    SideEffect {
        mapView.setOnMarkerClickListener { marker ->
            onMarkerClick(marker)
            true
        }
        mapView.isTrafficEnabled = isTrackingEnabled
    }
    
    AndroidView(factory = { mapView })
}

همگام‌سازی با Analytics SDK

یکی دیگر از سناریوهای مهم SideEffect — ارسال رویدادها به سیستم‌های تحلیلی هنگام تغییر وضعیت UI است. به عنوان مثال، وقتی کاربر برگه‌ها را در TabLayout درون یک صفحه Compose تغییر می‌دهد، SideEffect می‌تواند وضعیت فعلی برگه انتخاب شده را به Firebase Analytics یا AppsFlyer منتقل کند. هر بار که برگه انتخاب شده تغییر می‌کند (و بازترکیب رخ می‌دهد)، SideEffect رویداد مربوطه را ارسال می‌کند.

تفاوت با ارسال رویدادها مستقیماً درون onClick یا onTabSelected این است که SideEffect به تغییر وضعیت ناشی از هر روشی — نه فقط اقدام کاربر، بلکه تغییر برنامه‌ای، بازیابی وضعیت پس از چرخش صفحه یا Deep Link — واکنش نشان می‌دهد. این SideEffect را به یک مکانیسم همگام‌سازی جهانی، مستقل از منبع تغییر تبدیل می‌کند.

طبق Firebase Best Practices (Google, 2025)، ارسال رویدادهای تحلیلی از طریق SideEffect تصویر کامل‌تری از مسیر کاربر ارائه می‌دهد، زیرا همه تغییرات وضعیت از جمله آنهایی که بدون اقدام مستقیم کاربر رخ می‌دهند را ثبت می‌کند. با این حال، مهم است که زیاده‌روی نکنید: هر رویداد در Analytics یک درخواست شبکه است، بنابراین برای وضعیت‌هایی که مکرراً تغییر می‌کنند (موقعیت اسکرول، مختصات انگشت) SideEffect مناسب نیست — از debounce استفاده کنید یا رویدادها را فقط در تغییرات قابل توجه ارسال کنید.

kotlin
@Composable
fun ProductScreen(selectedTab: ProductTab, productId: String) {
    val firebaseAnalytics = remember { FirebaseAnalytics.getInstance(LocalContext.current) }
    
    SideEffect {
        val params = Bundle().apply {
            putString(FirebaseAnalytics.Param.CONTENT_TYPE, selectedTab.name)
            putString(FirebaseAnalytics.Param.ITEM_ID, productId)
        }
        firebaseAnalytics.logEvent(        FirebaseAnalytics.Event.VIEW_ITEM, params)
    }
    
    // UI با TabRow و برگه انتخاب شده
}

SideEffect در مقابل LaunchedEffect: چه زمانی از کدام استفاده کنیم

انتخاب بین SideEffect و LaunchedEffect به دو عامل بستگی دارد: آیا نیاز به ناهمزمانی دارید و آیا نیاز به مدیریت با کلید دارید. SideEffect همزمان است و در هر بازترکیب اجرا می‌شود. LaunchedEffect ناهمزمان (کروتین) است و فقط هنگام تغییر کلید اجرا می‌شود، نه در هر بازترکیب.

اگر نیاز به اجرای یک اقدام در هر تغییر UI دارید — از SideEffect استفاده کنید. اگر نیاز به اجرای یک اقدام یک بار هنگام ظاهر شدن صفحه یا هنگام تغییر یک پارامتر خاص دارید — از LaunchedEffect با کلیدها استفاده کنید. اگر عملیات ناهمزمان (بارگذاری داده، تأخیر، کار با Flow) لازم است — فقط LaunchedEffect، زیرا SideEffect از توابع suspend پشتیبانی نمی‌کند.

ویژگیSideEffectLaunchedEffect
اجرادر هر بازترکیبهنگام تغییر کلیدها
ناهمزمانیهمزمانکروتین
کلیدهاندارددارد (vararg)
پاک‌سازینداردلغو خودکار کروتین
کاربرد معمولCallbackها، Analytics، همگام‌سازی Viewبارگذاری، اشتراک Flow، تایمرها

در عمل 70٪ موارد استفاده از side effects توسط LaunchedEffect (عملیات ناهمزمان، بارگذاری داده)، 20٪ توسط DisposableEffect (منابع با پاک‌سازی) و فقط 10٪ توسط SideEffect (همگام‌سازی توابع callback) پوشش داده می‌شود. SideEffect یک ابزار تخصصی برای طیف محدودی از وظایف است، اما در این وظایف غیرقابل جایگزین است.

خطاهای رایج با SideEffect

خطای اصلی — تغییر وضعیت Compose درون SideEffect. اگرچه SideEffect مستقیماً باعث حلقه بی‌نهایت نمی‌شود (زیرا پس از فاز ترکیب اجرا می‌شود)، می‌تواند باعث بازترکیب‌های اضافی شود. اگر وضعیت (mutableStateOf) درون SideEffect تغییر کند، این کار باعث ایجاد بازترکیب جدید در فریم بعدی می‌شود که دوباره SideEffect را اجرا می‌کند — و این تا پایدارسازی ادامه می‌یابد. این یک حلقه بی‌نهایت نیست، اما کار اضافی برای فریم‌ورک است.

خطای دوم — انجام محاسبات سنگین درون SideEffect. از آنجایی که SideEffect در هر بازترکیب فراخوانی می‌شود و بازترکیب‌ها می‌توانند ده‌ها بار در ثانیه (در هنگام انیمیشن‌ها، اسکرول) رخ دهند، هر کد سنگین درون SideEffect منجر به افت فریم می‌شود. عملیات سنگین را به خارج از ترکیب — به کروتین (LaunchedEffect) منتقل کنید یا از طریق derivedStateOf / remember محاسبه کنید.

خطای سوم — تلاش برای استفاده از SideEffect برای کد ناهمزمان. SideEffect یک تابع suspend نیست، بنابراین delay()، await()، collect() و سایر عملیات کروتین درون آن کامپایل نمی‌شوند. اگر نیاز به اجرای یک اقدام ناهمزمان پس از بازترکیب دارید، از snapshotFlow { ... } در ترکیب با LaunchedEffect استفاده کنید یا کروتین را از طریق rememberCoroutineScope راه‌اندازی کنید.

سوالات متداول

آیا SideEffect در اولین ترکیب اجرا می‌شود؟

بله، SideEffect در هر ترکیب موفق، از جمله اولین — زمانی که کامپوننت تازه روی صفحه ظاهر می‌شود — اجرا می‌شود. این آن را از LaunchedEffect(Unit) متمایز می‌کند که آن نیز یک بار در اولین ترکیب اجرا می‌شود، اما در بازترکیب‌های بعدی اجرا نمی‌شود (اگر کلید تغییر نکرده باشد).

آیا SideEffect می‌تواند باعث حلقه بی‌نهایت شود؟

خیر، SideEffect پس از فاز ترکیب اجرا می‌شود — تغییرات انجام شده درون آن فقط در فریم بعدی اعمال می‌شوند که از ایجاد حلقه جلوگیری می‌کند. با این حال، تغییر مکرر وضعیت درون SideEffect می‌تواند باعث بهمنی از بازترکیب‌ها شده و عملکرد را کاهش دهد. وضعیت را درون SideEffect فقط در صورت نیاز واقعی تغییر دهید.

تفاوت بین SideEffect و snapshotFlow چیست؟

SideEffect به صورت همزمان در هر بازترکیب اجرا می‌شود. snapshotFlow یک Flow از وضعیت Compose ایجاد می‌کند و می‌تواند با collectLatest در LaunchedEffect برای پردازش واکنشی تغییرات استفاده شود. snapshotFlow برای مواردی مناسب است که نیاز به واکنش به تغییرات با debounce، filter یا distinctUntilChanged دارید — که در SideEffect همزمان امکان‌پذیر نیست.

اگر SideEffect بیش از حد مکرر اجرا شود، چگونه آن را دیباگ کنیم؟

از Android Studio Compose Modifier Debugger استفاده کنید یا لاگ‌گذاری با نام کامپوننت و فرکانس فراخوانی اضافه کنید. اگر SideEffect بیش از حد انتظار اجرا می‌شود، بررسی کنید که آیا وضعیت کامپوننت والد بدون نیاز تغییر نمی‌کند. بهینه‌سازی: بخش‌های پایدار UI را به توابع composable جداگانه با حاشیه‌نویسی unstable استخراج کنید تا تعداد بازترکیب‌ها کاهش یابد.

آیا می‌توان SideEffect را با DisposableEffect ترکیب کرد؟

بله، می‌توان از آن‌ها در یک کامپوننت برای اهداف مختلف استفاده کرد. DisposableEffect مسئول راه‌اندازی و پاک‌سازی منبع (یک بار) است و SideEffect مسئول همگام‌سازی وضعیت فعلی با این منبع در هر بازترکیب. مثال معمول: DisposableEffect callback را از طریق API ثبت می‌کند و SideEffect داده‌های محصور شده در این callback را در هر تغییر به‌روز می‌کند.

خلاصه

  • SideEffect — API side-effect برای کد همزمان که پس از هر بازترکیب موفق در Jetpack Compose اجرا می‌شود.
  • همگام‌سازی — سناریوی اصلی: انتقال وضعیت Compose به سیستم‌های خارجی (Google Maps, WebView, ViewPager, Analytics SDK).
  • Callbackها — SideEffect به‌روزرسانی توابع callback که وضعیت فعلی را محصور می‌کنند در هر به‌روزرسانی UI تضمین می‌کند.
  • بدون حلقه — پس از فاز ترکیب اجرا می‌شود، بنابراین تغییر وضعیت درون SideEffect باعث بازترکیب بی‌نهایت نمی‌شود.
  • محدودیت‌ها — از کلیدها، ناهمزمانی و بلوک پاک‌سازی پشتیبانی نمی‌کند؛ برای این وظایف از LaunchedEffect یا DisposableEffect استفاده کنید.
  • عملکرد — از محاسبات سنگین درون SideEffect خودداری کنید زیرا در هر بازترکیب (تا ۶۰ بار در ثانیه) اجرا می‌شود.
  • دیباگ — فرکانس فراخوانی را از طریق Compose Debugger کنترل کنید و با remember برای فیلتر کردن بازترکیب‌های غیرضروری بهینه‌سازی کنید.

ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد

IT Sectr از سال 2017 برنامه‌های iOS و Android را برای استارتاپ‌ها و کسب‌وکارها ایجاد می‌کند. ما به شما مشاوره می‌دهیم و بهترین راه‌حل را پیشنهاد خواهیم کرد.

بحث درباره پروژه

همچنین بخوانید