آموزش کامل Side Effect در Jetpack Compose

آموزش کامل Side Effect در Jetpack Compose
در این پست می‌خوانید:

در این مبحث به معرفی Side Effect ها میپردازم و نحوه اثر گذاری این ها را در کد نویسی بررسی میکنم و در اخر به راهکاری برای این side Effect ها پیشنهاد  میکنیم ،

در ادامه این توضیحات ، قصد دارم از launchEffect به عنوان میزان استفاده کنم ، و در هر تعریف با این هندلر مقایسه کنم که مطلب را بهتر انتقال بدم .

Side Effect و کنترل کنند های آن

Side effect  چیست ؟

Side Effect به هر گونه تغییر در state برنامه که خارج از محدوده تابع Composable رخ میده، گفته می‌شه. این تغییرات می‌تونه شامل موارد زیر باشه:

  • تغییر در یک متغیر سراسری (Global Variable)
  • نوشتن در دیتابیس
  • انجام درخواست شبکه (Network Request)
  • تغییر در UI خارج از محدوده Composable فعلی
  • ذخیره اطلاعات در SharedPreferences
  • Log کردن

ساده بخوام بگم:
«برنامه‌ی ما یه داده‌ای داره و می‌خواد اون رو نمایش بده، مثلاً “نمایش یک نام”.

حالا اگه این نام تغییر کنه، یعنی داده‌مون تغییر کرده. اینجاست که چیزی به اسم Side Effect یا اثر جانبی وارد ماجرا میشه.

اگه بخوام باز هم ساده‌تر بگم:
وقتی داده‌ای تغییر می‌کنه، تابع‌هایی که به اون داده وابسته هستن باید خودشون رو با داده‌ی جدید هماهنگ کنن. این هماهنگ‌سازی گاهی فقط روی UI (مثلاً آپدیت شدن متن روی صفحه) تأثیر می‌ذاره. اما گاهی بخش‌هایی از برنامه که مستقیماً ربطی به UI ندارن هم تحت تأثیر قرار می‌گیرن — مثل ذخیره‌سازی در دیتابیس یا ارسال لاگ به سرور.

اون بخش‌هایی که خارج از UI تغییر می‌کنن، همون Side Effect‌هایی هستن که توی برنامه اتفاق می‌افتن»

چرا Composable ها باید بدون Side Effect باشند؟ (Pure Functions)

تابع‌های Composable باید “Pure” (خالص) باشن، یعنی:

  1. Deterministic: با ورودی یکسان، همیشه خروجی یکسان تولید کنن.
  2. No Side Effects: هیچ تغییری در state برنامه خارج از محدوده خودشون ایجاد نکنن.

حالا، چرا این “خالص بودن” مهمه؟

  • پیش‌بینی‌پذیری (Predictability): وقتی Composable شما Pure باشه، می‌تونید مطمئن باشید که با یک سری ورودی مشخص، همیشه همون خروجی رو خواهید داشت. این باعث میشه که بتونید رفتار UI رو به راحتی پیش‌بینی کنید و از باگ‌های غیرمنتظره جلوگیری کنید.
  • قابلیت تست (Testability): تست کردن Composable های Pure خیلی آسون‌تره. چون فقط کافیه ورودی‌ها رو به تابع بدید و خروجی رو بررسی کنید. نیازی نیست نگران Side Effect ها و stateهای خارجی باشید.
  • بهینه‌سازی (Optimization): کامپایلر Compose می‌تونه Composable های Pure رو به شکل بهینه‌تری کامپایل کنه. مثلاً، اگه ورودی‌های یک Composable تغییر نکرده باشن، می‌تونه از Recompose اون جلوگیری کنه.
  • Concurrency (همزمانی): کامپایلر Compose می‌تونه Composable ها رو به صورت موازی اجرا کنه. اما این کار فقط زمانی امکان‌پذیره که Composable ها Pure باشن و هیچ Side Effectای نداشته باشن.

مشکل Side Effect ها در Compose:

وقتی یک Composable ساید افکت Side Effect داشته باشه، این مشکلات به وجود میاد:

  • غیرقابل پیش‌بینی شدن: ممکنه Composable شما در شرایط مختلف، رفتارهای متفاوتی از خودش نشون بده.
  • مشکل در تست: تست کردن Composable های دارای Side Effect سخته، چون باید stateهای خارجی رو هم در نظر بگیرید.
  • مشکل درRecompose : کامپوز ممکنه Composable شما رو بیشتر از حد لازم Recompose کنه، چون نمی‌تونه به درستی تشخیص بده که آیا تغییرات واقعاً ضروری هستن یا نه.
  • باگ‌های غیرمنتظره: Side Effect ها می‌تونن باعث باگ‌های عجیبی بشن که پیدا کردنشون خیلی سخته. « به عنوان مثال شما داری رنگ ، یک ویو رو  عوض میکنی ، اما داده ای از اینترنت در همون لحظه دریافت میکنی و محاسبات شما رو تحت تاثیر قرار میده »

خلاصه:

Composable های Pure، اساس یک UI قابل پیش‌بینی، قابل تست و بهینه رو تشکیل میدن. Side Effect ها این ویژگی‌ها رو از بین می‌برن. به همین دلیل، باید از انجام Side Effect ها در Composable ها خودداری کنیم ، « خب ، حالا راه حل چیه ؟ بعد از چند تا مثال به راه حل ، می رسیم .»

تعاریف و اصطلاح های برای بهتر فهمیدن مفهوم SideEffect

در ادامه اصطلاح های معرفی میکنم که در ظاهر هیچ ربطی نداشته باشند امابرای بیان مفهوم side Effect ضروروی هستند .

توابع Pure : بدون اثرات جانبی Side Effect

در ادامه مفهوم تابع های Pure رو با مثال توضیح میدم .

مثال اول: Composable Pure (بدون Side Effect)

فرض کنید یک Composable داریم که اسم یک شخص رو به عنوان ورودی می‌گیره و یه متن خوشامدگویی رو نمایش میده:

@Composable
fun Greeting(name: String): String {
    return "Hello, $name!"
}

این  یک کامپوزیبل Pure هست، چون:

  1. Deterministic: اگه اسم “Ali” رو به عنوان ورودی بدیم، همیشه خروجی “Hello, Ali!” رو خواهیم داشت.
  2. No Side Effects: این تابع هیچ تغییری در state برنامه خارج از خودش ایجاد نمی‌کنه. فقط یه رشته رو برمی‌گردونه.

Composable Pure در واقع این تابع به ما میگه در صورت ورودی خروجی پیش بینی پذیری خواهی داشت

مثال دوم: Composable No Pure (دارای Side Effect)

حالا فرض کنید Composable دیگه‌ای داریم که یک شمارنده رو نمایش میده و هر بار که Recompose میشه، مقدار شمارنده رو در یک فایل ذخیره می‌کنه (منظور از دخیره کردن ، یعنی داخل counter می مونه نه این که داخل دیتا بیس ذخیره میشه )

var counter = 0

