Jetpack Compose: force recomposition when a state changes from another activity

Issue

I have two Activities: ActivityOne.kt and ActivityTwo.kt. Both of them use Jetpack Compose to display an UI. In the first one, an item (let’s say, a Text) has to be shown with variable showtText is true, and hidden when it’s false. I achieve this by using:

@Composable
fun MyUI(){   
 AnimatedVisibility(visible = viewModel.showText) {
      Text("Some text")      
   }
}

My variable showText is defined in a ViewModel such as:

val showText by mutableStateOf(false)

That way, anytime I change the value of showText within my ViewModel when ActivityOne.kt is visible, it appears or disaappears. What I want to achieve is the following:

  1. From ActivityOne.kt the user can navigate to ActivityTwo.kt (with the first one running in background, it’s not cleared from the stack).
  2. There, there’s a Switch that can toggle showText value.
  3. When the user press "back", ActivityTwo.kt calls finish() and it dissapears, showing again ActivityOne.kt (it was in the stack).
  4. If user has toggled showText value in ActivityTwo.kt, the text should be hidden or shown automatically, as the state of showText has changed.

The problem is that, although the value of showText does change, the UI in ActivityOne.kt does not respond to those changes. I’ve checked that recomposition of ActivityOne.kt‘s UI is not taking place after ActivityTwo.kt is finished, because it is not remembering the previous state of showText.

How could I achieve that? Thanks in advance!

EDIT 1
The problem is a little more difficult as I explained, but the basis are the same. Here is my complete code:

ActivityOne.kt:

class ActivityOne : ComponentActivity() {

    private lateinit var vm: MainViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val vmFactory = MainViewModelFactory()
        vm = ViewModelProvider(this, vmFactory).get(MainViewModel::class.java)
        setContent {
            MyTheme {
                Surface {
                    MyUI()
                }
            }
        }
    }

    @Composable
    fun MyUI() {
        AnimatedVisibility(visible = myPrefs.showText) {
            Text("Some text")
        }
    }
}

MainViewModel.kt:

class MainViewModel : ViewModel() {
    companion object {
        val myPrefs by mutableStateOf( AppPrefs() )
    }
}

ActivityTwo.kt:

class ActivityTwo : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyTheme {
                Surface {
                    MyUI2()
                }
            }
        }
    }

    @Composable
    fun MyUI2() {
        val showText = remember {
            mutableStateOf(MainViewModel.myPrefs.showText)
        }

        Switch(
            checked = showText.value,
            onCheckedChange = {
                showText.value = it
                MainViewModel.myPrefs.showText = it
            }
        )
    }
}

AppPrefs.kt:

class AppPrefs {
    var showText: Boolean = false
}

Solution

The recomposition is not taking place because mutableStateOf is tracking the state of myPrefs which is never changed (the reference itself never changes).

You can achieve the recomposition in a few ways.

Here I will be assuming that your AppPrefs class either already contains more than one member/field, or that you would want to add more members to it in the future easily. That is why I added 2 more properties to it in the samples below and I only suggested solutions where adding more members won’t affect the existing code.

Option 1

If you can change the AppPrefs class to track the state of each property individually, then make these changes

class MainViewModel : ViewModel() {
    companion object {
        val myPrefs = AppPrefs()
    }
}

class AppPrefs {
    var showText by mutableStateOf(false)
    var prop2 by mutableStateOf(0)
    var prop3: String? by mutableStateOf(null)
}

Everything else stays the same. Here the recomposition works again because now the state is tracked on members that are actually being changed.

Option 2

If you can’t (or don’t want to) have mutableStateOf inside AppPrefs you can change AppPrefs from a normal class to a data class. In that way you get the copy function automatically implemented (alongside equals, hashCode, toString and componentN for destructuring support).

class MainViewModel : ViewModel() {
    companion object {
        // the only change here is val -> var
        var myPrefs by mutableStateOf(AppPrefs())
    }
}

// data class instead of a normal class
data class AppPrefs(
    val showText: Boolean = false,
    val prop2: Int = 0,
    val prop3: String? = null,
)

In this case you have to also change how you change the value of myPrefs. This is what makes the recomposition work correctly again

    onCheckedChange = {
        showText.value = it
        MainViewModel.myPrefs = MainViewModel.myPrefs.copy(showText = it)
    }

In case you cannot use a data class, then you can still go with option 2, but you implement the copy function on your existing class

// normal class with a copy function
class AppPrefs(
    val showText: Boolean = false,
    val prop2: Int = 0,
    val prop3: String? = null,
) {
    fun copy(
        showText: Boolean = this.showText,
        prop2: Int = this.prop2,
        prop3: String? = this.prop3,
    ) = AppPrefs(showText, prop2, prop3)
}

As a bonus, if you go with any of the above solutions, you can even simplify your existing code for the MyUI2 composable and the recomposition will still work.

Example:

@Composable
fun MyUI2() {
    val showText = MainViewModel.myPrefs.showText

    Switch(
        checked = showText,
        onCheckedChange = {
            // for Option 1
            MainViewModel.myPrefs.showText = it
            // for Option 2
            MainViewModel.myPrefs = MainViewModel.myPrefs.copy(showText = it)
        }
    )
}

Answered By – Ma3x

This Answer collected from stackoverflow, is licensed under cc by-sa 2.5 , cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply

(*) Required, Your email will not be published