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:
- From
ActivityOne.kt
the user can navigate toActivityTwo.kt
(with the first one running in background, it's not cleared from the stack). - There, there's a
Switch
that can toggleshowText
value. - When the user press "back",
ActivityTwo.kt
callsfinish()
and it dissapears, showing againActivityOne.kt
(it was in the stack). - If user has toggled
showText
value inActivityTwo.kt
, the text should be hidden or shown automatically, as the state ofshowText
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
}
CodePudding user response:
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)
}
)
}