@Composable
fun CounterComposable() {

     counter++

این Composable No Pure هست، چون:

  1. هر بار که Recompose میشه، یه واحد به  counter اضافه میشه و خارج از محدوده ای تابع  مقدار نگه داری میشه این یه Side Effect هست، چون Composable داره state (اون داده ای که در حال تفییره )برنامه رو خارج از محدوده خودش تغییر میده (نوشتن در فایل).

یه مثال دیگه :

var nameGlobal = "ali"

@Composable
fun NoPureComposable(name:String) {
    nameGlobal = name
}

این Composable No Pure هست، چون:

  1. Deterministic: اگر مقدار متغییر nameGlobal را قبل از فراخوانی Composable چاپ کنیم برابر ali است ولی بلافاصله پس از فراخوانی مقدار آن تغییر میکند.
  2. Side Effects: این تابع state برنامه رو خارج از محدوده خودش تغییر میده (تغییر مقدار nameGlobal).

خومونی تر بگم :

«از این مثال‌ها فهمیدیم که وقتی توابع توی Jetpack Compose دوباره Recompose میشن، اگه داده‌هایی که داخلشون داریم (همون State خودمون) خارج از محدوده اون تابع تغییر کنن، یه اتفاقی میفته به اسم Side Effect. در واقع، یه جورایی یه اثر جانبی از Recompose شدن تابع به وجود میاد. یعنی ما میخواهیم ui رو عوض کنیم اما از این ور داده هم عوض میشه .

ما اینجا یه عدد یا اسم رو مثال زدیم، اما فکر کن اگه توی Recompose شدن یه تابع، یه داده‌ای توی دیتابیس ذخیره بشه چی میشه؟ ممکنه یه تابع توی هر فریم چندین بار Recompose بشه. اگه این Side Effect رو مدیریت نکنیم، اون داده ممکنه چند بار توی دیتابیس ذخیره بشه و حسابی به دردسر بیفتیم!

یه مثال دیگه هم داریم: فرض کن توی تابعی که Recompose میشه، داریم یه داده‌ای رو از یه API می‌گیریم (Fetch API) اگه Side Effect رو مدیریت نکنیم، ممکنه توی هر Recompose، تابع چند بار درخواست بزنه به اون API و داده دریافت کنه ، اینجوری منابع سیستم‌مون مثل رم و… با محدودیت مواجه میشن. چرا؟ چون Recompose کردن مربوط به UI هست، ولی درخواست زدن به API باعث میشه UI ما فریز بشه و کارایی‌ش بیاد پایین.»

 

 

Effect ها در Compose چیست؟

! Effectها کارهای جانبی که در Compose وجود دارن و مستقیماً رابط کاربری (UI) رو تولید نمی‌کنند.

کارشون چیه؟ انجام دادن کارهایی که در کنار تولید UI لازمه.

تصور کن داری یه اتاق رو مرتب می‌کنی( تولید UI) ، در کنار این کار، ممکنه نیاز داشته باشی جاروبرقی بکشی، یه لامپ سوخته رو عوض کنی (اینا میشن Effectها) ،  این کارها مستقیماً بخشی از مرتب کردن اتاق نیستن، اما برای اینکه اتاق کاملاً آماده بشه، باید انجام بشن.

پس، Effectها کارهای جانبی هستن که باید در واکنش به اتفاقاتی که در UI می‌افته، انجام بشن. مثلاً:

  • ذخیره اطلاعات در پایگاه داده: وقتی کاربر روی دکمه “ذخیره” کلیک می‌کنه (یه رویداد UI) باید اطلاعات رو ذخیره کنی.
  • نمایش یه پیام موقت: بعد از ذخیره، یه پیام “اطلاعات ذخیره شد” نشون بدی.
  • درخواست اطلاعات از اینترنت: وقتی وارد یه صفحه میشی، باید اطلاعات رو از سرور بگیری و نشون بدی.
  • تنظیمات گوشی (مثل روشن و خاموش کردن بلوتوث): برنامه‌ات به بلوتوث نیاز داره، باید چک کنی و اگه خاموشه، یه درخواست برای روشن کردنش بدی.

حالا ، مدیریت کردن این Effect  که در کنار تولید UI وجود دارند یا به عبارتی Side Effect ، موضوع اصلی بحث ما است . الان که این موضوع رو شناختیم راه حل چیه ؟

چرا Side Effectها (اثرات جانبی) بیرون از محدوده تابع UI رخ می‌دهند؟

این یه نکته خیلی مهمه! توابع کامپوزبل که UI رو تولید می‌کنن، باید یه سری ویژگی‌های خاص داشته باشن تا Compose بتونه درست کار کنه:

  1. بدون Side Effect باشن (Pure Functions): یعنی هر وقت اون‌ها رو صدا می‌زنی، با ورودی‌های یکسان، همیشه خروجی یکسان (UI یکسان) رو بدن و هیچ کار دیگه‌ای انجام ندن. مثل یه تابع ریاضی f(x) = x + 2 که همیشه با x=3، عدد 5 رو میده و مثلاً اطلاعاتی رو پاک نمی‌کنه.
  2. سریع باشن: چون Compose ممکنه بارها و بارها این توابع رو برای بازسازی UI صدا بزنه، باید خیلی سریع باشن تا برنامه کند نشه.

حالا فرض کن توی تابع MyButton() که یه دکمه رو می‌سازه، همزمان اطلاعات کاربر رو هم توی دیتابیس ذخیره کنی. چه اتفاقی میفته؟

  • Compose ممکنه هر لحظه به دلایلی (مثلاً تغییر رنگ دکمه) MyButton() رو دوباره اجرا کنه. هر بار که اجرا بشه، اطلاعات دوباره توی دیتابیس ذخیره میشه! این یه مشکل بزرگ و ناخواسته است.
  • ذخیره اطلاعات توی دیتابیس (یا درخواست شبکه و …) یه کار زمان‌بره و ممکنه UI رو کند کنه یا حتی باعث فریز شدنش بشه.

پس، برای اینکه این مشکلات پیش نیاد و توابع UI پاک (Pure) و سریع بمونن، کارهایی مثل ذخیره اطلاعات، درخواست شبکه، یا نشون دادن پیام که اثرات جانبی دارن، باید جدا و خارج از این توابع UI مدیریت بشن. اینجا است که Effectها وارد عمل میشن. اون‌ها به Compose میگن “این کار رو انجام بده، اما نه الان، و نه مستقیماً توی فرایند ساخت UI، بلکه در واکنش به این اتفاق و به روش صحیح خودش.”

UI واکنش‌گرا (Reactive UI) ذاتاً ناهمگام (Asynchronous) است، یعنی چی؟

این جمله کمی فنی به نظر میاد، اما مفهومش خیلی ساده است:

  • UI واکنش‌گرا: یعنی رابط کاربری شما واکنش نشون میده. به چی واکنش نشون میده؟ به تغییرات. مثلاً کاربر روی یه دکمه کلیک می‌کنه، یه اطلاعات جدید از اینترنت میاد، یا یه داده توی برنامه تغییر می‌کنه. UI شما باید فوراً خودش رو با این تغییرات تطبیق بده و به‌روز بشه.
  • ناهمگام (Asynchronous): یعنی کارها پشت سر هم و به ترتیب دقیق انجام نمیشن. تصور کن داری یه کیک می‌پزی. وقتی کیک تو فر هست، منتظر نمیشی تا کیک بپزه و بعد بری سراغ بقیه کارها. در همون حین که کیک داره می‌پزه، میتونی بری ظرف‌ها رو بشوری یا قهوه درست کنی. این یعنی کارها “همزمان” و “در کنار هم” پیش میرن، نه “پشت سر هم و منتظر همدیگه”.

حالا ارتباطش با UI چیه؟

اگه UI شما همگام (Synchronous) باشه، یعنی وقتی شما روی یه دکمه کلیک می‌کنی، برنامه منتظر میمونه تا کل کارهایی که بعد از اون کلیک باید انجام بشه (مثلاً یه درخواست شبکه طولانی) تموم بشه، بعد به بقیه کارها برسه. نتیجه‌اش میشه یه UI که فریز می‌کنه و جواب نمیده (یخ می‌زنه). کاربر دکمه رو میزنه، ولی هیچ اتفاقی نمی‌افته تا اون کار سنگین تموم بشه.

اما در UI واکنش‌گرای ناهمگام، وقتی روی دکمه کلیک می‌کنی و یه کار سنگین (مثل درخواست شبکه) شروع میشه، UI یخ نمی‌زنه. اون کار سنگین در پس‌زمینه (در یه کوروتین یا ترد جداگانه) انجام میشه و UI شما همچنان پاسخگو می‌مونه. وقتی نتیجه اون کار سنگین آماده شد، UI شما به اون نتیجه واکنش نشون میده و خودش رو به‌روز می‌کنه.

این ماهیت ناهمگام بودن برای اینکه برنامه‌های اندروید (و کلاً هر برنامه با UI) روان و پاسخگو باشن، حیاتیه.

اصطلاح “Use Case” چیه؟

در متون و مقاله های انگلیسی ما به این اصطلاح use case زیاد بر میخوریم مانند متن زیر که در این آدرس هست :

State and effect use cases

As covered in the Thinking in Compose documentation, composables should be side-effect free. When you need to make changes to the state of the app (as described in the Managing state documentation doc), you should use the Effect APIs so that those side effects are executed in a predictable manner.

Use Case (یوز کیس) در برنامه‌نویسی، و به خصوص در طراحی نرم‌افزار، به معنی یک سناریوی خاص از چگونگی استفاده کاربر از سیستم برای رسیدن به یک هدف معین است.

بیا با مثال توضیح بدم:

فرض کن داری یک اپلیکیشن خرید و فروش سهام می‌نویسی (مثلاً به بازار مالی علاقه داری🤑):

  • یک Use Case می‌تونه این باشه: “کاربر قیمت لحظه‌ای سهام X را مشاهده می‌کند.”
  • یک Use Case دیگه: “کاربر یک سفارش خرید برای سهام Y ثبت می‌کند.”
  • یک Use Case دیگه: “سیستم به کاربر در مورد تغییرات ناگهانی قیمت سهام هشدار می‌دهد.”

پس، “Use Case” به “مورد استفاده” یا “سناریوی کاربردی” اطلاق می‌شود. در متن بالا وقتی می‌گوید “State and effect use cases” یعنی “سناریوهای استفاده از State و Effect ها” یا “موارد کاربرد State و Effect ها”. این اصطلاح به ما کمک می‌کنه تا بفهمیم هر کدوم از این ابزارها (مثل Effect ها) در چه مواقعی و برای حل چه مشکلاتی باید استفاده بشن

تا به اینجا مفاهیم مورد نیاز side Effect و مقدمه ها گفته شد ، و درک کردیم که مشکل ای که سای افکت ایجاد میکنه ، چی ممکنه باشه ، حالا در ادامه به بررسی راه حل های برای این ساید افکت میپردازیم .

بررسی و دسته‌بندی Handlerهای کنترل Side Effect و State

خب ، این اثر Effect رخ داده، Side Effect به وجود اومده ، ما میخوایم این رو کنترل کنیم یا به عبارتی Handle کنیم ،این توابع که این کار را برای ما انجام میدن Handler ها نامیده می شوند .

حالا این Handlerها به ما اجازه میدن که Side Effect ها رو در یک محیط کنترل شده و آگاه از چرخه حیات Composable کنترل کنیم .

نمونه‌هایی از این Handlerها عبارتند از:

  • LaunchedEffect
  • rememberCoroutineScope
  • produceState
  • rememberUpdatedState
  • SideEffect
  • DisposableEffect
  • DerivedStateOf

این Handler ها به دو دسته کلی تقسیم بندی میشن :

Suspended

  • LaunchedEffect
  • rememberCoroutineScope
  • produceState

Non Suspended

  • rememberUpdatedState
  • side Effect
  • DisposableEffect
  • derivedStateOf

حالا چه اهمیتی داره که این Handler ها رو suspend  یا non suspend در نظر بگیریم ؟

برای کارای اونها  ، چون وقتی ببینی که این کار، وظیفه ، سناریو شما در برنامه  تعلیق نیاز داره ، یا نداره ،  اون وقت میدونی که از کدام Handler باید استفاده کنی .

در ادامه به بررسی این Handler های میپردازیم

شکلی کلی این کنترل کنند Handler به این صورت است :

اما در داخل کد های ما اغلب به این صورت استفاده میشه :

LaunchedEffect(isLoading.value) {
        if (isLoading.value) {
            // Perform a long-running operation, such as fetching data from a network
            val newData = fetchData()
            // Update the state with the new data
            data.value = newData
            isLoading.value = false
        }
    }

هنگامی که LaunchedEffect وارد ترکیب (Composition) می‌شود، یک کوروتین (coroutine) با بلوک کدی که به عنوان پارامتر به آن داده شده است، راه‌اندازی می‌کند. اگر LaunchedEffect از ترکیب خارج شود، این کوروتین لغو خواهد شد

LaunchedEffect به صورت خلاصه پل ارتباطی بین دنیای UI کامپوزبل و دنیای ناهمگام (Asynchronous) کوروتین‌هاست.

با جزئیات بیشتر توضیح بدم:

  1. هدف اصلی:
    • اجرای توابع suspend (توابعی که می‌تونن موقتاً اجرای خودشون رو متوقف کنن و بعداً از همون نقطه ادامه بدن، بدون فریز کردن برنامه) در محدوده عمر یک کامپوزبل خاص.
    • وقتی نیاز داری در طول عمر یک Composable کاری ناهمگام انجام بدی (مثل درخواست شبکه، تأخیر دادن، انیمیشن‌های پیچیده)، LaunchedEffect ابزار اصلی شماست.
  2. ایجاد کوروتین (Coroutine):
    • وقتی LaunchedEffect وارد Composition (همون ساختار درختی UI که Compose میسازه) میشه، یک کوروتین جدید راه‌اندازی می‌کنه.
    • کد بلوک { … } که به LaunchedEffect میدی، داخل این کوروتین جدید اجرا میشه.
  3. مدیریت چرخه عمر (Lifecycle Management):
    • این مهم‌ترین ویژگی LaunchedEffect هست: کوروتین راه‌اندازی شده توسط LaunchedEffect به چرخه عمر کامپوزبلی که LaunchedEffect رو فراخوانی کرده، متصل میشه.
    • چه اتفاقی میفته؟
      • ورود به Composition: وقتی کامپوزبل حاوی LaunchedEffect برای اولین بار روی صفحه ظاهر میشه، کوروتین راه‌اندازی میشه و کارش رو شروع می‌کنه.
      • خروج از Composition: اگر کامپوزبل از صفحه حذف بشه (مثلاً کاربر به صفحه دیگه بره)، کوروتین مربوط به LaunchedEffect به صورت خودکار لغو (Cancelled) میشه . این یعنی هیچ کار اضافه‌ای در پس‌زمینه انجام نمیشه و منابع بیهوده مصرف نمیشن.
      • تغییر “Key”ها: LaunchedEffect یک یا چند پارامتر می‌گیره که بهشون “کلید” (Keys) میگن (مثلاً LaunchedEffect(myId)).
        • اگر مقدار این کلیدها تغییر کنه، LaunchedEffect قبلی لغو میشه و یک LaunchedEffect جدید با کوروتین جدید دوباره راه‌اندازی میشه.
        • این قابلیت برای وقتی عالیه که می‌خوای کاری رو دوباره انجام بدی چون یه ورودی تغییر کرده. مثلاً اگه ID کاربر تغییر کرده، دوباره اطلاعات کاربری رو از شبکه بگیر.
        • اگر هیچ کلیدی ندی (LaunchedEffect {} یا LaunchedEffect(Unit) یا LaunchedEffect(true))، اون LaunchedEffect فقط یک بار زمانی که برای اولین بار وارد Composition میشه، اجرا میشه و دیگه با Recomposition مجدد اجرا نمیشه، مگر اینکه از Composition خارج و دوباره وارد بشه. (البته استفاده از LaunchedEffect(true) معمولاً توصیه نمیشه، مگر اینکه مطمئن باشی که دقیقاً همین رو می‌خوای و حالت‌های دیگه رو پوشش نمیده.)

مثال :

@Composable
fun UserProfileScreen(userId: String) {
    // ... UI for profile screen ...

    LaunchedEffect(userId) { // کلید رو userId قرار می‌دیم
        // این بلوک کد زمانی اجرا می‌شود که UserProfileScreen وارد Composition شود
        // یا وقتی userId تغییر کند.
        // اگر userId تغییر کند، LaunchedEffect قبلی لغو شده و این دوباره راه‌اندازی می‌شود.

        try {
            val userData = getUserDataFromServer(userId) // یک تابع suspend برای دریافت داده از شبکه
            // بعد از دریافت داده:
            // update UI state to display userData
        } catch (e: Exception) {
            // handle error, show a message
        }
    }
}

 

در این مثال، getUserDataFromServer یک تابع suspend است که درخواست شبکه رو انجام میده. LaunchedEffect تضمین می‌کنه که:

  • این درخواست شبکه در پس‌زمینه انجام بشه (UI فریز نشه).
  • وقتی صفحه پروفایل بسته میشه، درخواست لغو بشه.
  • اگه userId تغییر کنه (مثلاً از طریق یک دکمه یا لینک در برنامه)، درخواست قدیمی لغو بشه و درخواست جدید برای کاربر جدید ارسال بشه.

از آنجایی که تابع LaunchedEffect خودش یک تابع Composable است، تنها می‌تواند در درون سایر توابع Composable مورد استفاده قرار بگیرد.
اما اگر بخواهی یک Coroutine را خارج از Composable اجرا کنی، ولی در عین حال به ترکیب (Composition) وابسته باشد و به‌صورت خودکار هنگام خارج شدن از ترکیب لغو (Cancel) شود، باید از rememberCoroutineScope استفاده کنی.

همچنین هر زمان که نیاز داشتی کنترل چرخه‌ی زندگی یک یا چند coroutine را به‌صورت دستی در اختیار داشته باشی (مثلاً لغو کردن یک انیمیشن هنگام وقوع یک رویداد کاربری)، می‌توانی از rememberCoroutineScope کمک بگیری.

تابع rememberCoroutineScope یک تابع Composable است که یک شیء CoroutineScope برمی‌گرداند؛ این Scope به همان نقطه‌ای از ترکیب که تابع در آن فراخوانی شده وابسته است. این Scope زمانی که این فراخوانی از ترکیب خارج شود، به‌طور خودکار لغو می‌شود.

در مثال زیر ما یه کوروتین رو در داخل یه تابع ایجاد کرده ایم ، و با استفاده از remember CoroutineScope یه کوروتین د این تابع ایجاد کرده ایم :

@Composable
fun MoviesScreen(snackbarHostState: SnackbarHostState) {

    // Creates a CoroutineScope bound to the MoviesScreen's lifecycle
    val scope = rememberCoroutineScope()

    Scaffold(
        snackbarHost = {
            SnackbarHost(hostState = snackbarHostState)
        }
    ) { contentPadding ->
        Column(Modifier.padding(contentPadding)) {
            Button(
                onClick = {
                    // Create a new coroutine in the event handler to show a snackbar
                    scope.launch {
                        snackbarHostState.showSnackbar("Something happened!")
                    }
                }
            ) {
                Text("Press me")
            }
        }
    }
}

توضیح مثال :

فرض کنید ما یک صفحه به نام MoviesScreen داریم که کاربر می‌تواند بر روی یک دکمه کلیک کند. هدف ما این است که وقتی کاربر دکمه را فشار می‌دهد، یک پیام (Snackbar) به او نشان دهیم.

  1. ایجاد دامنه coroutine:
    • در ابتدا، ما از rememberCoroutineScope استفاده می‌کنیم تا یک دامنه coroutine به‌دست آوریم. این به ما این امکان را می‌دهد که coroutine های جدیدی را در این دامنه ایجاد کنیم که به چرخه عمر صفحه ما متصل هستند.
  2. ساختار صفحه:
    • ما از Scaffold استفاده می‌کنیم که یک ساختار پایه برای صفحات در Jetpack Compose است و به ما امکان می‌دهد که یک SnackbarHost برای نمایش پیام‌ها داشته باشیم.
  3. دکمه برای تعامل:
    • یک دکمه با متن “بزنید من را” داریم. وقتی کاربر روی این دکمه کلیک می‌کند، در واقع یک رویداد رخ می‌دهد.
  4. ایجاد coroutine جدید:
    • درون هندلر کلیک دکمه، ما با استفاده از scope.launch یک coroutine جدید ایجاد می‌کنیم. این به ما این امکان را می‌دهد که یک عملیات ناهمگام (مثل نمایش Snackbar) را انجام دهیم.
  5. نمایش پیام:
    • درون coroutine، ما از snackbarHostState.showSnackbar استفاده می‌کنیم تا پیام “چیزی اتفاق افتاد!” را نشان دهیم. این پیام به کاربر اعلام می‌کند که یک رویداد خاص رخ داده است.

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

شما می‌توانید به تعداد دلخواه coroutine ایجاد کنید. هر بار که scope.launch را صدا می‌زنید، یک coroutine جدید راه‌اندازی می‌شود. اما باید به یاد داشته باشید که این coroutine ها همگی به دامنه‌ای که با rememberCoroutineScope ایجاد شده‌اند متصل هستند و زمانی که این دامنه لغو می‌شود (یعنی زمانی که composable از ترکیب خارج می‌شود)، همه coroutine های مربوط به آن هم لغو می‌شوند. بنابراین، از نظر تعداد محدودیتی وجود ندارد، اما باید به چرخه عمر و منابع استفاده شده توجه کنید.

فرض کنید ما یک صفحه داریم که اطلاعات فیلم‌ها را از یک API دریافت می‌کند. در این سناریو، وقتی کاربر بر روی یک دکمه کلیک می‌کند، داده‌ها از اینترنت بارگذاری می‌شوند و در صورت موفقیت، نمایش داده می‌شوند. در اینجا می‌توانیم از rememberCoroutineScope استفاده کنیم تا coroutine هایی را مدیریت کنیم که داده‌ها را بارگذاری می‌کنند. این هندلر این امکان رو به ما میدهد که در تابع های که کاموزیبل نیستند ، Side Effect کنترل کنید .

این هندلر ، استیت State های خارجی را به استیت کامپوز تبدیل میکند .

حالا یه مقدار خودمونی تر توضیح میدم :

اول از همه، produceState چیه؟

produceState یه پل هست بین جهان خارجی (مثل دیتا از اینترنت، دیتابیس، یا حتی Sensorها) و جهان Compose State.

تو Compose هر چی که UI تغییر کنه باید از State بیاد. اما مثلاً فرض کن:

  • یه دیتا می‌خوای از سرور بیاری.
  • این دیتا خودش از دنیای بیرون میاد و به شکل suspend function هست.
  • باید این دیتا رو به Compose بدی تا UI باهاش تغییر کنه.

اینجاست که produceState به کارت میاد.

خب ، اگه از این استفاده نکنیم چی میشه ؟

سوال خیلی مهمیه.

تو می‌تونی با LaunchedEffect یا rememberCoroutineScope هم دیتا بیاری و بعد یه متغیر mutableStateOf رو به‌روز کنی. درسته؟

اما:

  • این حالت جدا کردن لاجیک خیلی تمیز نیست.
  • مشکل Lifecycle پیدا می‌کنی (مثلاً وقتی key تغییر کرد چی؟ ری‌استارت میشه؟).
  • دستی باید مدیریت کنی کی coroutine ری‌استارت بشه.
  • در تست و خوانایی کد هم دردسر داری.

produceState همه اینا رو خودش حل می‌کنه.

  • Coroutine خودش داره.
  • وقتی key1 تغییر کنه خودش ری‌استارت میشه.
  • State<T> مستقیم تولید می‌کنه.
  • نیازی به متغیر اضافی یا مدیریت Scope نداری.

یه مثال واقعی که بدون produceState دردسر داره:

فرض کن می‌خوایم یه عدد تصادفی از سرور بگیریم (شبیه‌سازی شده با delay) و توی UI نشون بدیم.

import androidx.compose.foundation.layout.*

import androidx.compose.material3.*

import androidx.compose.runtime.*

import androidx.compose.ui.Modifier

import androidx.compose.ui.unit.dp

import kotlinx.coroutines.delay




@Composable

fun RandomNumberScreen(userId: String) {

    val numberState = produceState<Int?>(initialValue = null, key1 = userId) {

        delay(1000) // شبیه سازی دانلود عدد از سرور

        value = (1..100).random() // عدد تصادفی

    }




    Column(modifier = Modifier.padding(16.dp)) {

        Text(text = "User ID: $userId")




        if (numberState.value == null) {

            CircularProgressIndicator()

        } else {

            Text(text = "Random Number: ${numberState.value}")

        }

    }

}

 توضیح مثال :

بخشتوضیح
produceState<Int?>(initialValue = null, key1 = userId)یه coroutine راه میندازه، خروجیش رو توی State میریزه
delay(1000)شبیه سازی صبر برای سرور
value = (1..100).random()وقتی coroutine تموم شد، عدد تصادفی میسازه
numberState.value == nullتا وقتی مقدار نیومده null هست (یعنی در حال لود شدن)
وقتی مقدار آماده شدعدد رو توی Text نشون میده

از طرفی در مثال بالا ما میتونستیم از LuanchEffect و mutableStateOF استفاده کنیم  اما :
سوال:

فرق اصلی produceState با LaunchedEffect + mutableStateOf چیه؟

۱) وقتی از LaunchedEffect استفاده می‌کنی:

