8

Tricky refactoring of Jetpack Compose code — be careful with side effects

 3 years ago
source link: https://blog.thefuntasty.com/tricky-refactoring-of-jetpack-compose-code-be-careful-with-side-effects-13768275b3db
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Tricky refactoring of Jetpack Compose code — be careful with side effects

Jetpack Compose code is relatively easy to understand most of the time. But as in every other technology, the more you are trying to achieve something complex, the more you need to know about some internals and more advanced concepts. In this article, I will show you an example of how simple refactoring can lead to unexpected issues caused by side effects.

Setup

Let’s say that we have the composable code with the following behavior:

  • It allows the user to insert data into the TextField
  • It shows a snackbar with an indefinite duration. When the snackbar is dismissed, then it does some action with data inserted into TextField.

It is nothing difficult to implement. Even though this is quite an unordinary behavior from the UX perspective, it will demonstrate our problem nicely.

Let’s say that this is a section of our code in which we are interested:

The original version of the Jetpack Compose code

The composable is quite simple. But let’s go over it to be clear about the functionality:

  1. Creates and remembers SnackbarHostState class responsible for showing and hiding the snackbar.
  2. Creates and remembers text state that holds the data from the TextField.
  3. Creates side-effect via LaunchedEffect that runs suspend functions in the scope of a composable. It uses Unit value as a parameter. It means that it will be started only once when the first composition of the parent composable is executed.
  4. Shows a snackbar viasuspended function and stops the given block’s execution until the snackbar is dismissed. Since the snackbar has an indefinite duration, it can be dismissed only via user action.
  5. When the snackbar is dismissed, we perform some action with the text state.
  6. The rest of the code, which is not important right now, is responsible for changing the text state.

The code is quite simple and works as we wanted. Let’s say that this is our result:

1*O79wVqP1B46rBncYHfTVxw.gif?q=20
tricky-refactoring-of-jetpack-compose-code-be-careful-with-side-effects-13768275b3db
Original solution with correct behavior

Everything works as intended. The user is able to insert some data, and when the snackbar is dismissed, these data are used within the LaunchedEffect and presented on the screen.

Let’s start with refactoring

Let’s say that our new feature is finished. The last thing that we want to do is to perform a small refactoring of the code. In order to get more reusable code, we decide to move the LaunchedEffect code into a standalone composable function.

Refactoring: wrapper for LaunchedEffect function

All we did was copy-paste the code from the LaunchedEffect into a new composable function with state and text as arguments. Now we replace the original code with our new function.

At first glance, this is a very simple refactoring. It’s just a matter of moving code from one function to another. What could go wrong? Let’s test our code.

1*LTxYOnZAYwKk0pEct0q1JQ.gif?q=20
tricky-refactoring-of-jetpack-compose-code-be-careful-with-side-effects-13768275b3db
Refactored solution with incorrect behavior

Well, our code is not working anymore. The text value captured within the snackbar now holds the initial data instead of changed data. What is going on?

The problem

Although Jetpack Compose may be seen as some kind of miracle tool that turns functions into UIs, we should not forget that the functions are still functions and the basic principles of Kotlin still apply.

We need to understand how LaunchedEffect works. The first thing that we need to understand is that LaunchedEffect is code for side effects. The code inside of it is not a composable function, so no recomposition is automatically done. The second thing that we need to understand is that it is launched only when the key parameter is changed and then captures its surrounding variables.

Our LaunchedEffect is executed only on the first recomposition because the key parameter is constant (Unit). It means that it captures its environment only once. The problem with the refactoring is that we replaced a reference to the remembered MutableState object with a reference to the string parameter. Even though that text state in the original code is also a string but it is a string wrapped with a delegate.

At first glance, this small refactor may not seem like a significant change, but it is the cause of the invalid result behavior.

Solution

Now, when we know where the problem is, we need to find a solution. We know that LaunchedEffect is relaunched when the key is changed. Since we are using Unit as a key parameter of the LauchedEffect, it is launched only on the first composition and capturing the reference to the initial data only.

In order to capture the most recent value of the text parameter, we can replace the Unit with the text parameter in the LaunchedEffect.

Incorrect solution: replacing Unit constant with the text parameter

This way, we will achieve that LaunchedEffect will be launched every time the composable function is executed with a new text parameter. It sounds like something that is going to fix our problem. Let’s test this change.

1*yn9ETZiEPblN7rLztTQNnw.gif?q=20
tricky-refactoring-of-jetpack-compose-code-be-careful-with-side-effects-13768275b3db
Refactored solution with the correct result but with incorrect snackbar behavior

Well, the result is correct, but we introduced a new problem. LaunchedEffect is executed whenever the text parameter is changed. The previous coroutine scope is then canceled, and a new one is launched. This is causing the effect of showing the new snackbar for every text change.

A correct solution

Changing the key argument is not a good solution. We do not want to launch a new LaunchedEffect when the text is changed. So what can we try as a next step? Well, we could pass a MutableState object as a parameter. If we would pass the same instance and change only its internal value, then it would work. But if we would pass a different instance, then the problem would still be there. So passing some other kind of object is not a good solution.

However, the correct solution to this problem is already created in the Compose runtime. The authors of Compose were aware of this problem and created the rememberUpdatedState function.

Correct solution: use rememberUpdatedState as a wrapped for function parameters

Let’s test the solution with rememberUpdatedState.

1*O79wVqP1B46rBncYHfTVxw.gif?q=20
tricky-refactoring-of-jetpack-compose-code-be-careful-with-side-effects-13768275b3db
Refactored solution with correct behavior

The result captured within LaunchedEffect has the correct value, and the snackbar is shown only once. Our refactored solution is finally working correctly again.

How rememberUpdatedState works?

The rememberUpdatedState may seem mysterious at first glance, but the function is just a wrapper for the following code.

Internal implementation of rememberUpdatedState

It wraps the parameter with remembered MutableState and updates its value on every recomposition. By doing this, the LaunchedEffect is referencing the same instance of the MutableState object instead of the text parameter. So even after recomposition, the LaunchedEffect still references the correct instance, which changes only its internal value.

TL;DR
Always be careful when using the side effects in Jetpack Compose. Especially be cautious whether parameters used within functions such as SideEffect, LaunchedEffect or DisposableEffect, hold the most recent value. This can be achieved by referencing remembered state (rememberUpdatedState) or passing the value as the key parameter of the side effect function.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK