Side Effects of Android Jetpack Compose

Side Effects of Android Jetpack Compose

Compose side-effect
Jetpack Compose is preferred by many developers because of its fun, simple, effective and straightforward nature, and the ability to easily build custom components declaratively. However, to get the most out of its capabilities, it's important to have a good grasp of side effects and effect handlers.

What are collateral effects?

Managing side effects can be one of the biggest challenges developers face when building UIs in Android. It is an application state change that occurs outside the scope of a composable function.

// Side Effect
private var i = 0

@Composable
fun SideEffect() {
    
    
    var text by remember {
    
    
        mutableStateOF("")
    }
    Column {
    
    
        Button(onClick = {
    
     text += "@" }) {
    
    
            i++
            Text(text)
        }
    }
}

In this example, SideEffect creates mutableStateOfa mutable state object with an initial value of the empty string. Now on button click we are updating the text and on text update we want to update the value of i. But the Button combination may recombine without clicking, which doesn't change the text, but increases ithe value. If this is a network call, it will be made every time the Button is reassembled.

Ideally your composition should be side-effect-free, but sometimes you need side-effects like triggering one-off events like making a network call or collecting a stream.

To address these issues, Compose provides a variety of side effects for different situations, including the following:

LaunchedEffect

LaunchedEffect is a composable function used to launch coroutines within the scope of the composition. When the LaunchedEffect enters the composition, it starts a coroutine that cancels it when it leaves the composition. LaunchedEffect accepts multiple keys as parameters, and if any key changes, it cancels the existing coroutine and restarts. This is useful for performing side effects, such as making network calls or updating a database, without blocking the UI thread.

// Launched Effect
private var i = 0

@Composable
fun SideEffect() {
    
    
    var text by remember {
    
    
        mutableStateOF("")
    }
    
    LaunchedEffect(key1 = text) {
    
    
        i++
    }
    Column {
    
    
        Button(onClick = {
    
     text += "@" }) {
    
    
            Text(text)
        }
    }
}

In the above example, every time the text is updated, a new coroutine is started and the ivalue is updated accordingly. Since iit is incremented only when the text value changes, this function is side-effect free.

rememberCoroutineScope

To make sure LaunchedEffectit starts when you combine it for the first time, use it as-is. However, if you need manual control over startup, use instead rememberCoroutineScope. This can be used to get a coroutine scope bound to a composition point to start coroutines outside of composition. rememberCoroutineScopeis a composable function that returns a coroutine scope that is bound to where it is called Composable. This scope will be canceled when the call leaves the composition.

@Composable
fun MyComponent() {
    
    
    val coroutineScope = rememberCoroutineScope()
    val data = remember {
    
     mutableStateOf("") }

    Button(onClick = {
    
    
        coroutineScope.launch {
    
    
            // Simulate network call
            delay(2000)
            data.value = "Data loaded"
        }
    }) {
    
    
        Text("Load data")
    }

    Text(text = data.value)
}

In this example, rememberCoroutineScopeis used to create a coroutine scope bound to the lifetime of a Composable function. Doing this allows efficient and safe management of coroutines by ensuring that they are canceled when the Composable function is removed from the composition. You can use functions within this scope launchto easily and safely manage asynchronous operations.

rememberUpdatedState

Can be used when you want to reference a value in an effect if the value should not restart when changed rememberUpdatedState. The LaunchedEffect restarts when any value of the key parameter is updated, but sometimes we want to capture the changed value inside the effect without restarting it. This is very helpful if we have a long-running option where restarts are expensive.

@Composable
fun ParentComponent() {
    
    
     setContent {
    
    
            ComposeTheme {
    
    
                // A surface container using the 'background' color from the theme
                Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
    
    
                    var dynamicData by remember {
    
    
                        mutableStateOf("")
                    }
                    LaunchedEffect(Unit) {
    
    
                        delay(3000L)
                        dynamicData = "New Text"
                    }
                    MyComponent(title = dynamicData)
                }
            }
        }
}

@Composable
fun MyComponent(title: String) {
    
    
  var data by remember {
    
     mutableStateOf("") }

    val updatedData by rememberUpdatedState(title)

    LaunchedEffect(Unit) {
    
    
        delay(5000L)
        data = updatedData
    }

    Text(text = data)
}

Initially, titleit is an empty string. After 3 seconds, titleit becomes "New Text". After 5 seconds, datait also becomes "New Text", which triggers a reassembly of the UI. This updates the Text composition. So the total delay is 5 seconds, if we don't use it rememberUpdatedStatethen we have to restart the second LaunchedEffect which will take 8 seconds.