تو باید دستی یه State تعریف کنی:

var number by remember { mutableStateOf<Int?>(null) }




LaunchedEffect(userId) {

    delay(1000)

    number = (1..100).random()

}

یعنی ۲ تا بخش جدا نیاز داری:

  1. تعریف state (remember { mutableStateOf() })
  2. به‌روزرسانی اون state داخل LaunchedEffect

۲) ولی وقتی از produceState استفاده می‌کنی:

val numberState = produceState<Int?>(initialValue = null, key1 = userId) {

    delay(1000)

    value = (1..100).random()

}

اینجا این دو مرحله یکی شده:

  • هم state ساخته می‌شه.
  • هم در همون بلاک مقداردهی و به‌روزرسانی می‌شه.

🔍 در واقع produceState خودش داره mutableStateOf + LaunchedEffect با هم انجام میده.

به زبان ساده:

روشلازمه State رو دستی بسازی؟توضیح
LaunchedEffect✅ آرهباید mutableStateOf بسازی و توی LaunchedEffect مقدار بدی.
produceState❌ نهخودش State رو برات می‌سازه و مقداردهی می‌کنه. تو فقط مقدار value رو مشخص می‌کنی.

🎯 چرا جمله‌ی «باید دستی بسازی» توی LaunchedEffect درسته؟

