কম্পোজ এর পার্শ্বপ্রতিক্রিয়া

A side-effect is a change to the state of the app that happens outside the scope of a composable function. Due to composables' lifecycle and properties such as unpredictable recompositions, executing recompositions of composables in different orders, or recompositions that can be discarded, composables should ideally be side-effect free .

However, sometimes side-effects are necessary, for example, to trigger a one-off event such as showing a snackbar or navigate to another screen given a certain state condition. These actions should be called from a controlled environment that is aware of the lifecycle of the composable. In this page, you'll learn about the different side-effect APIs Jetpack Compose offers.

অবস্থা এবং প্রভাব ব্যবহারের ক্ষেত্র

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 .

Due to the different possibilities effects open up in Compose, they can be easily overused. Make sure that the work you do in them is UI related and doesn't break unidirectional data flow as explained in the Managing state documentation .

LaunchedEffect : run suspend functions in the scope of a composable

একটি কম্পোজেবলের জীবনচক্র জুড়ে কাজ সম্পাদন করতে এবং সাসপেন্ড ফাংশন কল করার ক্ষমতা পেতে, LaunchedEffect কম্পোজেবলটি ব্যবহার করুন। যখন LaunchedEffect কম্পোজিশনে প্রবেশ করে, তখন এটি প্যারামিটার হিসেবে পাস করা কোড ব্লকসহ একটি কো-রুটিন চালু করে। LaunchedEffect কম্পোজিশন থেকে বেরিয়ে গেলে কো-রুটিনটি বাতিল হয়ে যাবে। যদি LaunchedEffect LaunchedEffect ভিন্ন কী (key) দিয়ে পুনরায় কম্পোজ করা হয় (নিচের ' Restarting Effects' অংশটি দেখুন), তাহলে বিদ্যমান কো-রুটিনটি বাতিল হয়ে যাবে এবং নতুন সাসপেন্ড ফাংশনটি একটি নতুন কো-রুটিনে চালু হবে।

উদাহরণস্বরূপ, এখানে একটি অ্যানিমেশন রয়েছে যা একটি কনফিগারযোগ্য বিলম্ব সহ আলফা মানকে স্পন্দিত করে:

// Allow the pulse rate to be configured, so it can be sped up if the user is running
// out of time
var pulseRateMs by remember { mutableLongStateOf(3000L) }
val alpha = remember { Animatable(1f) }
LaunchedEffect(pulseRateMs) { // Restart the effect when the pulse rate changes
    while (isActive) {
        delay(pulseRateMs) // Pulse the alpha every pulseRateMs to alert the user
        alpha.animateTo(0f)
        alpha.animateTo(1f)
    }
}

In the code above, the animation uses the suspending function delay to wait the set amount of time. Then, it sequentially animates the alpha to zero and back again using animateTo . This will repeat for the life of the composable.

rememberCoroutineScope : একটি কম্পোজেবলের বাইরে কো-রুটিন চালু করার জন্য একটি কম্পোজিশন-সচেতন স্কোপ পাওয়া।

যেহেতু LaunchedEffect একটি কম্পোজেবল ফাংশন, তাই এটি শুধুমাত্র অন্যান্য কম্পোজেবল ফাংশনের ভিতরেই ব্যবহার করা যায়। একটি কম্পোজেবলের বাইরে কোনো কো-রুটিন চালু করতে, কিন্তু সেটিকে এমনভাবে স্কোপড করতে যাতে কম্পোজিশন থেকে বেরিয়ে গেলেই তা স্বয়ংক্রিয়ভাবে বাতিল হয়ে যায়, rememberCoroutineScope ব্যবহার করুন। এছাড়াও, যখনই আপনার এক বা একাধিক কো-রুটিনের লাইফসাইকেল ম্যানুয়ালি নিয়ন্ত্রণ করার প্রয়োজন হয়, যেমন—কোনো ইউজার ইভেন্ট ঘটলে একটি অ্যানিমেশন বাতিল করা, তখনও rememberCoroutineScope ব্যবহার করুন।

rememberCoroutineScope is a composable function that returns a CoroutineScope bound to the point of the Composition where it's called. The scope will be cancelled when the call leaves the Composition.