DisposableEffect

The DisposableEffect composition function is used to execute an effect when the Composable function is initially created. When the Composable is removed from the screen, it clears the effect.

@Composable
fun MyComponent() {
    
    
    var data by remember {
    
     mutableStateOf("") }
    val disposableEffect = remember {
    
     mutableStateOf<Disposable?>(null) }

    DisposableEffect(Unit) {
    
    
        val disposable = someAsyncOperation().subscribe {
    
    
            data = it
        }
        onDispose {
    
    
            disposable.dispose()
        }
        disposableEffect.value = disposable
    }

    // rest of the composable function
}

In this example, we create a Composable MyComponentcalled . It has two mutable state variables: dataand disposableEffect.

In DisposableEffect we call an asynchronous operation someAsyncOperation()which returns Observablea new value which is emitted when the operation completes. We subscribe to it and update it data.

We also use onDisposeto handle the disposable and stop operations, which are called automatically when the Composable is removed.

Finally, we disposableEffect set the to disposablethe object so that calling Composable has access to it.

SideEffect

SideEffectUsed to publish Compose state to non-Compose code. SideEffect Fired on every recomposition, it is not a coroutine scope, so suspending functions cannot be used in it.

When I first found out about this side effect, I was unsure about how and how important it was, so I dug deeper into the issue to gain a better understanding.

class Ref(var value: Int)

@Composable
inline fun LogCompositions(tag: String) {
    
    
        val ref = remember {
    
     Ref(0) }
        SideEffect {
    
     ref.value++ }
        Logger.log("$tag Compositions: ${
      
      ref.value}")
}

When the effect is called, it keeps track of the number of compositions created.

produceState

produceStateConvert non-compose state to compose state. It starts a coroutine, scoped to composition, that can push values ​​into the returned state. A producer starts when produceStateit enters a composition; it stops when it leaves a composition. The returned State is grouped together; setting the same value will not cause a reorganization.

Here's produceStatean example of how to use Loading images from the web. loadNetworkImageComposable functions provide functions that can be used in other composables State. This example is taken from the official documentation.

@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

derivedStateOfis a composable function that can be used to derive new states based on the values ​​of other state variables. It is useful when you need to calculate a value that depends on other values, and you want to avoid unnecessary recalculations.

Here is an derivedStateOfexample of usage:

@Composable
fun MyComponent() {
    
    
    var firstName by remember {
    
     mutableStateOf("") }
    var lastName by remember {
    
     mutableStateOf("") }

    val fullName = derivedStateOf {
    
    
        "$firstName $lastName"
    }

    Text(text = "Full Name: $fullName")
}

In this example, we define a MyComponentComposable function named Composable that takes a mutable state variable firstNameand lastName. We snapshotFlowcreate a new flow variable using fullName, which concatenates the value of firstNameand . lastNameWhenever firstName or lastName changes, snapshotFlowrecombine the Composable and update fullName. Finally, we display using Text Composable fullName. snapshotFlowCan be used to create streams that respond to changes in state without manually managing callbacks or listeners.

snapshotFlow

snapshotFlow is a function that can be used to create a Flow that first emits the current value of a state object and then any subsequent changes. This is useful for creating responsive UIs that respond to changes in state without manually managing callbacks or listeners.

Here is an example of using snapshotFlow in Compose:

@Composable
fun MyComponent() {
    
    
    val count = remember {
    
     mutableStateOf(0) }

    val countFlow = snapshotFlow {
    
     count.value }

    LaunchedEffect(countFlow) {
    
    
        countFlow.collect {
    
     value ->
            // Handle the new value
        }
    }

    Button(onClick = {
    
     count.value++ }) {
    
    
        Text("Clicked ${
      
      count.value} times")
    }

In this example, MyComponent mutableStateOf(0)creates a mutable state object using Then call snapshotFlow, passing in a lambda expression returns the current value of the state object. The resulting countFlowstream emits the current value and any subsequent changes to the state object.

Use a LaunchedEffect to collect data from countFlowthe stream, making sure the collection only happens when the component is active and stops when it is removed. Finally, use the Button to update the value of the state object.

reference

https://proandroiddev.com/mastering-side-effects-in-jetpack-compose-b7ee46162c01

Guess you like

Origin blog.csdn.net/u011897062/article/details/130558254