چون اونجا کامپوز خودش برات State نمی‌سازه. تو باید بنویسی:

var myState by remember { mutableStateOf(...) }

اما در produceState خودش می‌گه:
«بیا این State آماده، من برات ساختم. فقط بگو value چی باشه.»

اگه بخوایم از یه زاویه دیگه ای نگاه کنیم  :

ما در این جا :

val numberState = produceState<Int?>(initialValue = null, key1 = userId) {

    delay(1000)

    value = (1..100).random()

}

مگه من این state رو به صورت دستی نمیسازیم ؟

پاسخ : نه. چون در produceState تو فقط داری initialValue می‌دی. خود produceState پشت صحنه این رو برات به شکل State آماده می‌کنه.
ولی در LaunchedEffect خودت باید mutableStateOf درست کنی.

🔥 مثال تصویری برای مقایسه:

روشظاهر کدساخت Stateبه‌روزرسانی State
LaunchedEffect2 مرحلهخودت باید بنویسی remember { mutableStateOf() }دستی مقداردهی می‌کنی
produceState1 مرحلهخودش درست می‌کنهفقط مقدار value رو می‌دی

یه نتیجه گیری به صورت خلاصه :

produceState = خودم برات state می‌سازم، تو فقط بگو value چی باشه.

LaunchedEffect = خودت باید state رو بسازی، خودت هم باید مقدار بدی.

 

rememberUpdatedState یک تابع در Jetpack Compose است که برای ارجاع به یک مقدار در یک اثر (Effect) که نباید در صورت تغییر مقدار، دوباره راه‌اندازی (restart) شود، استفاده می‌شود.

عملکرد LaunchedEffect

  • LaunchedEffect: این تابع یک اثر (Effect) را در کامپوزابل‌ها تعریف می‌کند که به محض تغییر یکی از پارامترهای کلیدی (key parameters) آن، دوباره راه‌اندازی می‌شود. این ویژگی می‌تواند در مواردی که نیاز به نگهداری یک اثر Effect طولانی‌مدت است، مشکل‌ساز شود، زیرا ممکن است راه‌اندازی مجدد اثر هزینه‌بر یا غیرممکن باشد.

نیاز به rememberUpdatedState

  • در برخی از موقعیت‌ها، شما ممکن است بخواهید یک مقدار را در اثر Effect خود ذخیره کنید که اگر تغییر کند، اثر Effect نباید دوباره راه‌اندازی شود. در این شرایط، نیاز به استفاده از rememberUpdatedState دارید تا یک ارجاع به این مقدار ایجاد کنید که می‌تواند ذخیره و به‌روزرسانی شود.

توضیح ساده تر :

وقتی شما در Jetpack Compose یک انیمیشن یا عملی را اجرا می‌کنید، اگر وضعیت یا پارامترهای آن تغییر کنند، آن عمل دوباره اجرا می‌شود. این ممکن است باعث مصرف منابع بیشتر و کندی برنامه شود.

حالا چطور rememberUpdatedState کمک می‌کند؟

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

