jetpack compose not updating icon - android-jetpack-compose

the title is updated on changes but the icon ist not.
#Composable
fun topBar(title: String) {
val context = LocalContext.current
var showMenu by remember { mutableStateOf(false) }
val health = remember { Health() }
val healthMin = remember { mutableStateOf(R.drawable.music) }
TopAppBar(title = {
val location = remember { mutableStateOf("") }
SystemBroadcastReceiver(MediaPlaybackService.LOCATION_CHANGE) { intent ->
location.value = intent!!.getStringExtra(MediaPlaybackService.LOCATION_CHANGE)!!
}
Text(text = location.value)
},
navigationIcon = {
SystemBroadcastReceiver(Health.STATUS) { intent ->
health.onReceive(null, intent)
healthMin.value = health.updateLogo()
}
IconButton(onClick = {}) {
Log.i("TEST","Value = ${healthMin.value}")
Icon(
painter = painterResource(healthMin.value),
"contentDescription"
)
}
},
...
I can see the changes in the log. But the UI is not updated
2022-02-24 12:56:24.117 3032-3032/com.example.myapplication I/TEST: Value = 2131231223
2022-02-24 12:56:24.525 3032-3032/com.example.myapplication I/TEST: Value = 2131231226
The SystemBroadcastReceiver is taken from the case-study official documentaion

The code actually works. My problem was, that the visual icon appeared unchanged because the changed icon has only a different color. And the color is overruled by tint. So the change is not visible.
Use this if you want to see the color from the ressource and not the one from the theme.
Icon( painter = painterResource(healthMin.value),
tint = Color.Unspecified,
"contentDescription"
)

Related

How to prevent accessibility focus from moving to controls behind ExposedDropdownMenuBox

I am having an issue where the accessibility focus is going to controls behind the PopUp Window when using a ExposedDropdownMenuBox
If there is a single ExposedDropdownMenuBox everything works as expected, but when I add a second ExposedDropdownMenuBox or another control the focus goes to the second ExposedDropdownMenuBox before going to the PopUp Window.
GIF of single dropdown behavior
https://giphy.com/gifs/gapy0XK1CGmbyltJxU
GIF of two dropdowns on the same screen
https://giphy.com/gifs/WkL5TcMWlumfcGHPmD
Source
#Composable
fun Screen() {
Column (
modifier = Modifier
.wrapContentSize(Alignment.TopCenter)
.padding(top = 48.dp)
) {
Text(
text = stringResource(id = R.string.greeting),
fontSize = 30.sp,
modifier = Modifier.padding(bottom = 24.dp)
)
LocaleDropdownMenu()
Spacer(modifier = Modifier.height(8.dp))
// LocaleDropdownMenu()
}
}
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun LocaleDropdownMenu() {
val localeOptions = mapOf(
R.string.en to "en",
R.string.fr to "fr",
R.string.hi to "hi",
R.string.ja to "ja"
).mapKeys { stringResource(it.key) }
// boilerplate: https://developer.android.com/reference/kotlin/androidx/compose/material/package-summary#ExposedDropdownMenuBox(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1)
var expanded by remember { mutableStateOf(false) }
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = {
expanded = !expanded
}
) {
TextField(
readOnly = true,
value = stringResource(R.string.language),
onValueChange = { },
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(
expanded = expanded
)
}
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = {
expanded = false
}
) {
localeOptions.keys.forEach { selectionLocale ->
DropdownMenuItem(
onClick = {
expanded = false
// set app locale given the user's selected locale
AppCompatDelegate.setApplicationLocales(
LocaleListCompat.forLanguageTags(
localeOptions[selectionLocale]
)
)
},
content = { Text(selectionLocale) }
)
}
}
}
}
A repository that reproduces this issue is here:
https://github.com/dazza5000/ExposedDropdownMenuBox-accessibility-issue

How to add and delete items in a lazyVerticalGrid Column in jetpack compose

I'm new to jetpack compose and I am just looking for a way to add and delete items from a lazyVerticalGrid using jetpack compose from a pop-up menu. Nothing complicated, just simple code to make me easily understand what I have to do
This is pretty easy as other LazyLists. You need to have a list that you can trigger recomposition after delete, add or update and use unique keys to not recompose your entire list and limit recomposition to a range of items.
val list = remember { mutableStateListOf<Snack>() }
You can remove or add inside you menu by changing this list
Adding, removing items or replacing any items with new one, to update you need to pass a new instance of object, will trigger recomposition
#Composable
private fun GridExample() {
val scrollState = rememberLazyListState()
val coroutineScope = rememberCoroutineScope()
val list = remember { mutableStateListOf<Snack>() }
Column {
LazyVerticalGrid(
contentPadding = PaddingValues(12.dp),
modifier = Modifier
.weight(1f)
.background(backgroundColor),
columns = GridCells.Fixed(3),
content = {
items(items = list,
key = { snack: Snack ->
snack.name
}) { snack: Snack ->
GridSnackCard(snack = snack)
}
}
)
Row(
modifier = Modifier.padding( 8.dp),
horizontalArrangement = Arrangement.SpaceEvenly
) {
Button(
modifier = Modifier.weight(1f),
onClick = {
if (list.size < snacks.size) {
list.add(snacks[list.size])
}
},
shape = RoundedCornerShape(8.dp)
) {
Text(text = "Add")
}
Spacer(modifier = Modifier.width(10.dp))
Button(
modifier = Modifier.weight(1f),
onClick = {
if (list.size > 0) {
list.removeLast()
}
},
shape = RoundedCornerShape(8.dp)
) {
Text(text = "Remove")
}
}
}
}

