Advanced state and side effects in Jetpack Compose

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
[MUSIC PLAYING] ALEJANDRA STAMATO: Hi there. I'm Alejandra, engineer on the Android Developer Relations team. MANUEL VICENTE VIVO: Hi, everyone. I'm Manuel-- same team, same title. ALEJANDRA STAMATO: Welcome to advanced state and Side Effects in Jetpack Compose workshop. In this code lab, you will learn all about complex state management in Compose and side-effect APIs. MANUEL VICENTE VIVO: Yeah, in particular, you will learn how to produce state and observe streams of data in Compose to update the UI, how to create a state holder for stateful composables, side-effect API's, such as LaunchedDefect, RememberUpdatedState, reviewState, and deriveStateOf, and also how to create coroutines and code Suspend functions in composables using the rememberCoroutineScope API. Quite exciting, isn't it? ALEJANDRA STAMATO: So you might remember us from another code [INAUDIBLE],, in which we built a wellness app. If you haven't solved the [INAUDIBLE] codelab and are unfamiliar with basic handling state in Compose, with APIs like MutableState and Remember, make sure you check that one first. You can find the link in the video description below. MANUEL VICENTE VIVO: Today, we will work on an unfinished application [INAUDIBLE] material study app which consists of our travel search app that helps you find flights, hotels, and restaurants given different parameters, like amount of people, location, and dates. We'll go step by step through the codelab, adding features and refactoring the code to improve it-- for example, populating a list of suggested destinations and adding a splash screen to the app. ALEJANDRA STAMATO: So I'm going to be sharing my screen with a combo of text, Android Studio, and the emulator so you can easily follow along as we solve it. As always with codelabs, we'll advise that you try to solve it as you watch or maybe later. But do try to solve it yourself so all the concepts really stick. Also, I already completed step number two of the codelab, which is setting up the environment and downloading the app Start Code in the main branch. MANUEL VICENTE VIVO: OK, now that we know what we need, what we are building, and we have the code ready, let's take a look at the UI state production pipeline section. ALEJANDRA STAMATO: So the first feature that we want to implement in creating our travel sample app is populating the list of suggested destinations so it looks like our design. If we run the app from a main branch right now, we see that this list is empty. And to fix this, we have to complete essentially two steps. Add the logic to the View model to produce the UI state, which is the list of suggested destinations. And consume the state from the UI to show the list. Let's see how to do it. MANUEL VICENTE VIVO: For any kind of app, we recommend implementing a layered architecture to a very common, good architectural principle like separation of concerns, single source of truth, and testability. That's the architecture you can see in this app. To populate the UI with data coming from other layers of the hierarchy, we use a UI state production pipeline. This is a process in which we typically access the data layer from the View model or any other state holder, apply business rules if needed, and then expose the screen noise [INAUDIBLE] in an observable data folder class. The UI consumes that UI state and displays the information on the screen. If you're interested in learning more about UI state production, check the link in the video description. ALEJANDRA STAMATO: So the data layer in this application is already implemented. What we have to do now is use that data layer to produceState. So we can open the main View model. And we can define a suggested destination variable of type, MutableStateFlow, to represent the list of suggested destinations and set an empty list as the initial value. And this is a variable. It's private and will only be mutated in this View model. And next up, we can define an immutable variable, suggested destination just below, of type state flow. This is the public read only variable that can be consumed from the UI. And also, I'm using an extension as state flow that will transform the mutable state flow with defined V4 from mutable to immutable. MANUEL VICENTE VIVO: Yeah, this is a general good practice. You ensure the screen UI state cannot be modified unless it goes through the View model. Thus, we can say that the View model is the single source of truth for the screen UI state. ALEJANDRA STAMATO: So in the ViewModels, any block now, what we can do is just access a data layer, calling the Repository.destinations and assign it to our prior variable suggestedDestinations we defined before. And finally, we can uncomment all the usages of suggestedDestination in this class, for example here, here, and here. And these will make our variable to be properly updated with events that are coming from the UI. The screen has selectors, for example, the Adult selector and Destination selector. And our suggested destination will be updated when you change the amount of people in this function or when you change, for example, the destination in this other method. MANUEL VICENTE VIVO: This is great. The first step is done. Now, the View model is able to produce a UI state. Next up, we need to consume the UI state from the UI. ALEJANDRA STAMATO: So our list is still empty. So let's open CraneHome. And look at CraneHome contents. So CraneHomeContent composable contains the BackdropScaffold. And this component allows you to define a back layer content or the part of the screen where the selectors are, like the select number of adults and destination. And then you have frontLayerContent, which is where our list of suggested destinations will be. So we want the UI to update and show the list of suggested destinations here whenever there is a new item emitted into the stream of data. And to help us do that, we can use a collectAsStateWithLifecycle API. MANUEL VICENTE VIVO: To start using the collectAsStateWithLifecycle API, first, add the Lifecycle Runtime compose dependency to your app's d.gradle file. The variable lifecycle_version is defined already in the project with the right version, at least 2.6. Now we can sync the project. ALEJANDRA STAMATO: Great, so now, we can go back to CraneHomeContent. We can assign suggestedDestinations to a call to viewModel.suggestedDestinations, which is our flow. And then we use collectAsStateWithLifecycle to collect from that flow. And then we can run the app in the meantime. This API will collect the values from the state flow and map the latest values to Compose state in a lifecycle-aware manner. And this allows your app to save app resources when not needed, such as when the app is in the background. Now, the Compose codes within this state value recompose on new omissions So we run the app now. We see that the list of destinations is actually populated. Also, this list updates thanks to the logic present in the View model. For example, when you change the amount of adults, it changes. And when you change the to destination-- for example, Argentina. MANUEL VICENTE VIVO: Compose also offers APIs to transform other stream-based solutions, like [INAUDIBLE] data into Compose state. Check the Compose and Other Libraries documentation to learn more about that. [MUSIC PLAYING] OK, so we just learned how to produce the UI state from the state folder using state flow and how to consume it in the UI. Let's move on to the next step in the codelab. ALEJANDRA STAMATO: So next step in this step of our project, we want to add a landing screen to the app, which could be used to load all the data needed in the background. In a real app starting in Android 12, you should use the Android Splash API instead of implementing a custom splash. So keep it in mind that this is just an example to illustrate the next API. So for a splash screen, we want to show the full screen with the logo. Then there will be a delay as a simulation of data being loaded in the background. And then we navigate to the main Home screen. There is already a file called LandingScreen, which we are going to open now, which is where we can put the codes that we need to implement. And somewhere in our landing screen, we're going to make a call to our backend to load data. And we can represent this if we remove this with a call to delay on SplashWaitTime. The problem is that we cannot just run a Suspend function here just like that. We have two problems. The first one is obvious. This does not compile, as it says. Suspend Function Delay should be called only from a [INAUDIBLE] or another Suspend function. So we need another API to help us here. But also, we shouldn't just call this method like this in this composable function because remember, composable functions can run multiple times. This can make our backend call run multiple times too, which can be expensive. And also, composables can be restarted in the middle of the call without you knowing. So for more on how Compose works, you can check the Thinking in Compose docs linked below. MANUEL VICENTE VIVO: Calling the network to fetch data asynchronously is a side-effect. And we need a way to run side-effects in a safe and controlled way in Compose. In general, a side-effect is a change to the state of the app. When talking about side-effects in Compose, we are referring to invoking side-effects from a composable and altering the state of the app outside the scope of that composable function-- for example, opening a new screen when the user taps on a button, showing a message when the app doesn't have internet connection, sending analytics events, or calling the network, like is the case for this section of the codelab. ALEJANDRA STAMATO: OK, so back to our code, how do we safely run the Suspend function inside my composable? MANUEL VICENTE VIVO: To call Suspend functions safely from inside the composable, use the Launch Defect API, which triggers a CoroutineScope side-effect in Compose. ALEJANDRA STAMATO: OK, so I can wrap my Delay call in a LaunchedEffect, like so, which launches a coroutine with a block of code passed as a parameter. And the coroutine will be canceled if LaunchedEffect leaves the composition for any reason. MANUEL VICENTE VIVO: LaunchedEffect takes keys as parameters that are used to restart the defect whenever one of those keys changes. What happens is that the ongoing coroutine is canceled. And a new coroutine is triggered with the new state values. ALEJANDRA STAMATO: OK, then I need the key. What if I use onTimeout as my key? Will that work? And then I can just call onTimeout after the delay to trigger whatever happens after the data is loaded. MANUEL VICENTE VIVO: Sure, that would work. In this case, if onTimeout gets a new value, for example, when the parent composable changes how the lambdas will behave, then the effect would restart. However, for this particular use case, it doesn't make that much sense, right? We wouldn't want to restart the effect when the lambda changes because that will restart the delay. And it will take longer. Ideally, we would like the effect to keep running even if the lambda changes. ALEJANDRA STAMATO: OK, I can fix this by changing the key. To trigger the side-effect only once during the lifecycle of this composable, we can use a constant as a key-- for example, LaunchedEffect of [INAUDIBLE] MANUEL VICENTE VIVO: Yeah, with the constant, the effect will be triggered just once when this composable enters the composition. However, I think we still have some issues to fix. If onTimeout changes while the side-effect is in progress, there is no guarantee that the new onTimeout is called when the effect finishes. We want the LaunchedEffect to invoke the delay only once during the lifecycle of this composable function and also ensure that we invoke the latest onTimeout lambda value. ALEJANDRA STAMATO: We can guarantee this by remembering onTimeout using that rememberUpdatedState API. So it will capture and update the newest value. So here, we define currentOnTimeout with rememberUpdatedState, which wraps onTimeout lambda. And now, we call currentOnTimeout instead of onTimeout directly. MANUEL VICENTE VIVO: Exactly. rememberUpdatedState is actually a very important API. It should be used when parameters or values computed during composition are referenced by a long-lived lambda or object expression, which might be common when working with side-effects. So as a general rule of thumb, when using lambdas in a side-effect, always think if you should be using a rememberUpdatedState. Most of the time, the answer might be yes. If you want to restart effect when the lambda changes, then don't use this API and not the lambda as a key to the effect. ALEJANDRA STAMATO: Great. So when the landing screen is completed, it loads data in the background. And after it's done, it calls the currentOnTimeout, which calls the onTimeout lambda. So now, our second step is to actually call the landing screen somewhere to display it when the app is open. So we go to our main activity. We find the main screen, which is the first screen that we showed in the app, the first composable. And it will call CraneHome, which contains search parameters and the list of suggested destinations. So instead of just calling CraneHome directly, we can define a variable, showLoadingScreen, which is a variable that will determine whether to show or hide the landing screen. And then we can write an if statement. So if showLoadingScreen, is true, we show the landing screen-- else, we just go to CraneHome. Now, landing screen receives an onTimeout lambda parameter, which is called after the delay. So we want to change showLandingScreen to be false, which will make the composable recomposed. And when the function recomposes, showLandingScreen is false. Then we navigate to CraneHome. We ran it very fast. But we can run it again to see that if we run the app, we show the landing screen for two seconds. And then we navigate straight into our CraneHome. MANUEL VICENTE VIVO: Cool, nice. That's a pretty nice splash screen. We've implemented it like this for learning purposes. But again, if you want to add a proper splash screen to your app, consider the SplashScreen API that was added in Android 12. It also provides a compact library that supports older versions-- link in the description. [MUSIC PLAYING] OK, we learned a lot of things in this section of the codelab-- what a side-effect is and how to use launch effect and rememberUpdatedState. What's next? ALEJANDRA STAMATO: In this next step, we'll make the navigation drawer work. Currently, nothing happens when you try to tap the Drawer icon. So you will open the CraneHome file. And we go to CraneHomeContent here. There is an openDrawer event that we need to complete. And we have a scaffoldState defined with a rememberScaffoldState remember function. ScaffoldState has a draw state with methods to open or close the navigation drawer programmatically. So we can just call scaffoldState.drawer.open to open the navigation drawer. And that should be that. However, we see an error. And that's because open is a Suspend function. And we get an error, the same as we did before. We need to run this in a coroutine. MANUEL VICENTE VIVO: Actually, some Compose APIs are Suspend functions. Suspend functions help represent concepts as they happen over time. And this is the case of opening the navigation drawer. Open suspends the execution of decrypting, where it's been called, until it finishes. Then the coroutine resumes execution. A Suspend function needs to be called for my coroutine. But the openDrawer function is just a simple regular function. And here, we cannot use LaunchedEffect because we are not in a composable context. ALEJANDRA STAMATO: Right, so I cannot use LaunchedEffect like before to run my Suspend function because we get a different error, which is LaunchedEffect can only be run in composition. MANUEL VICENTE VIVO: Yeah, so how can we create a new coroutine here? ALEJANDRA STAMATO: So ideally, with [INAUDIBLE] coroutine scope that follows the lifecycle of the call site. And we can use that rememberCoroutineScope API to create a coroutine scope. With this scope, you can start coroutines when you're not in composition, like for example, here. We just call Launch and run our scaffoldState.drawerState.open. We just run the app. And then in the [INAUDIBLE] drawerContent, we just call this function. And that's it. The scope will be automatically canceled when this composable leaves the composition. And if we run the app now, we see that the navigation drawer works. And our drawer works. MANUEL VICENTE VIVO: Cool, nice, thank you. The last part of this section of the codelab explains the difference between LaunchedEffect and rememberCoroutineScope. Make sure to check it out. [MUSIC PLAYING] Let's move on to the next section of the codelab. We'll see how to create a new state holder. In our app, if you tap on Choose Destination, you can edit the field and filter cities based on your search input. The text weight also changes from regular to bold. There is a fair bit of logic unstated there. Let's take a look and see how we can improve this code. ALEJANDRA STAMATO: Great. So the composable for Choose Destination is editable user input. The CraneEditableUserInput composable takes some parameters, like hints and caption. And caption corresponds to the optional text next to the icon where you type your destination. In our case, it's this To: string beside Spain. MANUEL VICENTE VIVO: This implementation has some downsides. We can make it better. For example, the value of the text field is not hoisted. And therefore, cannot be controlled from outside. But also, the logic of this composable could become more complex. And the internal state could be out of sync. Something we can do to alleviate this is create a state holder responsible for the internal state of this composable so that you can centralize all state changes in one place. ALEJANDRA STAMATO: Definitely, let's do that. Let's create a plain state holder class name, EditableUserInput in the same file. And let's see a little bit what we have now. So this class has a text, which is a mutableStateOf type string. And we define it as mutable state because Compose tracks changes to the values and recomposes when changes happen. And we expose also an updateText method to modify the text instead of making the text [INAUDIBLE] public. So now, this class is a single source of truth to modify the text. The class also takes an initial text, which is just dependency that is used to initialize our text. And the logic to know if the text is a hint or not is in the isHint method here, which performs the check on demand. Now, if these logics get more complex in the future, we only need to make changes to this one class, EditableUserInputState. MANUEL VICENTE VIVO: This will simplify things for our future self. I think it's a good practice to follow. Compose offers this type of classes for their own composables as well. You might have already encountered the scaffoldState and LazyListState state holders. And actually, a typical Compose pattern we can do now is providing a function to remember the state, like rememberScaffoldState or remember LazyListState. In this way, you can create state more easily and keep the same instance in the composition instead of creating a new one every time. For our case, we could call this function rememberEditableUserInputState. ALEJANDRA STAMATO: Sure, let's implement that in this very same file-- remember, EditableUserInputState. So we define RememberEditableUserInputState function that will remember an instance of EditableUserInputState It also takes hints as a key. So the hint changes. And there is a recomposition. This class instance will be recreated with the newest value. And now, if we only remember this state, it won't survive activity recreations. So we can use rememberSaveable instead, which behaves similar to remember. But now, the stored value will also survive activity and process recreation. MANUEL VICENTE VIVO: rememberSaveable uses the safe instance state mechanism under the hood, which stores information inside a bundle. The bundle can store primitive types without any further instructions. But our state here is a little bit more complicated. ALEJANDRA STAMATO: Exactly. We need to tell rememberSaveable how to save and restore an instance of this class using a saver. MANUEL VICENTE VIVO: In our case, for the saver of the EditableUserInputState class, we can use some existing Compose API, such as ListSaver or MapSaver to reduce the amount of code that we need to write. This function stores the values to save in a list or a map, respectively. ALEJANDRA STAMATO: So it's a good practice to place the saver definitions close to the class they work with because they need to be statically accessed. So let's implement it. We first define a companion object in EditableUserInput plain class. Then we can write a Saver. A Saver describes how an object can be converted into something that is saveable. Implementations of a Saver need to override two functions, Save and Restore. In this case, we use a listSaver as the implementation detail of the Saver to store and restore an instance of EditableUserInputState. In the Save function, we are storing all of the class parameters as a list. And in the Restore, you can retrieve them by the position that they occupy on this list. We have our Saver completed. Now, we can go to rememberEditableUserInputState, where we can use our new Saver. So we go and we write our Saver as EditableUserInputState of Saver. And now, rememberSaveable knows how to store and restore an instance of our class. But keep in mind, because rememberSaveable will store state in a bundle, which has limited size, you just store the minimum state required-- very simple objects, IDs, or keys. And avoid storing heavy objects or a list of objects. To know more, see the Saving UI State documentations in the link below. MANUEL VICENTE VIVO: Nice. Let's see how this looks like from the holder side. Instead of having the state and the logic in the composable function itself, we need to delegate that complexity to the state holder. And to make this function more reusable across the app and in test previews, we should allow hoisting. ALEJANDRA STAMATO: Yep, let's do that. We'll use EditableUserInputState instead of Text and isHint. First, we want to replace Hint with our state. And we can give it a default value so that we don't always have to pass state in simpler cases, which is a best practice. And for this, we can use our rememberEditableUserInputState that we defined before with an empty Hint. We can also remove the onInputChanged lambda event because all of the state is hoisted now. So [INAUDIBLE] need to know the input changed. They can define the state. They can hoist define the state and pass it into this function. And next, we can remove the internal state. We're not going to need it because now, we have our state implemented like so. And now, it's a matter of just-- obviously, our function doesn't compile. Just make the tweaks necessary to use our state instead of the internal state that we had before. So now, we write state.isHint like so everywhere that we need. We are not going to need this isHint call like here. In the value of the basic [INAUDIBLE] we call state.text. And every time there is a value change, we call state.updateText with the latest value to update the text. And since we changed the API, we need to check in on the places where it's called to make sure we're passing the appropriate parameters. And if we go and find the instances of this, we're going to see that the only place that calls this composable is ToDestinationUserInput, which we need to fix. But first, let's quickly see where we are in our composable structure. Search content has three types-- Fly, Sleep, and Eat. Right now, we're in the Fly tab of FlySearchContent. And on the selectors, to change things like people or destinations are here. And ToDestinationUserInput is just the wrapper around CraneEditableUserInput. So now, we are in ToDestinationUserInput composable. You should see a build error. We need to fix the parameters because we no longer have Hint or onInputChanged. So let's start by removing on InputChanged, like so. And for Hints, in order to define a state with an initial value different than the default, we can define the state here with RememberEditableUserInputState and pass the same Hint it originally had. We replace Hint parameter with State. We're calling it State now and passing EditableUserInputState. And that's it. That's our composable compiling now. MANUEL VICENTE VIVO: Cool, nice. And thanks for showing the diagram. It's much easier to see that. In ToDestinationUserInput, there is some functionality missing. We need to notify colors when the input changes so that they can react to it and apply logic when that happens. Due to how the app is structured, we don't want to hoist the EditableUserInputState any higher up in the hierarchy because we don't want to couple other composables, like FlySearchContent, with it. How can we call the onToDestinationChanged lambda from here and still keep this composable reusable? ALEJANDRA STAMATO: So we can trigger a side-effect every time the input changes and call the onToDestinationChanged lambda that we're not using yet. So let's define a current onDestinationChanged variable and use rememberUpdatedState to wrap the onToDestinationChanged lambda, in turn. And this is exactly what we did before because we are going to use this inside a launchedEvent. Then, we will create a launchedEffect. And using EditableUserInputState as key-- and don't worry too much about this now. We're going to explain it later. And the next code right now, what we are doing is right here, observing-- whenever we have new changes in our text, filtering out whenever it's a Hint because we're not interested in that and collecting every new event where there is a new text and calling CurrentonDestinationChanged with the new text. MANUEL VICENTE VIVO: Oh, and there is actually a new API that you are using there, right? snapshotFlow converts Compose state objects into a flow. When the state read inside snapshotFlow mutates, the Flow will emit the new value to the collector. In our case, we want to convert this data into our Flow to use the power of Flow operators. With that, we filter when the text is not a Hint and collect the emitted items to notify the parent that the current destination changed. ALEJANDRA STAMATO: Exactly. And now that the code is complete, let me explain the decisions that we made. So first, because we're using a lambda instead of the LaunchedEffect, we use the RememberUpdatedState API to guarantee that the latest value passed to onDestinationChanged is used. And then we're passing EditableUserInputState as the key to the LaunchedEffect because we want the effect to restart-- if the toDestinationUserInput composable is recomposed with a new state instance. If we don't do this, even if the function recomposes, this LaunchedEffect might keep running using an incorrect version of the latest state. And apart from seeing an incorrect behavior in your app, you might also be leaking all state in memory. MANUEL VICENTE VIVO: From what you explained and what we've seen so far, I think there are two rules of thumb we can pretty much follow all the time. We already explained the first one. And it's if you're using a lambda inside a side-effect, consider wrapping it inside a RememberUpdatedState. The second one is about keys to use with effects. Almost every instance used inside a side-effect should be considered a key unless you know that the instance is very unlikely to change. In our LaunchedEffect, we are using the state instance to perform operations. That's why we are adding [INAUDIBLE] ALEJANDRA STAMATO: Exactly, and there are no visual changes in this step of the codelab. But we've added a state holder to a UI element to make it more robust, encapsulating state and UI logic to improve its reusability. If we run the app now, we should see that everything is working like it did before. [MUSIC PLAYING] In this section, we'll improve how the Detail screen starts. Whenever we go and click on a city, we show the Details screen. This is what we'll be working. So we go to Details Activity and find the Details screen, which represents the whole screen. And we see that we get the cityDetails by calling viewModel.cityDetails, which has the city name, country, description, and image URL. And then if the result is successful, we just show that composable's DetailsContent-- else, we show an error. Now, imagine if cityDetails takes longer to load because it requires a lot of complex data. You don't want your user staring at a blank screen for too long. In this case, what we could do is add a loading screen to show what data is loading and when the data is ready, display the Details content. Let's see how we can build that. MANUEL VICENTE VIVO: One way to model the state of the screen is with a data class that exposes multiple but related pieces of state, data to display on the screen and then loading network signals. ALEJANDRA STAMATO: So this class, Details UI State, can have the information we need. We have city details, like name, the coordinates of the city. It's loading to represent if the screen is loading. Or if we get an error, we have Boolean throw error. Then in the DetailsViewModel, we could define a new pair of variables, like so. And uiState is a StateFlow. Just like we have before, we expose a public version of a variable with a private backing variable as well. And this Flow can be updated inside the DetailsViewModel when the information is ready or fails. And then in the UI, Compose can collect from the stream with collectAsStateWithLifecycle API that you already know about. But let's do something different this time. MANUEL VICENTE VIVO: Yeah, for the sake of this exercise of learning a new API, we are going to implement an alternative to the UI state production solution we saw in the first steps. Instead of producing the screen UI state in the View model, now, we are going to do it in Compose code using the produceState API. produceState allows you to convert non-Compose state that you can get from other layers of the hierarchy into Compose state, so that composables can recompose when that state changes. I think it's interesting to see how this API works because these other stream wrappers, like collectAsStateWithLifecycle, use it under the hood. ALEJANDRA STAMATO: So let's rewrite this code entirely. So first, let me clear this part of the code. And let's define our UI state with the produceState to emit UI state updates. So this API receives an initial value. And for this, we can send DetailsUiState with an isLoading value of True. So the first thing the user will see when opening the screen is the Loading screen. And then we have a lambda producer call. So the first thing we can do is, just like we had before, have a call to viewModel.cityDetails. And then we can assign our value variable. So if cityDetail's result is successful, we omit DetailsUiState with the result of loading that data, else, we omit DetailsUiState with a True value for throwError because there's been an error. MANUEL VICENTE VIVO: Cool. produceState launches a coroutine scoped to the composition that can push values into their returned state using the value property that you can see there. Because we are inaccurate in context, we can do asynchronous calls from here and update our state. ALEJANDRA STAMATO: So finally, let's add a when statement where we map different values of the DetailsUiState to composables. Depending on the UI state, if UiState.cityDetails is not known, it will show the details. If the value is loading, it will show a circular progress indicator, which is our loading spinner. Else, we show the Error composable. Now, if we run the app now, you won't see probably the loading screen because for now, loading is too quick for us to see any progress bar. But if the code was doing something more interesting, like fetching data from the network, you would see it spin, as it would take a little bit more to load. So just to test it, what we can do is just add a fake delay to our producer call. And we can run the app in the meantime to imitate what would happen if this call took longer. And we can add the delay directly to the producer call as Manuel was saying before, because produceState launches the coroutines [INAUDIBLE] to the composition. And now, if we go and open any city, we see the spinner. And then we navigate to the Details screen. MANUEL VICENTE VIVO: Cool, nice. Our recommendation is to produce the screen UI state in a view model class that can access the data layer. But if your UI state is very simple and doesn't need to communicate with other layers of the app, produceState is the best Compose alternative. You can learn more about this in the UI State Production Architecture documentation. [MUSIC PLAYING] ALEJANDRA STAMATO: Well, I think we're ready to move on to our last and final API. The last improvement we are going to make to Crane is showing a button to scroll to the top in the list of suggested destinations. It will show this button when you scroll the list and you pass the first element on the screen. Tapping the button takes you to the first element on the list. So let's open Explore section. And let's see the ExploreSection composable, which contains what we see in the bottom sheet, like the section title, [INAUDIBLE] column, and different events. So to calculate whether the user has passed the first item, we can define a showButton variable. We use Lazy columns, LazyListState sta and check if firstVisibleItemIndex is larger than zero. And this is an [INAUDIBLE] implementation that gets the information that we need. The rest of the code should be familiar. The first thing we want to do is wrap this code-- a bit of refactor here. Wrap this code in a box so our button shows on top of the Explore section as we scroll. And then we add a simple if statement. So if showButton is true, we are going to show the FloatingActionButton-- else, we hide it. Now, in order to scroll to an item, we need to implement onClick of our FloatingActionButton. And to do this, we can just call LazyListState now that we have it-- .scrollToIteam. But again, this is a Suspend function. So we need a coroutineScope. And we already know how to solve this. We can define a coroutineScope and use this coroutineScope to launch a coroutine. And we use it to run our Suspend function, like so. And that's pretty much all of the codes done. Now, if we run the app, we go to a list of suggested destinations. And when we scroll, we should see our button. It shows up whenever we scroll past the first item. We tap on the button. We go to the top of the list. MANUEL VICENTE VIVO: That button is actually very handy. The implementation works. But I think we can make it more efficient. Right now, the composable scope [INAUDIBLE] showButton recomposes as often as first visible item index, which happens frequently when scrolling. ALEJANDRA STAMATO: Yeah, and also notice that Android Studio complains as well. There is an error saying that frequently changing states should not be directly read in a composable function. MANUEL VICENTE VIVO: Exactly. Instead, we want the composable scope to recompose only when the condition toggles between true and false or vise versa. There is an API that allows us to do this, the derivedStateOf API. ALEJANDRA STAMATO: The derivedStateOf is used when you want to compose state that's derived from another state. The derivedStateOf calculation block is executed every time the internal state changes. But the composable function will recompose only when the result of the calculation changes. This minimizes the amount of time functions reading showButton recomposes. MANUEL VICENTE VIVO: ListState is composed state. And our calculation also needs to be composed state because we want the UI to recompose when its value changes to show or hide the button. ALEJANDRA STAMATO: Using the derivedStateOf API in this case is a better, more efficient alternative. But [INAUDIBLE] is still giving us a warning. And it's saying, creating a state object during composition without using Remember. So we still have one last change to make. And this is using Remember together with derivedStateOf. So the calculated value survives recomposition. And this is very similar to what we do when we use mutable state together with remember. Now we're all done. Our code works. And it's super efficient. Make sure you check out the, When Should I use derivedStateOf blog post linked in the video description to learn more about one of my favorite APIs. [MUSIC PLAYING] So you did it. You've completed the advanced state and side-effects in Jetpack Compose workshop. MANUEL VICENTE VIVO: Well done! Today, you learned complex concepts about how to produce UI state and how to consume it from the UI. You also learned what side-effects are and the different APIs that you can use. ALEJANDRA STAMATO: A lot of APIs. Let's do a quick recap. MANUEL VICENTE VIVO: Use LaunchedEffect to call Suspend functions safely in the composition. rememberUpdatedState to guarantee the usage of the latest lambda value plus the subparameter in a side-effect. And rememberCoroutineScope to launch coroutine scope to the composition outside the scope of a composable function. ALEJANDRA STAMATO: produceState to produce Compose data asynchronously in the composition and derivedStateOf to derive Compose state from another state efficiently. Two more takeaways to remember before we leave-- if you're using a Lambda inside a side-effect, consider wrapping it inside the rememberUpdatedState guarantee the latest pass value is always used. MANUEL VICENTE VIVO: And if you are using objects inside a side-effect, consider having them as keys to restart the side-effect whenever one of those objects changes. Strong emphasis and consider-- stop for a second. And think about it. Think if you want to restart the effect or not. It might be the case or it might not. Hashtag, it depends. ALEJANDRA STAMATO: After you're done with the codelab, you can check out the code in the end branch to compare your solutions and see if you missed anything. Also, we recommend taking a look at now in Android, our sample app that showcases multiple of the best practices we saw here today. All the important links of APIs, documentations, and blocks are attached in the video description. MANUEL VICENTE VIVO: Thanks for watching this workshop. And please let us in the comments if you found the guide useful, and also what you would like to know more about. Thanks, and have a good one. Bye! [MUSIC PLAYING]
Info
Channel: Android Developers
Views: 26,330
Rating: undefined out of 5
Keywords: Android, Google I/O, Google IO, IO, I/O, IO 23, I/O 23, Google I/O 23, Google IO 23, Google I/O 2023, Google IO 2023, IO 2023, Google New, Google Announcement, Google Developers, Developer, Development
Id: TbxCz5AljQk
Channel Id: undefined
Length: 41min 20sec (2480 seconds)
Published: Wed May 10 2023
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.