مثال ساده

فرض کنید شما یک انیمیشن دارید که وقتی کاربر روی یک دکمه کلیک می‌کند، شروع به کار می‌کند و به مدت ۵ ثانیه ادامه دارد. اگر کاربر دوباره روی دکمه کلیک کند، می‌خواهید انیمیشن دوباره شروع نشود، اما فقط به آخرین وضعیت (مثل زمان باقی‌مانده) دسترسی داشته باشید.

در اینجا rememberUpdatedState به شما کمک می‌کند تا انیمیشن را فقط یک بار اجرا کنید و بدون نیاز به راه‌اندازی مجدد، به وضعیت آن دسترسی داشته باشید.

کد مثال

@Composable
fun MyAnimation() {
    var isAnimating by remember { mutableStateOf(false) }

    // استفاده از rememberUpdatedState برای به روزرسانی تابع انیمیشن
    val currentAnimationState = rememberUpdatedState(isAnimating)

    LaunchedEffect(currentAnimationState.value) {
        if (currentAnimationState.value) {
            // شروع انیمیشن
            delay(5000) // انیمیشن به مدت ۵ ثانیه ادامه دارد
            isAnimating = false // بعد از ۵ ثانیه، انیمیشن متوقف می‌شود
        }
    }

    Button(onClick = { isAnimating = true }) {
        Text("Start Animation")
    }
}

در این مثال، وقتی دکمه “Start Animation” کلیک می‌شود، انیمیشن شروع می‌شود و بعد از ۵ ثانیه به‌طور خودکار متوقف می‌شود. با استفاده از rememberUpdatedState، اگر isAnimating تغییر کند، اثر دوباره اجرا نمی‌شود و به‌طور صحیح به روز رسانی می‌شود.

 

این هندلر ، به موضوع این بحث شباهت اسمی دارد  ، که به این دلیل است که در داخل تابع کامپوزیبل زمانی که ما بخواهیم  ، کاری یا عملی را در تابع Composable بعد از هر Recompose انجام بدیم ، که این کار ، یک نوع کنترل اثر جانبی است که به صورت مدیریت شده انجام میگیرد ، اما کارهای که سبک هستند ، مثلاً: لاگ گرفتن، کار با سرویس‌های بیرونی API، دیتابیس، آنالیتیکس، GPS، بلوتوث و  . . .

تعریف: SideEffect یک کامپوزابل (Composable) در Jetpack Compose است که برای انتشار وضعیت Compose به کدهایی که تحت مدیریت Compose نیستند، ❓ این یعنی چی ؟  استفاده می‌شود. این امکان به شما اجازه می‌دهد که اثرات جانبی را در پاسخ به تغییرات وضعیت Compose مدیریت کنید.

در Jetpack Compose،‌ وقتی می‌خوای کاری انجام بدی که:

  1. خارج از دنیای Compose باشه (مثلاً: پرینت در لاگ، ارسال به سرور، به‌روز کردن یک API خارجی مثل Analytics یا Map و …)،
  2. اما این کار فقط زمانی انجام بشه که این کامپوزابل اصلی (و نه زیرکامپوزابل‌ها) دوباره رسم (recompose) بشه،

مثال کاربردی

به عنوان مثال، فرض کنید کتابخانه آنالیز شما به شما این امکان را می‌دهد که با پیوست کردن متاداده‌های سفارشی (مانند “ویژگی‌های کاربر”) به همه رویدادهای آنالیز بعدی، جمعیت کاربران خود را تقسیم‌بندی کنید. برای اینکه نوع کاربر فعلی را به کتابخانه آنالیز خود منتقل کنید، می‌توانید از SideEffect استفاده کنید تا مقدار آن را به‌روزرسانی کنید.

چرا به درد می‌خوره؟

مثلاً:

  • می‌خوای هر بار صفحه لود می‌شه به آنالیتیکس بگی.
  • می‌خوای یه مقدار خاص رو به یه API خارجی (که Compose نمی‌شناسه) بفرستی.
  • می‌خوای هر بار این بخش از صفحه کشیده شد یه لاگ بگیری.

مثال خیلی ساده:

@Composable
fun SimpleScreen(userName: String) {
    SideEffect {
        println("این صفحه دوباره رسم شد برای کاربر: $userName")
    }

    Text(text = "سلام $userName")
}

کی نباید از SideEffect استفاده کنیم؟

وقتی:

  • می‌خوای کاری زمان‌بر (suspend) انجام بدی → بهتره از LaunchedEffect استفاده کنی.
  • می‌خوای State یا UI تغییر بدی → اصلاً نباید توی SideEffect انجام بدی.
ویژگیSideEffectLaunchedEffect
زمان اجرابعد از هر recomposition موفق کامپوزابل جاریفقط موقع mount یا وقتی key تغییر کنه
برای کارهای طولانی (suspend)❌ نه✅ بله
برای کارهای سریع و ساده (log, sync)✅ بله❌ نه مناسب نیست
ایجاد coroutine❌ نه✅ بله

در Jetpack Compose، زمانی که می‌خواهیم یک اثر جانبی (Side Effect) اجرا کنیم که نیاز به تمیزکاری (Cleanup) دارد،«یعنی از منابع سیستم که استفاده کردیم مانند استفاده از رم و سی پی یو ، باید این ها رو لغو کنیم»  از DisposableEffect استفاده می‌کنیم. این نوع از افکت زمانی مناسب است که کامپوزابل از کامپوزیشن خارج شود «منظور این که ما با یه دنیایی دیگه سر کار داریم ، مانند این که ما در حال استفاده از کوروتین یا استفاده از کاموزیبل هستیم ، که این ها دوتا دنیا، کانتکس ، بخش جداگانه هستند ، و زمانی که شما از تایمر استفاده میکنید ، این تایمر خارج از کامپوز یا کوروتین است ، شروع شدن و با پایان اومدن این ها در حوزه اختیار کامپوز نیست ، که این ها رو باید دستی مدیریت کنیم »  یا کلیدهای افکت تغییر کند و لازم باشد منابع یا عملیات‌هایی که راه‌اندازی کرده‌ایم، به درستی متوقف یا آزاد شوند.

چه زمانی از DisposableEffect استفاده می‌کنیم؟

وقتی که:

  • به یک منبع خارجی وصل می‌شویم که باید در زمان مناسب آزاد شود.
  • در حال ثبت و لغو Listener هستیم (مثل سنسور گوشی، موقعیت مکانی، یا Broadcast Receiver).
  • نیاز داریم که هنگام تغییر کلیدها یا نابود شدن کامپوزابل منابع پاک شوند یا عملیاتی متوقف شود.

مثال‌های دنیای واقعی برای استفاده از DisposableEffect:

  1. ثبت و لغو Listener برای رویدادهای Lifecycle:
    فرض کن می‌خواهی رویدادهای Lifecycle (مثل onStart, onStop) را برای یک صفحه شنود کنی. در این صورت باید یک LifecycleObserver بسازی و در DisposableEffect آن را ثبت و در زمان خروج کامپوزابل آن را لغو کنی.
  2. WebSocket یا اتصال به سرور:
    زمانی که یک WebSocket یا اتصال شبکه‌ای را باز می‌کنی، لازم است هنگام خروج از صفحه یا تغییر کلیدها، اتصال را ببندی تا منابع هدر نروند.
  3. سنسورها (SensorListener):
    وقتی از سنسورهایی مثل شتاب‌سنج یا ژیروسکوپ استفاده می‌کنی، باید هنگام خروج از صفحه، Listener آن‌ها را غیرفعال کنی.
  4. تایمر (Timer):
    اگر از تایمرهای کلاسیک جاوا مثل Timer استفاده کنی (و نه coroutine-based delay)، باید مطمئن شوی که تایمر به درستی متوقف می‌شود.