পূর্ববর্তী উদাহরণ অনুসরণ করে, ব্যবহারকারী কোনো Button ট্যাপ করলে একটি Snackbar দেখানোর জন্য আপনি এই কোডটি ব্যবহার করতে পারেন:

@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")
            }
        }
    }
}

rememberUpdatedState : কোনো ইফেক্টের এমন একটি মানকে নির্দেশ করে, যার মান পরিবর্তিত হলে সেটি পুনরায় চালু হবে না।

যখন কোনো একটি মূল প্যারামিটার পরিবর্তিত হয়, তখন LaunchedEffect পুনরায় চালু হয়। তবে, কিছু পরিস্থিতিতে আপনি আপনার ইফেক্টের মধ্যে এমন একটি মান ধরে রাখতে চাইতে পারেন, যা পরিবর্তিত হলে আপনি ইফেক্টটি পুনরায় চালু করতে চান না। এটি করার জন্য, এই মানটির একটি রেফারেন্স তৈরি করতে rememberUpdatedState ব্যবহার করা প্রয়োজন, যা পরবর্তীতে ধরে রাখা এবং আপডেট করা যায়। এই পদ্ধতিটি সেইসব ইফেক্টের জন্য সহায়ক, যেগুলিতে দীর্ঘস্থায়ী অপারেশন থাকে, যা পুনরায় তৈরি এবং পুনরায় চালু করা ব্যয়বহুল বা অসম্ভব হতে পারে।

For example, suppose your app has a LandingScreen that disappears after some time. Even if LandingScreen is recomposed, the effect that waits for some time and notifies that the time passed shouldn't be restarted:

@Composable
fun LandingScreen(onTimeout: () -> Unit) {

    // This will always refer to the latest onTimeout function that
    // LandingScreen was recomposed with
    val currentOnTimeout by rememberUpdatedState(onTimeout)

    // Create an effect that matches the lifecycle of LandingScreen.
    // If LandingScreen recomposes, the delay shouldn't start again.
    LaunchedEffect(true) {
        delay(SplashWaitTimeMillis)
        currentOnTimeout()
    }

    /* Landing screen content */
}

কল সাইটের লাইফসাইকেলের সাথে সামঞ্জস্যপূর্ণ একটি এফেক্ট তৈরি করতে, Unit বা true মতো একটি অপরিবর্তনীয় ধ্রুবক প্যারামিটার হিসেবে পাস করা হয়। উপরের কোডে, LaunchedEffect(true) ব্যবহার করা হয়েছে। onTimeout ল্যাম্বডাটি যেন সবসময় LandingScreen রিকম্পোজ করার সময়কার সর্বশেষ মানটি ধারণ করে, তা নিশ্চিত করতে onTimeout rememberUpdatedState ফাংশনের মধ্যে র‍্যাপ করতে হবে। রিটার্ন করা State , যা কোডে currentOnTimeout , সেটি এফেক্টে ব্যবহার করা উচিত।

DisposableEffect : যে প্রভাবগুলো পরিষ্কার করা প্রয়োজন

For side effects that need to be cleaned up after the keys change or if the composable leaves the Composition, use DisposableEffect . If the DisposableEffect keys change, the composable needs to dispose (do the cleanup for) its current effect, and reset by calling the effect again.

উদাহরণস্বরূপ, আপনি একটি LifecycleObserver ব্যবহার করে Lifecycle ইভেন্টের উপর ভিত্তি করে অ্যানালিটিক্স ইভেন্ট পাঠাতে চাইতে পারেন। Compose-এ সেই ইভেন্টগুলো শোনার জন্য, প্রয়োজন অনুযায়ী অবজারভারটিকে রেজিস্টার এবং আনরেজিস্টার করতে একটি DisposableEffect ব্যবহার করুন।

