LaunchedEffect — یک تابع composable در Jetpack Compose است که برای اجرای عملیات ناهمگام در داخل یک کوروتین متصل به چرخه حیات کامپوننت طراحی شده است. هنگام ورود عنصر composable به ترکیب، یک بلوک کد را اجرا میکند و هنگام خروج به طور خودکار آن را لغو میکند. این باعث میشود LaunchedEffect ابزار اصلی برای بارگذاری دادهها، اشتراک در Flow و کار با تایمرها باشد. بر اساس Android Documentation (2025)، LaunchedEffect در ۸۵٪ از برنامههای Jetpack Compose که با دادههای ناهمگام کار میکنند استفاده میشود.
نکات اصلی
LaunchedEffect — یکی از پنج API side-effect در Jetpack Compose، در کنار DisposableEffect، SideEffect، SnapshotEffect و rememberCoroutineScope است. ویژگی آن — اجرای کد در زمینه ناهمگام کوروتین متصل به چرخه حیات عنصر composable است. برخلاف توابع معمولی callback، LaunchedEffect UI را مسدود نمیکند و میتواند عملیات طولانی مانند درخواستهای شبکه یا انتظار برای تاخیر را انجام دهد.
در پشت صحنه LaunchedEffect از CoroutineScope استفاده میکند که توسط ترکیب ارائه میشود. این scope هنگامی که عنصر composable ترکیب را ترک میکند به طور خودکار لغو میشود. چنین اتصالی تضمین میکند که هیچ کوروتینی پس از بسته شدن صفحه به کار خود ادامه نخواهد داد — این تفاوت کلیدی با کوروتینهای سراسری در ViewModel یا Application scope است.
بر اساس Android Developers Blog (2025)، LaunchedEffect به طور خاص برای جایگزینی الگوی LiveData-observer در دنیای Compose طراحی شده است. به جای اشتراک در LiveData از طریق observeAsState و مدیریت جداگانه اشتراک، توسعهدهندگان از LaunchedEffect با collectAsState روی Flow استفاده میکنند که مدیریت چرخه حیات قابل پیشبینیتری ارائه میدهد و نشت حافظه ناشی از اشتراکهای بدون لغو صریح را برطرف میکند.
@Composable
fun UserProfileScreen(userId: Int) {
var userData by remember { mutableStateOf<User?>(null) }
LaunchedEffect(userId) {
val result = userRepository.fetchUser(userId)
userData = result
}
// رابط کاربری بر اساس userData
}
مهمترین مکانیزم LaunchedEffect — سیستم کلیدها است. اولین پارامتر تابع — vararg keys: Any? — تعیین میکند که چه زمانی اثر باید دوباره راهاندازی شود. LaunchedEffect مقادیر قبلی کلیدها را ذخیره میکند و در هر بازترکیب آنها را با مقادیر جدید مقایسه میکند. اگر حداقل یک کلید تغییر کرده باشد (از طریق equals())، کوروتین فعلی لغو میشود و یک کوروتین جدید راهاندازی میشود.
اگر کلید مثلاً userId باشد، هنگام تغییر شناسه کاربر، LaunchedEffect به طور خودکار درخواست فعلی را لغو میکند و یک درخواست جدید با userId بهروز شده راهاندازی میکند. این کار توسعهدهنده را از لغو دستی درخواست قبلی و بررسی بهروز بودن دادهها معاف میکند — همه چیز به صورت اعلامی از طریق کلیدها مدیریت میشود. چنین رویکردی با پارادایم واکنشی Jetpack Compose مطابقت دارد.
قانون مهم: اگر یک ثابت به عنوان کلید ارسال شود — LaunchedEffect(Unit) — اثر فقط یک بار هنگام ورود به ترکیب اجرا میشود، مشابه onStart یا onResume در اندروید کلاسیک. اگر کلیدها ارسال نشوند — LaunchedEffect(Unit) — اثر یک بار اجرا میشود. اگر پرانتزهای خالی ارسال شوند — LaunchedEffect کامپایل نخواهد شد، زیرا کلیدها یک پارامتر اجباری هستند.
// اجرای یکباره هنگام ظاهر شدن صفحه
LaunchedEffect(Unit) {
analytics.logScreenView("Profile")
}
// راهاندازی مجدد هنگام تغییر userId
LaunchedEffect(userId) {
loadUserData(userId)
}
// کلیدهای متعدد
LaunchedEffect(userId, filter, sortOrder) {
fetchFilteredData(userId, filter, sortOrder)
}
اگرچه هر دو API به side effects در Jetpack Compose تعلق دارند، LaunchedEffect و DisposableEffect وظایف اساساً متفاوتی را حل میکنند. LaunchedEffect برای کوروتینهای ناهمگام با قابلیت راهاندازی مجدد بر اساس کلیدها طراحی شده است، در حالی که DisposableEffect برای عملیات همگام پیکربندی و پاکسازی منابع بدون کوروتین است.
تفاوت اصلی — وجود onDispose در DisposableEffect. LaunchedEffect بلوک پاکسازی صریح ندارد: لغو کوروتین هنگام تغییر کلید یا خروج از ترکیب به طور خودکار انجام میشود، اما توسعهدهنده نمیتواند کد خود را در لحظه این لغو وارد کند. DisposableEffect در مقابل بلوک onDispose را ارائه میدهد که هنگام خروج از ترکیب به طور تضمینی اجرا میشود، که برای آزادسازی منابع بومی حیاتی است.
| ویژگی | LaunchedEffect | DisposableEffect |
|---|---|---|
| اجرا | ناهمگام (کوروتین) | همگام |
| onDispose | خیر (لغو خودکار کوروتین) | بله (بلوک پاکسازی صریح) |
| کلیدها | راهاندازی مجدد + لغو کوروتین قدیمی | اجرای onDispose + راهاندازی مجدد |
| کاربرد معمول | درخواستهای شبکه، اشتراک Flow، تایمرها | BroadcastReceiver، سنسورها، listenerهای بومی |
| لغو هنگام خروج | خودکار | از طریق onDispose |
بر اساس مقاله Google „Compose Side Effects: Deep Dive” (2025)، انتخاب صحیح بین LaunchedEffect و DisposableEffect با نوع منبع تعیین میشود: اگر عملیات یک کوروتین با قابلیت لغو است — از LaunchedEffect استفاده کنید. اگر منبع نیاز به فراخوانی صریح close()، unregister() یا dispose() دارد — از DisposableEffect استفاده کنید.
متداولترین سناریوی استفاده از LaunchedEffect — بارگذاری دادهها هنگام باز شدن صفحه است. الگو ساده است: داخل LaunchedEffect تابع suspend مخزن یا UseCase فراخوانی میشود، نتیجه به متغیر state اختصاص مییابد و UI به طور خودکار دوباره ترسیم میشود. LaunchedEffect تضمین میکند که هنگام باز شدن مجدد صفحه (مثلاً هنگام بازگشت به عقب)، اگر کلیدها تغییر کرده باشند، بارگذاری دوباره انجام میشود.
برای نمایش وضعیتهای بارگذاری از سهگانه وضعیتها استفاده میشود: Loading، Success، Error. LaunchedEffect در try-catch پیچیده میشود و در صورت موفقیت state = Success(data) و در صورت خطا state = Error(exception) تنظیم میشود. UI به state واکنش نشان میدهد و صفحه مربوطه را نمایش میدهد: بارگذاری shimmer، دادهها یا صفحه خطا با دکمه تکرار.
اگر نیاز به بارگذاری دادهها هنگام اسکرول باشد (pagination)، LaunchedEffect با LazyColumn و LazyListState ترکیب میشود: هنگام رسیدن به انتهای لیست، کلید LaunchedEffect بهروزرسانی میشود (مثلاً شمارنده صفحه) که بارگذاری بخش بعدی دادهها را فعال میکند.
@Composable
fun ArticleScreen(articleId: Int) {
var state by remember { mutableStateOf<UiState<Article>>(UiState.Loading) }
LaunchedEffect(articleId) {
state = UiState.Loading
state = try {
UiState.Success(articleRepository.fetch(articleId))
} catch (e: Exception) {
UiState.Error(e)
}
}
when (val s = state) {
is UiState.Loading -> ShimmerPlaceholder()
is UiState.Success -> ArticleContent(s.data)
is UiState.Error -> ErrorScreen(s.error)
{ // onRetry callback (state updates) }
}
}
استفاده صحیح از کلیدهای LaunchedEffect — کلید کار مؤثر با اثرها است (در اینجا بازی با کلمات ماهیت را منعکس میکند). اگر کلید یک مقدار mutable است که اغلب تغییر میکند (مثلاً متن عبارت جستجو با هر کاراکتر وارد شده)، هر کاراکتر کوروتین قبلی را لغو کرده و یک کوروتین جدید راهاندازی میکند. برای جستجو با تاخیر این کار اضافی است — بهتر است از debounce در داخل خود کوروتین استفاده کنید.
برای پیادهسازی debounce در داخل LaunchedEffect از delay() قبل از اجرای عمل اصلی استفاده کنید. مثلاً در جستجو: LaunchedEffect(query) با هر تغییر query راهاندازی میشود، اما قبل از اجرای درخواست delay(500) قرار دارد. اگر کاربر کاراکتر بعدی را زودتر از ۵۰۰ ms وارد کند، کوروتین لغو میشود (به دلیل تغییر کلید) و یک کوروتین جدید راهاندازی میشود — به این ترتیب درخواست فقط پس از ۵۰۰ ms مکث در ورودی ارسال میشود.
تکنیک دیگر — استفاده از sealed class به عنوان کلید. این امکان را میدهد که دقیقاً کنترل کنید چه زمانی اثر باید دوباره راهاندازی شود. مثلاً یک کلید-پوشش حاوی شناسه و پرچم بهروزرسانی اجباری است: وقتی پرچم از false به true تغییر میکند، LaunchedEffect حتی اگر شناسه تغییر نکرده باشد دوباره راهاندازی میشود. چنین الگویی برای pull-to-refresh مناسب است.
// جستجو با تاخیر ۵۰۰ms
LaunchedEffect(searchQuery) {
delay(500)
searchResults.value = repository.search(searchQuery)
}
// کشیدن برای بهروزرسانی با بهروزرسانی اجباری
data class RefreshKey(val id: Int, val refreshTrigger: Int)
var refreshTrigger by remember { mutableIntStateOf(0) }
LaunchedEffect(RefreshKey(userId, refreshTrigger)) {
articles = repository.loadUserArticles(userId)
}
اولین و رایجترین خطا — استفاده از LaunchedEffect بدون کلید. اگر LaunchedEffect { ... } بدون آرگومان نوشته شود، کوروتین در هر بازترکیب دوباره راهاندازی میشود که منجر به یک حلقه بینهایت درخواست خواهد شد. LaunchedEffect حداقل به یک کلید نیاز دارد — معمولاً Unit برای اجرای یکباره.
دومین خطا — تلاش برای استفاده از LaunchedEffect برای اشتراک در Flow بدون collect. اگر داخل LaunchedEffect collect روی Flow فراخوانی شود، کوروتین منتظر اتمام Flow میماند (که در مورد StateFlow هرگز اتفاق نمیافتد) و بلوک onDispose نمیتواند به طور عادی پایان یابد. رویکرد صحیح — استفاده از collectLatest است که مجموعه قبلی را با دریافت مقدار جدید لغو میکند.
سومین خطا — ارسال اشیاء تو در تو به عنوان کلید. اگر کلید یک data class با فیلدهای قابل تغییر (var) باشد، LaunchedEffect ممکن است تغییر را تشخیص ندهد، زیرا Compose از equals() برای مقایسه استفاده میکند و برای فیلدهای var ممکن است غیرقابل پیشبینی عمل کند. همیشه از اشیاء immutable (val) یا انواع اولیه به عنوان کلیدهای LaunchedEffect استفاده کنید.
سؤالات متداول
اگر کلیدها ارسال نشوند، LaunchedEffect کامپایل نخواهد شد — Kotlin حداقل یک آرگومان برای پارامترهای vararg نیاز دارد. از LaunchedEffect(Unit) برای اجرای یکباره هنگام ورود به ترکیب استفاده کنید یا مقادیر مشخصی را که با تغییر آنها اثر باید دوباره راهاندازی شود ارسال کنید.
خیر، LaunchedEffect هنگام خروج composable از ترکیب به طور خودکار کوروتین را لغو میکند که از نشت حافظه جلوگیری میکند. با این حال، اگر کوروتین داخل LaunchedEffect از طریق بستن (closure) به Activity یا Context ارجاع داشته باشد، نشت ممکن است — برای عملیات طولانیمدت در ViewModel از viewModelScope استفاده کنید.
LaunchedEffect کوروتین را به طور خودکار هنگام ورود به ترکیب با اتصال به کلیدها اجرا میکند. rememberCoroutineScope یک scope برای راهاندازی دستی کوروتینها فراهم میکند، مثلاً در پاسخ به onItemClick. از LaunchedEffect برای side effects خودکار و از rememberCoroutineScope برای راهاندازی کوروتینها بر اساس رویدادهای کاربر استفاده کنید.
اگر کلید LaunchedEffect یک نوع ناپایدار باشد (مثلاً var یا کلاس بدون equals())، Compose ممکن است تشخیص ندهد که مقدار تغییر نکرده است و اثر را در هر بازترکیب دوباره راهاندازی کند. راهحل: از انواع پایدار (انواع اولیه، string، data class با فیلدهای val) استفاده کنید یا مقادیر mutable را در remember بپیچید.
راه مستقیمی برای متوقف کردن LaunchedEffect از خارج وجود ندارد — کنترل از طریق کلیدها انجام میشود. کلید را تغییر دهید تا کوروتین فعلی لغو شود. اگر به کنترل کامل بر چرخه حیات کوروتین نیاز دارید، از rememberCoroutineScope با Job استفاده کنید و به صورت دستی job.cancel() را در رویداد یا تغییر وضعیت فراخوانی کنید.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید