I have the following issue where the state change on the counter variable does not trigger recomposition of the Text component. Any ideas of what might be the problem???
#Model
class CounterState(var counter:Int = 0)
#Composable
fun CounterView(counterState: CounterState){
Column {
Text(text= "${counterState.counter}",
style=(+typography()).body1)
Button(text ="Increment",
onClick = {counterState.counter++},
style = ContainedButtonStyle())
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
app {
CounterView(CounterState())
}
}
}
var counter by +state { 0 }
Column {
Text(
text = "$counter",
style = (+typography()).body1
)
Button(
text = "Increment",
onClick = { counter++ },
style = ContainedButtonStyle()
)
}
Related
I am trying to update a list which is kept as state of a composable view, but the composable view is not recomposed even though the values of list are changed.
var list = remember {mutableStateOf(getListOfItems())}
ItemListView({ selectedItem ->
list.value = updateList(selectedItem, list.value)
}, list.value)
private fun updateList(selectedItem: String,
itemsList: List<Product>): List<Product> {
for (item in itemsList){
// selected item in the view is updated
item.selected = item.name == selectedItem
}
return itemsList
}
Any idea why the composable is not getting updated? I noticed the issue happens when using a List as state.
You need to update value of MutableState, which is a List in your example, to trigger recomposition not any property of value you set to MutableState.
#Stable
interface MutableState<T> : State<T> {
override var value: T
operator fun component1(): T
operator fun component2(): (T) -> Unit
}
But there is another function called mutableStateListOf() which can trigger recomposition when you add or remove items, or update existing one with new instance.
val list =remember {
mutableStateListOf< Product>().apply {
addAll(getListOfItems())
}
}
The jetpack compose looks at your object itself to see if it has changed to decide whether to update it or not. You need to update the list itself to do that:
data class A(var v: Int)
#Composable
fun Test1() {
var list by remember { mutableStateOf(listOf(A(1), A(2), A(3))) }
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(list) {
Text(text = it.toString())
}
item {
// it does not work
Button(onClick = {
list[0].v = 2
}) { Text("Change value") }
// it works
Button(onClick = {
list = list.map { it.copy(v = it.v + 1) }
}) { Text("Change list") }
}
}
}
Also, you can use mutableStateListOf, which will monitor the addition and removal of elements.
#Composable
fun Test2() {
val list = remember { mutableStateListOf(A(1), A(2), A(3)) }
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(list) {
Text(text = it.toString())
}
item {
// it not work
Button(onClick = {
list[0].v = 2
}) { Text("Change value") }
// it work
Button(onClick = {
list.add(A(3))
}) { Text("Change list") }
}
}
}
In your case, you can represent the selection like this:
val selectedList = remember { mutableStateListOf<String>() }
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(list) {
val selected = selectedList.contains(it.name)
Text(text = if (selected) "selected" else "not selected")
Button(onClick = {
if (!selected) selectedList.add(it.name)
}) { Text("Select it") }
}
}
Fixed this by taking a copy of list with new values :
private fun updateList(selectedItem: String,
itemsList: List<Product>): List<Product> {
val updatedList = itemsList.map { product ->
if(product.name == selectedItem) {
product.copy(selected = true)
} else {
product.copy(selected = false)
}
}
return updatedList
}
In the following viewModel code I am generating a list of items from graphQl server
private val _balloonsStatus =
MutableStateFlow<Status<List<BalloonsQuery.Edge>?>>(Status.Loading())
val balloonsStatus get() = _balloonsStatus
private val _endCursor = MutableStateFlow<String?>(null)
val endCursor get() = _endCursor
init {
loadBalloons(null)
}
fun loadBalloons(cursor: String?) {
viewModelScope.launch {
val data = repo.getBalloonsFromServer(cursor)
if (data.errors == null) {
_balloonsStatus.value = Status.Success(data.data?.balloons?.edges)
_endCursor.value = data.data?.balloons?.pageInfo?.endCursor
} else {
_balloonsStatus.value = Status.Error(data.errors!![0].message)
_endCursor.value = null
}
}
}
and in the composable function I am getting this data by following this code:
#Composable
fun BalloonsScreen(
navHostController: NavHostController? = null,
viewModel: SharedBalloonViewModel
) {
val endCursor by viewModel.endCursor.collectAsState()
val balloons by viewModel.balloonsStatus.collectAsState()
AssignmentTheme {
Column(modifier = Modifier.fillMaxSize()) {
when (balloons) {
is Status.Error -> {
Log.i("Reyjohn", balloons.message!!)
}
is Status.Loading -> {
Log.i("Reyjohn", "loading..")
}
is Status.Success -> {
BalloonList(edgeList = balloons.data!!, navHostController = navHostController)
}
}
Spacer(modifier = Modifier.weight(1f))
Button(onClick = { viewModel.loadBalloons(endCursor) }) {
Text(text = "Load More")
}
}
}
}
#Composable
fun BalloonList(
edgeList: List<BalloonsQuery.Edge>,
navHostController: NavHostController? = null,
) {
LazyColumn {
items(items = edgeList) { edge ->
UserRow(edge.node, navHostController)
}
}
}
but the problem is every time I click on Load More button it regenerates the view and displays a new set of list, but I want to append the list at the end of the previous list. As far I understand that the list is regenerated as the flow I am listening to is doing the work behind this, but I am stuck here to get a workaround about how to achieve my target here, a kind hearted help would be much appreciated!
You can create a private list in ViewModel that adds List<BalloonsQuery.Edge>?>
and instead of
_balloonsStatus.value = Status.Success(data.data?.balloons?.edges)
you can do something like
_balloonsStatus.value = Status.Success(myLiast.addAll(
data.data?.balloons?.edges))
should update Compose with the latest data appended to existing one
I am adding a ComposeView to my XML layout. The ComposeView is a widget that will stay hidden until a button is clicked (button is in XML).
I have tried a number of things, but the way the app is setup my ComposeView widget does not recompose after the the button click.
Code
DetailsWidget is where the onClickListener is set for each widget
class DetailsWidget : ListingDetailsWidget {
...
override fun buildView(inflater: LayoutInflater) : View? {
...
val expandButton = baseView.findViewById<View>(R.id.btn_expand)
val buttonContainer = baseView.findViewById<View>(R.id.expand_button_container)
val tableContainerComposeview = baseView.findViewById<View>(R.id.table_container_compose_view) <- I added this line to existing code
tableView.post {
if (!tableView.isExpanded) {
buttonContainer.visibility = View.VISIBLE
expandButton.setOnClickListener {
tableView.isExpanded = true
buttonContainer.visibility = View.GONE
DetailsWidgetViewModel().onTableViewExpanded() <- I added this line to existing code
}
}
}
return baseView
}
}
DetailsWidgetViewModel
class DetailsWidgetViewModel : ViewModel() {
private val _showDetailsWidget = MutableStateFlow(false)
val showDetailsWidget: StateFlow<Boolean> = _showDetailsWidget.asStateFlow()
fun onTableViewExpanded() {
_showDetailsWidget.value = true
}
}
JetpackComposeWidget
#Parcelize
#Serializable
data class JetpackComposeWidgetData(
...
) : ListingDetailsWidget() {
override fun buildView(inflater: LayoutInflater): View {
val binding = ViewDetailWidgetDetailsBinding.inflate(inflater, null, false)
val view = binding.root
binding.tableContainerComposeView.setContent {
JetpackComposeWidget(data = this)
}
return view
}
}
#Composable
fun JetpackComposeWidget(data: JetpackComposeWidgetData) {
val showDetailsWidgetState: Boolean by DetailsWidgetViewModel().showDetailsWidget.collectAsState()
if (showDetailsWidgetState) {
LdpWidgetTheme {
Surface(
color = MaterialTheme.colors.surface,
modifier = Modifier.clickable { DetailsWidgetViewModel().onTableViewExpanded() }
) {
Column(
Modifier
.padding(8.dp),
) {
data.title?.let {
Text(
text = it,
style = MaterialTheme.typography.h4,
)
}
data.sections.mapIndexed { index, section -> Section(section, index == 0) }
}
}
}
}
}
Is there a better way to show / hide a ComposeView in XML?
What I've tried:
Add to ComposeView XML android:onClick='onTableViewExpanded'
Add to ComposeView XML android:visibility:"gone" then setting the ComposeView to View.VISIBLE in the setOnClickListener code block
Call an event listener on button click
Recompose on button click using _showDetailsWidget.copy(value = true)
Live Data and Mutable State instead of MutableStateFlow
I am currently trying to write an App for my thesis and currently, I am looking into different approaches. Since I really like Flutter and the Thesis requires me to use Java/Kotlin I would like to use Jetpack compose.
Currently, I am stuck trying to update ListElements.
I want to have a List that shows Experiments and their state/result. Once I hit the Button I want the experiments to run and after they are done update their state. Currently, the run Method does nothing besides setting the state to success.
The problem is I don't know how to trigger a recompose from the viewModel of the ExperimentRow once an experiment updates its state.
ExperimentsActivity:
class ExperimentsActivity : AppCompatActivity() {
val exViewModel by viewModels<ExperimentViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//For now this is just Dummy Data and will be replaced
exViewModel.experiments += listOf(
Experiment("Test1", exViewModel::experimentStateChanged),
Experiment("Strongbox", exViewModel::experimentStateChanged)
)
setContent {
TpmTheme {
// A surface container using the 'background' color from the theme
Surface {
ExperimentScreen(
exViewModel.experiments,
exViewModel::startTests
)
}
}
}
}
}
ExperimentViewModel:
class ExperimentViewModel : ViewModel() {
var experiments by mutableStateOf(listOf<Experiment>())
fun startTests() {
for (exp in experiments) {
exp.run()
}
}
fun experimentStateChanged(experiment: Experiment) {
Log.i("ViewModel", "Changed expState of ${experiment.name} to ${experiment.state}")
// HOW DO I TRIGGER A RECOMPOSE OF THE EXPERIMENTROW FOR THE experiment????
//experiments = experiments.toMutableList().also { it.plus(experiment) }
Log.i("Vi", "Size of Expirments: ${experiments.size}")
}
}
ExperimentScreen:
#Composable
fun ExperimentScreen(
experiments: List<Experiment>,
onStartExperiments: () -> Unit
) {
Column {
LazyColumnFor(
items = experiments,
modifier = Modifier.weight(1f),
contentPadding = PaddingValues(top = 8.dp),
) { ep ->
ExperimentRow(
experiment = ep,
modifier = Modifier.fillParentMaxWidth(),
)
}
Button(
onClick = { onStartExperiments() },
modifier = Modifier.padding(16.dp).fillMaxWidth(),
) {
Text("Run Tests")
}
}
}
#Composable
fun ExperimentRow(experiment: Experiment, modifier: Modifier = Modifier) {
Row(
modifier = modifier
.padding(horizontal = 16.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(experiment.name)
Icon(
asset = experiment.state.vAsset,
)
}
Experiment:
class Experiment(val name: String, val onStateChanged: (Experiment) -> Unit) {
var state: ExperimentState = ExperimentState.DEFAULT
set(value) {
field = value
onStateChanged(this)
}
fun run() {
state = ExperimentState.SUCCESS;
}
}
enum class ExperimentState(val vAsset: VectorAsset) {
DEFAULT(Icons.Default.Info),
RUNNING(Icons.Default.Refresh),
SUCCESS(Icons.Default.Done),
FAILED(Icons.Default.Warning),
}
There's a few ways to address this but key thing is that you need to add a copy of element (with state changed) to experiments to trigger the recomposition.
One possible example would be
data class Experiment(val name: String, val state: ExperimentState, val onStateChanged: (Experiment) -> Unit) {
fun run() {
onStateChanged(this.copy(state = ExperimentState.SUCCESS))
}
}
and then
fun experimentStateChanged(experiment: Experiment) {
val index = experiments.toMutableList().indexOfFirst { it.name == experiment.name }
experiments = experiments.toMutableList().also {
it[index] = experiment
}
}
though I suspect there's probably cleaner way of doing this.
I have a compose button which has a #Model state.
#Model
class CounterState(var count: Int = 0)
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApp {
Counter(CounterState())
}
}
}
}
#Composable
fun Counter(state: CounterState) {
Button(
text = "I've been clicked ${state.count} times",
onClick = {
state.count++
},
style = ContainedButtonStyle(color = if (state.count > 5) Color.Green else Color.White)
)
}
when I click button it's text is not updated.
Does anyone know why?
On this line
Counter(CounterState())
you're creating a new CounterState every time, which resets it to zero every time it's recomposed. You should create it once, outside of the composition, and then store it in a variable to pass into Counter each time.