@Composable
fun HomeScreen(
    lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
    onStart: () -> Unit, // Send the 'started' analytics event
    onStop: () -> Unit // Send the 'stopped' analytics event
) {
    // Safely update the current lambdas when a new one is provided
    val currentOnStart by rememberUpdatedState(onStart)
    val currentOnStop by rememberUpdatedState(onStop)

    // If `lifecycleOwner` changes, dispose and reset the effect
    DisposableEffect(lifecycleOwner) {
        // Create an observer that triggers our remembered callbacks
        // for sending analytics events
        val observer = LifecycleEventObserver { _, event ->
            if (event == Lifecycle.Event.ON_START) {
                currentOnStart()
            } else if (event == Lifecycle.Event.ON_STOP) {
                currentOnStop()
            }
        }

        // Add the observer to the lifecycle
        lifecycleOwner.lifecycle.addObserver(observer)

        // When the effect leaves the Composition, remove the observer
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }

    /* Home screen content */
}

উপরের কোডে, ইফেক্টটি lifecycleOwnerobserver যুক্ত করবে। যদি lifecycleOwner পরিবর্তিত হয়, তাহলে ইফেক্টটি ডিসপোজ হয়ে যায় এবং নতুন lifecycleOwner সাথে পুনরায় চালু হয়।

একটি DisposableEffect কোড ব্লকের শেষ স্টেটমেন্ট হিসেবে অবশ্যই একটি onDispose ক্লজ অন্তর্ভুক্ত থাকতে হবে। অন্যথায়, IDE একটি বিল্ড-টাইম ত্রুটি প্রদর্শন করে।

SideEffect : publish Compose state to non-Compose code

To share Compose state with objects not managed by compose, use the SideEffect composable. Using a SideEffect guarantees that the effect executes after every successful recomposition. On the other hand, it is incorrect to perform an effect before a successful recomposition is guaranteed, which is the case when writing the effect directly in a composable.

For example, your analytics library might allow you to segment your user population by attaching custom metadata ("user properties" in this example) to all subsequent analytics events. To communicate the user type of the current user to your analytics library, use SideEffect to update its value.

@Composable
fun rememberFirebaseAnalytics(user: User): FirebaseAnalytics {
    val analytics: FirebaseAnalytics = remember {
        FirebaseAnalytics()
    }

    // On every successful composition, update FirebaseAnalytics with
    // the userType from the current User, ensuring that future analytics
    // events have this metadata attached
    SideEffect {
        analytics.setUserProperty("userType", user.userType)
    }
    return analytics
}

produceState : নন-কম্পোজ স্টেটকে কম্পোজ স্টেটে রূপান্তর করুন

produceState launches a coroutine scoped to the Composition that can push values into a returned State . Use it to convert non-Compose state into Compose state, for example bringing external subscription-driven state such as Flow , LiveData , or RxJava into the Composition.

The producer is launched when produceState enters the Composition, and will be cancelled when it leaves the Composition. The returned State conflates; setting the same value won't trigger a recomposition.

যদিও produceState একটি কো-রুটিন তৈরি করে, এটি ডেটার নন-সাসপেন্ডিং সোর্স পর্যবেক্ষণ করতেও ব্যবহার করা যেতে পারে। সেই সোর্সের সাবস্ক্রিপশন বাতিল করতে, awaitDispose ফাংশনটি ব্যবহার করুন।

নিম্নলিখিত উদাহরণটি দেখায় কিভাবে নেটওয়ার্ক থেকে একটি ছবি লোড করতে produceState ব্যবহার করতে হয়। loadNetworkImage কম্পোজেবল ফাংশনটি একটি State রিটার্ন করে যা অন্যান্য কম্পোজেবলে ব্যবহার করা যেতে পারে।

@Composable
fun loadNetworkImage(
    url: String,
    imageRepository: ImageRepository = ImageRepository()
): State<Result<Image>> {
    // Creates a State<T> with Result.Loading as initial value
    // If either `url` or `imageRepository` changes, the running producer
    // will cancel and will be re-launched with the new inputs.
    return produceState<Result<Image>>(initialValue = Result.Loading, url, imageRepository) {
        // In a coroutine, can make suspend calls
        val image = imageRepository.load(url)

        // Update State with either an Error or Success result.
        // This will trigger a recomposition where this State is read
        value = if (image == null) {
            Result.Error
        } else {
            Result.Success(image)
        }
    }
}

derivedStateOf : এক বা একাধিক স্টেট অবজেক্টকে অন্য স্টেটে রূপান্তর করুন

In Compose, recomposition occurs each time an observed state object or composable input changes. A state object or input may be changing more often than the UI actually needs to update, leading to unnecessary recomposition.

You should use the derivedStateOf function when your inputs to a composable are changing more often than you need to recompose. This often occurs when something is frequently changing, such as a scroll position, but the composable only needs to react to it once it crosses a certain threshold. derivedStateOf creates a new Compose state object you can observe that only updates as much as you need. In this way, it acts similarly to the Kotlin Flows distinctUntilChanged() operator.

সঠিক ব্যবহার

নিম্নলিখিত কোড স্নিপেটটি derivedStateOf এর একটি উপযুক্ত ব্যবহার দেখায়:

@Composable
// When the messages parameter changes, the MessageList
// composable recomposes. derivedStateOf does not
// affect this recomposition.
fun MessageList(messages: List<Message>) {
    Box {
        val listState = rememberLazyListState()

        LazyColumn(state = listState) {
            // ...
        }

        // Show the button if the first visible item is past
        // the first item. We use a remembered derived state to
        // minimize unnecessary compositions
        val showButton by remember {
            derivedStateOf {
                listState.firstVisibleItemIndex > 0
            }
        }

        AnimatedVisibility(visible = showButton) {
            ScrollToTopButton()
        }
    }
}

In this snippet, firstVisibleItemIndex changes any time the first visible item changes. As you scroll, the value becomes 0 , 1 , 2 , 3 , 4 , 5 , etc. However, recomposition only needs to occur if the value is greater than 0 . This mismatch in update frequency means that this is a good use case for derivedStateOf .

ভুল ব্যবহার

A common mistake is to assume that, when you combine two Compose state objects, you should use derivedStateOf because you are "deriving state". However, this is purely overhead and not required, as shown in the following snippet:

// DO NOT USE. Incorrect usage of derivedStateOf.
var firstName by remember { mutableStateOf("") }
var lastName by remember { mutableStateOf("") }

val fullNameBad by remember { derivedStateOf { "$firstName $lastName" } } // This is bad!!!
val fullNameCorrect = "$firstName $lastName" // This is correct

এই কোড অংশে, fullName ঠিক ততবারই আপডেট করতে হয় যতবার firstName এবং lastName । তাই, এখানে কোনো অতিরিক্ত পুনর্গঠন ঘটছে না এবং derivedStateOf ব্যবহার করার প্রয়োজন নেই।

snapshotFlow : Compose-এর State-কে Flow-তে রূপান্তর করুন

State<T> অবজেক্টগুলোকে একটি কোল্ড ফ্লো-তে রূপান্তর করতে snapshotFlow ব্যবহার করুন। ডেটা সংগ্রহ করা হলে snapshotFlow তার নিজস্ব ব্লকটি চালায় এবং এর মধ্যে পঠিত State অবজেক্টগুলোর ফলাফল ইমিট করে। যখন snapshotFlow ব্লকের ভিতরে পঠিত কোনো State অবজেক্ট পরিবর্তিত হয়, তখন যদি নতুন মানটি পূর্ববর্তী ইমিট করা মানের সমান না হয়, তাহলে ফ্লো-টি তার কালেক্টরের কাছে নতুন মানটি ইমিট করবে (এই আচরণটি Flow.distinctUntilChanged এর অনুরূপ)।

নিম্নলিখিত উদাহরণটি এমন একটি সাইড এফেক্ট দেখায় যা ব্যবহারকারী যখন কোনো তালিকার প্রথম আইটেমটি স্ক্রল করে পার হয়ে যায়, তখন তা অ্যানালিটিক্সে রেকর্ড করে:

val listState = rememberLazyListState()

LazyColumn(state = listState) {
    // ...
}

LaunchedEffect(listState) {
    snapshotFlow { listState.firstVisibleItemIndex }
        .map { index -> index > 0 }
        .distinctUntilChanged()
        .filter { it == true }
        .collect {
            MyAnalyticsService.sendScrolledPastFirstItemEvent()
        }
}

উপরের কোডে, listState.firstVisibleItemIndex কে এমন একটি Flow-তে রূপান্তর করা হয়েছে যা Flow-এর অপারেটরগুলোর সুবিধা নিতে পারে।

পুনরায় চালু করার প্রভাব

Compose-এর কিছু এফেক্ট, যেমন LaunchedEffect , produceState বা DisposableEffect , পরিবর্তনশীল সংখ্যক আর্গুমেন্ট বা 'কী' গ্রহণ করে, যা চলমান এফেক্টটি বাতিল করতে এবং নতুন কী-গুলো ব্যবহার করে একটি নতুন এফেক্ট শুরু করতে ব্যবহৃত হয়।

এই এপিআইগুলোর সাধারণ রূপটি হলো:

EffectName(restartIfThisKeyChanges, orThisKey, orThisKey, ...) { block }

এই আচরণের সূক্ষ্মতার কারণে, ইফেক্টটি পুনরায় চালু করার জন্য ব্যবহৃত প্যারামিটারগুলো সঠিক না হলে সমস্যা দেখা দিতে পারে:

  • প্রয়োজনের চেয়ে কম রিস্টার্ট করলে আপনার অ্যাপে বাগ দেখা দিতে পারে।
  • প্রয়োজনের চেয়ে বেশি রিস্টার্ট করা অকার্যকর হতে পারে।

সাধারণত, ইফেক্ট কোড ব্লকে ব্যবহৃত পরিবর্তনযোগ্য (mutable) এবং অপরিবর্তনযোগ্য (immutable) ভেরিয়েবলগুলোকে ইফেক্ট কম্পোজেবলের প্যারামিটার হিসেবে যোগ করা উচিত। এগুলো ছাড়াও, ইফেক্টটিকে জোর করে পুনরায় চালু করার জন্য আরও প্যারামিটার যোগ করা যেতে পারে। যদি কোনো ভেরিয়েবলের পরিবর্তনের কারণে ইফেক্টটি পুনরায় চালু না হয়, তবে ভেরিয়েবলটিকে rememberUpdatedState এর মধ্যে রাখতে হবে। যদি ভেরিয়েবলটি কোনো কী (key) ছাড়া remember মধ্যে থাকার কারণে কখনও পরিবর্তিত না হয়, তবে ভেরিয়েবলটিকে ইফেক্টে কী (key) হিসেবে পাস করার প্রয়োজন নেই।

In the DisposableEffect code shown above, the effect takes as a parameter the lifecycleOwner used in its block, because any change to them should cause the effect to restart.

@Composable
fun HomeScreen(
    lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
    onStart: () -> Unit, // Send the 'started' analytics event
    onStop: () -> Unit // Send the 'stopped' analytics event
) {
    // These values never change in Composition
    val currentOnStart by rememberUpdatedState(onStart)
    val currentOnStop by rememberUpdatedState(onStop)

    DisposableEffect(lifecycleOwner) {
        val observer = LifecycleEventObserver { _, event ->
            /* ... */
        }

        lifecycleOwner.lifecycle.addObserver(observer)
        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }
}

currentOnStart এবং currentOnStop DisposableEffect কী হিসেবে ব্যবহার করার প্রয়োজন নেই, কারণ rememberUpdatedState ব্যবহারের ফলে Composition-এ এদের মান কখনও পরিবর্তিত হয় না। যদি আপনি lifecycleOwner প্যারামিটার হিসেবে পাস না করেন এবং এর মান পরিবর্তিত হয়, তাহলে HomeScreen পুনরায় কম্পোজ করে, কিন্তু DisposableEffect টি ডিসপোজ হয়ে পুনরায় চালু হয় না। এর ফলে সমস্যা তৈরি হয়, কারণ সেই মুহূর্ত থেকে ভুল lifecycleOwner ব্যবহৃত হতে থাকে।

ধ্রুবকগুলি চাবি হিসাবে

কল সাইটের লাইফসাইকেল অনুসরণ করানোর জন্য আপনি ইফেক্ট কী হিসেবে true মতো একটি কনস্ট্যান্ট ব্যবহার করতে পারেন। এর কিছু বৈধ ব্যবহারক্ষেত্রও রয়েছে, যেমন উপরে দেখানো LaunchedEffect উদাহরণটি। তবে, তা করার আগে, দুবার ভাবুন এবং নিশ্চিত হয়ে নিন যে আপনার সত্যিই এটাই প্রয়োজন।

{% হুবহু %} {% endverbatim %} {% হুবহু %} {% endverbatim %}