مثال تایمر با DisposableEffect:

@Composable

fun TimerComponent() {

    var time by remember { mutableStateOf(0) }




    DisposableEffect(Unit) {

        val timer = Timer()

        timer.scheduleAtFixedRate(object : TimerTask() {

            override fun run() {

                time++

            }

        }, 1000, 1000)




        onDispose {

            timer.cancel() // متوقف کردن تایمر در زمان خروج کامپوزابل

        }

    }




    Text("Time: $time seconds")

}

در این مثال:

  • وقتی کامپوزابل ساخته می‌شود (DisposableEffect اجرا می‌شود)، تایمر شروع به کار می‌کند.
  • وقتی کامپوزابل نابود می‌شود یا کلیدها تغییر می‌کنند، onDispose اجرا شده و تایمر متوقف می‌شود.

چرا LaunchedEffect مناسب این کار نیست؟

اگر از LaunchedEffect استفاده می‌کردیم، امکان صدا زدن timer.cancel() را نداشتیم، چون LaunchedEffect مخصوص اجرای coroutine است، نه مدیریت منابع خارجی جاوا مثل Timer. در نتیجه اگر تایمر رو در LaunchedEffect می‌ساختیم، بعد از نابودی کامپوزابل، تایمر همچنان به کار خود ادامه می‌داد و باعث نشت حافظه (Memory Leak) می‌شد.

خلاصه:

موردDisposableEffectLaunchedEffect
مناسب برایمنابع خارجی، Listener، تایمرهای کلاسیککار با Coroutine، delay، انیمیشن
قابلیت تمیزکاری (onDispose)✅ دارد❌ ندارد
مثال‌هاتایمر، WebSocket، SensorListenerdelay، API call

نکته مهم:

اگر از Coroutine استفاده کنی (مثلاً با delay())، خود LaunchedEffect به صورت خودکار cancel می‌شود و نیازی به تمیزکاری دستی نیست. اما اگر با منابع خارجی کار داری، حتماً از DisposableEffect استفاده کن.

در Compose، گاهی اوقات لازم است که یک وضعیت جدید ایجاد کنیم که از وضعیت‌های موجود دیگر مشتق (derived) شده باشد. اما ممکن است نخواهیم این وضعیت جدید هر بار که کامپوز بازترکیب (recompose) می‌شود، دوباره محاسبه شود، زیرا این کار می‌تواند عملکرد اپلیکیشن را کاهش دهد.

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

کاربرد اصلی derivedStateOf:

  1. وقتی می‌خواهیم از وضعیت‌های موجود یک وضعیت جدید بسازیم.
  2. وضعیت جدید فقط زمانی محاسبه شود که وضعیت‌های اصلی تغییر کنند.
  3. این کار باعث می‌شود بازترکیب‌های غیرضروری حذف شوند و اپلیکیشن سریع‌تر اجرا شود.

مثال‌ها:

مثال 1: محاسبه مجموع اعداد

فرض کنید لیستی از اعداد داریم و می‌خواهیم مجموع این اعداد را نمایش دهیم. مجموع فقط وقتی باید محاسبه شود که لیست اعداد تغییر کند.

@Composable
fun DerivedStateExample(numbers: List<Int>) {
    val totalSum by remember {
        derivedStateOf { numbers.sum() }
    }
    Text("Total Sum: $totalSum")
}

توضیح ساده:

  • با استفاده از derivedStateOf، مجموع اعداد (totalSum) فقط وقتی دوباره محاسبه می‌شود که لیست numbers تغییر کند. این باعث می‌شود محاسبات اضافی حذف شوند.

مثال 2: فیلتر کردن لیست بر اساس جستجو

فرض کنید لیستی از کلمات داریم و کاربر یک عبارت جستجو وارد می‌کند. می‌خواهیم لیست فیلتر شده فقط زمانی محاسبه شود که عبارت جستجو یا لیست اصلی تغییر کند.

توضیح ساده:

  • با استفاده از derivedStateOf، لیست فیلتر شده (filteredItems) فقط وقتی دوباره محاسبه می‌شود که عبارت جستجو (query) یا لیست اصلی (items) تغییر کنند.
  • اگر عبارت جستجو خالی باشد، لیست اصلی نمایش داده می‌شود. اگر جستجو انجام شود، لیست فیلتر می‌شود.

نکات مهم:

  1. دلیل استفاده از derivedStateOf:
    • اگر وضعیت مشتق‌شده (مانند مجموع یا لیست فیلتر شده) هر بار بدون تغییر وضعیت اصلی محاسبه شود، باعث بازترکیب‌های غیرضروری و کاهش عملکرد اپلیکیشن می‌شود.
    • با استفاده از derivedStateOf، این محاسبات فقط زمانی انجام می‌شوند که لازم باشد.
  2. کاربرد در لیست‌های بزرگ:
    • وقتی لیست‌ها بزرگ هستند، استفاده از derivedStateOf باعث می‌شود فیلتر کردن یا پردازش لیست بهینه‌تر انجام شود.

نتیجه‌گیری:

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

   
@Composable
fun FilteredListExample(items: List<String>) {
    var query by remember { mutableStateOf("") }

    // ساخت وضعیت مشتق‌شده بر اساس جستجو
    val filteredItems by remember {
        derivedStateOf {
            if (query.isEmpty()) {
                items
            } else {
                items.filter { it.contains(query, ignoreCase = true) }
            }
        }
    }

    Column(modifier = Modifier.padding(16.dp)) {
        TextField(
            value = query,
            onValueChange = { query = it },
            label = { Text("Search") }
        )
        Spacer(modifier = Modifier.height(8.dp))
        Text("Filtered Items:", style = MaterialTheme.typography.h6)
        Spacer(modifier = Modifier.height(8.dp))

        // نمایش لیست فیلتر شده
        LazyColumn {
            items(filteredItems) { item ->
                Text(item, modifier = Modifier.padding(vertical = 4.dp))
            }
        }
    }
}

 

در ابتدای کار برای این که ، مفهوم خوب متوجه بشیم نیاز داریم ، که مفهوم مشتق رو بدونیم :

مشتق‌شده یعنی چی؟

مشتق‌شده یعنی:

از روی یه چیز دیگه محاسبه شده یا نتیجه‌گیری شده باشه

🟢 مثال ساده‌ی روزمره:

فرض کن یه فروشگاه داری.

  • قیمت واحد کالا = 10 هزار تومان
  • تعداد خرید شده = 3

خب حالا:

  • جمع کل = قیمت × تعداد = 30 هزار تومان

اینجا جمع کل یه مقدار مشتق‌شده از قیمت و تعداد هست.
یعنی خودش مستقل نیست، بلکه بر اساس دو تا داده دیگه محاسبه شده.

🟦 حالا در Compose چی میشه؟

فرض کن:

val count = remember { mutableStateOf(3) }

و یه مقدار جدید تعریف می‌کنی که بگه آیا count بیشتر از 5 هست یا نه:

val isHigh = count.value > 5

اینجا isHigh یک مقدار مشتق‌شده از count هست.
یعنی خودش state نیست، ولی وابسته به یه state دیگه است و با تغییر اون دوباره محاسبه میشه.