I have a composable not setting button text as expected; wondering why. Have a reproducible example

this started as a new compose project
with the following code the intent is to change the text to the picked time. The code is commented where the behavior occurs
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApplicationTestTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
TimeCardButton(id = 1, symbol ="In", enabled=true,modifier = Modifier) { entry ->
Log.d("click", "$entry result")
}
}
}
}
}
}
data class TimeCardEntry(val id: Int = -1, var entry: String = "")
#Composable
fun TimeCardButton(
id: Int,
symbol: String,
enabled: Boolean = false,
modifier: Modifier,
onValueChange: (TimeCardEntry) -> Unit = {},
) {
// Value for storing time as a string
val timeState = remember {
mutableStateOf(TimeCardEntry(id, symbol))
}
val validState = remember {
timeState.value.entry.trim().isNotEmpty()
}
val mTime = remember { mutableStateOf(symbol) }
if (enabled) {
// Fetching local context
val mContext = LocalContext.current
// Declaring and initializing a calendar
val mCalendar = Calendar.getInstance()
val mHour = mCalendar[Calendar.HOUR_OF_DAY]
val mMinute = mCalendar[Calendar.MINUTE]
// Creating a TimePicker dialog
val mTimePickerDialog = TimePickerDialog(
mContext,
{ _, mHour: Int, mMinute: Int ->
timeState.value.entry = "$mHour:$mMinute"
mTime.value = "$mHour:$mMinute"
onValueChange(timeState.value)
}, mHour, mMinute, false
)
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.clip(CircleShape)
.then(modifier)
) {
TextButton(onClick = { mTimePickerDialog.show() }.also {
Log.d("click", "id $id clicked!") }) {
Column() {
// if I use just this it works [in changes to the time picked]
//Text(text = mTime.value)
// if i use both of these BOTH are set when the date picker is invoked
// if I just use the second one alone, the text never changes
Text(text = timeState.value.entry)
}
}
}
} else {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.clip(CircleShape)
.then(modifier)
) {
Text(text = symbol, color =
MaterialTheme.colors.onBackground)
}
}
}
#Preview(showBackground = true)
#Composable
fun DefaultPreview() {
MyApplicationTestTheme {
}
}
First of all how to fix it:
Your problem basically is this. The easiest way to fix it would be to reassign the whole value of TimeState, not just entry by calling
timeState.value = timeState.value.copy(entry = "$mHour:$mMinute")
The reason it doesn't work with only the second one is that the change of a property doesn't trigger recomposition, even if the variable containing it is a mutableState. To fix (as outlined in the answers to the question linked above) this you either have to reassign the whole variable or make the parameter you want to observe observable (for example changing the String to State<String>)
PS: if you use by with mutableStateOf (i.e. val timeState = remember { mutableStateOf(TimeCardEntry(id, symbol)) }) you don't have to use .value every time. I find that a lot cleaner and more readable

compose can not test Dialog

I have dialog in compose:
#Composable
fun testgtt() {
val saveDialogState = remember { mutableStateOf(false) }
Button(onClick = { saveDialogState.value = true }, modifier = Modifier.testTag(PLACE_TAG)) {
Text(text = "helllow")
}
Dialog(onDismissRequest = { saveDialogState.value = false }) {
Text(text = "helllow",modifier = Modifier.testTag(BUTTON_TAG))
}
}
and want to test it:
#Test
fun das(){
composeTestRule.setContent {
TerTheme {
testgtt()
}
}
composeTestRule.onRoot(useUnmergedTree = true).printToLog("currentLabelExists")
composeTestRule.onNodeWithTag(PLACE_TAG).performClick()
composeTestRule.onNodeWithTag(BUTTON_TAG).assertIsDisplayed()
}
but I get this error:
java.lang.AssertionError: Failed: assertExists.
Reason: Expected exactly '1' node but found '2' nodes that satisfy: (isRoot)
Nodes found:
1) Node #1 at (l=0.0, t=0.0, r=206.0, b=126.0)px
Has 1 child
2) Node #78 at (l=0.0, t=0.0, r=116.0, b=49.0)px
Has 1 child
Inspite of the fact that I see the Dialog itself.
The reason for this error is the line: composeTestRule.onRoot(useUnmergedTree = true).printToLog("currentLabelExists")
onRoot expects a single node, but i suspect both the containing view and the dialog each return their own root (Speculation)
A possible workaround is to instead print both root trees using something like
composeTestRule.onAllNodes(isRoot()).printToLog("currentLabelExists")
use navigation component:
#Composable
fun de(){
val navController = rememberNavController()
Scaffold { innerPadding ->
NavHost(navController, "home", Modifier.padding(innerPadding)) {
composable("home") {
// This content fills the area provided to the NavHost
val saveDialogState = remember { mutableStateOf(true) }
Button(onClick = {
navController.navigate("detail_dialog")
}, modifier = Modifier.testTag(PLACE_TAG)) {
Text(text = "helllow")
}
}
dialog("detail_dialog") {
// This content will be automatically added to a Dialog() composable
// and appear above the HomeScreen or other composable destinations
Dialog(onDismissRequest = {navController.navigate("home")}) {
Card(
shape = RoundedCornerShape(10.dp),
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
// .padding(horizontal = 16.dp)
.padding(vertical = 8.dp),
elevation = 8.dp
){
Text(text = "helllow", modifier = Modifier.testTag(BUTTON_TAG))
}
}
}
}
}
}
As #DanielO said you can use the isRoot() selector, see below. That however prints out the same message as before.
A possible workaround is to instead print both root trees using something like
composeTestRule.onAllNodes(isRoot()).printToLog("currentLabelExists")
You have to distinctivly select which root you are looking for. By using the selectors:
.get( index )
.onFirst()
.onLast()
When added it should look like this:
composeTestRule.onAllNodes(isRoot()).get(1).printToLog("T:")
composeTestRule.onAllNodes(isRoot()).onFirst().printToLog("T:")
composeTestRule.onAllNodes(isRoot()).onLast().printToLog("T:")

Android Jetpack Compose Row's height (IntrinsicSize.Min) is not stretched when children column generate more composables

The following is my code snippet. I pass in editClick that adds a data class object into chargingViewModel.contractSelfPay, which is observed as itemList state. When I click the icon, I can tell itemList state receives update by having more edit icons that are spaced evenly. However, BasicGrid Row's height is not stretched with Intrinsic.Min.
If I remove IntrinsicSize.Min, even though row's height is stretched, dividers no longer can fillMaxHeight as well as icon columns. without Intrinsic.Min
#Composable
fun ContractSelfPay(chargingViewModel: ChargingViewModel, editClick: () -> Unit = {}) {
val itemList by chargingViewModel.contractSelfPay.observeAsState()
val composeList: List<#Composable () -> Unit> = itemList?.map {
#Composable {
Row {
TempFunc { StyledText(text = it.itemTitle) }
TempFunc { StyledText(text = it.originalPrice.toString()) }
TempFunc { StyledText(text = it.selfPay.toString(), color = self_pay_blue) }
TempFunc { StyledText(text = it.count.toString()) }
TempFunc { StyledText(text = (it.selfPay * it.count).toString()) }
}
}
} ?: listOf()
val total = itemList?.map { (it.selfPay.toInt() * it.count.toInt()) }?.sum() ?: 0
BasicGrid("全自費", composeList, total = total.toString(), editClick = editClick)
}
#Composable
fun BasicGrid(
gridTitle: String,
itemList: List<#Composable () -> Unit>,
total: String = "0",
editClick: () -> Unit = {}
) {
Row(modifier = Modifier.height(IntrinsicSize.Min), verticalAlignment = Alignment.CenterVertically) {
StyledTextBold(text = gridTitle, modifier = Modifier.weight(15f).wrapContentWidth())
VerticalDivider()
Column(
modifier = Modifier.weight(60f)
) {
itemList.forEachIndexed { index, compose ->
compose()
if (index != itemList.size - 1)
HorizontalDivider()
}
if (itemList.isEmpty())
StyledText(text = "尚未有任何紀錄", modifier = Modifier.weight(1f).wrapContentSize())
}
VerticalDivider()
StyledTextBold(text = total, modifier = Modifier.weight(15f).wrapContentWidth())
VerticalDivider()
Column(
modifier = Modifier
.weight(10f)
.fillMaxHeight(),
verticalArrangement = Arrangement.SpaceEvenly
) {
itemList.forEachIndexed { index, detail ->
Image(
painter = painterResource(R.drawable.icon_mode_edit),
contentDescription = "",
modifier = Modifier
.align(Alignment.CenterHorizontally)
.clickable { editClick() },
)
if (itemList.isNotEmpty() && index != itemList.size - 1)
HorizontalDivider()
}
}
}
}
I have created issue here https://issuetracker.google.com/issues/217910352. Hopefully it gets solved.
One of the work-arounds I could think of is keeping track of height and removing IntrinsicSize.Min.
As in:
// _key_ is something that causes change of the height of the row
var height by remember(_key_) { mutableStateOf(0) }
Row(Modifier.onSizeChanged { height = it.height }) {
VerticalDivider(Modifier.height(height))
}
In your case I suppose key would be size of itemList.
Thank you Majkeee. It's been a while. The way I fixed it at the time was with custom layout modifier. Not sure if it still works today though.
fun Modifier.expandHeight() = this.then(layout { measurable, constraints ->
val placeable =
measurable.measure(constraints.copy(maxHeight = Constraints.Infinity))
layout(placeable.width, placeable.height) {
placeable.placeRelative(0, 0)
}
})
and to use it you can do
Column(modifier = Modifier.expandHeight())

Resources