خب کجا این مفهوم مشتق استفاده میشه  :

🧩 اول بریم سراغ مشکل اصلی:

در Jetpack Compose وقتی یه @Composable داره از یه state استفاده می‌کنه، هر بار که اون state تغییر کنه، اون Composable دوباره اجرا می‌شه (یعنی بازترکیب یا Recompose میشه).
حالا فرض کن یه state داریم که خیلی زیاد تغییر می‌کنه، ولی ما فقط در بعضی از اون تغییرات واقعاً نیاز به بروزرسانی UI داریم. اینجا اگه همه‌ش Recompose بشه، باعث می‌شه کارایی (performance) پایین بیاد.

💡 راه‌حل چیه؟ derivedStateOf

حالا ما با derivedStateOf می‌تونیم بگیم:

یه مقدار مشتق‌شده از state حساب کن، ولی فقط وقتی مقدار نهایی اون فرق کرد، Recompose انجام بده.

مثال ساده: دکمه «برو بالا»

فرض کن یه لیست داریم و یه دکمه‌ی “scroll to top”. ما می‌خوایم این دکمه فقط وقتی نشون داده بشه که کاربر اسکرول کرده پایین.

می‌تونستیم اینجوری بنویسیم:

val showButton = remember { mutableStateOf(false) }

LaunchedEffect(listState.firstVisibleItemIndex) {
showButton.value = (listState.firstVisibleItemIndex > 0)
}

این روش جواب می‌ده، ولی ممکنه بی‌خودی Recompose زیاد بشه.

😎 روش بهتر: derivedStateOf

val showButton by remember {

    derivedStateOf { listState.firstVisibleItemIndex > 0 }

}

حالا:

  • هر بار که کاربر اسکرول کنه، مقدار listState.firstVisibleItemIndex آپدیت میشه.
  • ولی فقط وقتی مقدار showButton از false به true یا برعکس تغییر کنه، کامپوز دوباره اجرا میشه.
  • این باعث میشه تغییرات غیرضروری نادیده گرفته بشن → کد سریع‌تر و بهینه‌تر!

🧠 خلاصه ساده:

derivedStateOf مثل یه فیلتر هوشمند عمل می‌کنه:

فقط وقتی مقدار واقعا تغییر کرد، برو ترکیب مجدد انجام بده!

🔑 نکته پایانی درباره «کلیدهای پایدار» (stable keys):

اگه توی لیست‌ها (LazyColumn، LazyRow و…) از کلیدهای منحصربه‌فرد و ثابت استفاده نکنی، Compose ممکنه فکر کنه آیتم‌ها عوض شدن و بی‌دلیل همه‌چیز رو بازترکیب کنه.

✅ پس همیشه بنویس:

items(list, key = { it.id }) { item ->

   ...

}

تا به اینجای کار داده های ما ، که به صورت State شناخته میشد ، به صورت تکی بود ، اگر این داده های به صورت لیست باشند چطور باید مدیریت شوند که کامپوز دوباره کاری انجام ندهد .

در Jetpack Compose، SnapshotStateList و SnapshotStateMap انواع خاصی هستند که برای مدیریت کارآمد مجموعه‌های قابل تغییر ارائه شده‌اند. این انواع اطمینان می‌دهند که تغییرات در مجموعه‌ها به گونه‌ای ردیابی می‌شوند که به Compose اجازه می‌دهد فقط بخش‌هایی از رابط کاربری را که تحت تأثیر آن تغییرات قرار دارند، دوباره کامپوز کند. این می‌تواند عملکرد را به طور قابل توجهی بهبود بخشد و مدیریت حالت را ساده‌تر کند.

ایجاد SnapshotStateList و SnapshotStateMap

این مجموعه‌ها با استفاده از توابع کمکی mutableStateListOf و mutableStateMapOf ایجاد می‌شوند. این توابع به ترتیب نمونه‌هایی از SnapshotStateList و SnapshotStateMap را ایجاد می‌کنند.

val items = mutableStateListOf("سیب", "موز", "پرتقال")
val userMap = mutableStateMapOf("کاربر1" to "آلیس", "کاربر2" to "باب")

مثال با SnapshotStateList

در اینجا مثالی آورده شده است که نحوه استفاده از SnapshotStateList را در یک رابط کاربری Jetpack Compose نشان می‌دهد:

       

@Composable

fun ShoppingList() {

    val items = remember { mutableStateListOf("شیر", "تخم مرغ", "نان") }




    Column {

        items.forEach { item ->

            Text(item)

        }

        Button(onClick = { items.add("کره") }) {

            Text("اضافه کردن کره")

        }

    }

}

در این مثال:

  • ما با استفاده از mutableStateListOf یک SnapshotStateList ایجاد می‌کنیم.
  • لیست در یک Column نمایش داده می‌شود و هر آیتم به عنوان یک کامپوننت Text رندر می‌شود.
  • یک دکمه برای اضافه کردن یک آیتم جدید (“کره”) به لیست ارائه شده است. هنگامی که روی این دکمه کلیک شود، لیست به روز می‌شود و Compose به طور خودکار یک بازترکیب را برای نشان دادن حالت جدید فعال می‌کند.

مثال با SnapshotStateMap

حالا بیایید به مثالی با استفاده از SnapshotStateMap نگاهی بیندازیم:“`kotlin @Composable fun UserProfileMap() { val userMap = remember { mutableStateMapOf(“کاربر1” to “جان”, “کاربر2” to “جین”) }

Column {

    userMap.forEach { (key, name) ->

        Text("$key: $name")

    }

    Button(onClick = { userMap["کاربر3"] = "آلیس" }) {

        Text("اضافه کردن آلیس")

    }

}

}

در این مثال:

*   ما با استفاده از `mutableStateMapOf` یک `SnapshotStateMap` ایجاد می‌کنیم.

*   نقشه در یک `Column` نمایش داده می‌شود، با هر جفت کلید-مقدار به عنوان یک کامپوننت `Text` رندر می‌شود.

*   یک دکمه برای اضافه کردن یک ورودی جدید (“کاربر3” به “آلیس”) به نقشه ارائه شده است. هنگامی که روی دکمه کلیک شود، نقشه به روز می‌شود و Compose به طور خودکار یک بازترکیب را برای نمایش ورودی جدید فعال می‌کند.

 

نتیجه گیری :
زمانی که تغییری در یک لیست معمولی ایجاد شود (مثلاً یک آیتم اضافه یا حذف شود)، Compose از این تغییر آگاه نمی‌شود. در نتیجه، Compose نمی‌داند که چه بخش‌هایی از UI باید دوباره ترسیم شوند.

  • SnapshotStateList: SnapshotStateList به Compose اطلاع می‌دهد که چه زمانی تغییراتی در لیست رخ داده است. Compose می‌تواند این تغییرات را ردیابی کند و فقط قسمت‌هایی از UI را که تحت تأثیر این تغییرات قرار دارند، دوباره ترسیم کند.
  • SnapshotStateList یک مفهوم و یک نوع داده است که به طور نامرئی در کد شما وجود دارد و توسط Jetpack Compose برای مدیریت کارآمد حالت استفاده می‌شود. شما نیازی ندارید مستقیماً با SnapshotStateList تعامل داشته باشید، زیرا mutableStateListOf و سایر توابع کمکی این کار را برای شما انجام می‌دهند.
دیدگاه‌ها ۰
ارسال دیدگاه جدید