I've been experimenting a bit with the new Jetpack Compose for the last few days and it's been great but now I'm stuck on something that should be quite simple. All I want to do is from a LazyColumn (RecyclerView) show an AlertDialog when the user clicks (or long presses) an item in the list AND pass the clicked item as an argument to the AlertDialog. I've managed to do it without passing any arguments and just showing an AlertDialog with preset info. It also works fine to show a Toast message with the clicked item. Here is my code (it is basically the same as the Rally app from the compose-samples on GitHub):
#ExperimentalFoundationApi
#Composable
fun AccountsBody(navController: NavController, viewModel: AccountsViewModel) {
val accountsFromVM = viewModel.accounts.observeAsState()
accountsFromVM.value?.let { accounts ->
StatementBody(
items = accounts, // this is important for the question
// NOT IMPORTANT
amounts = { account -> account.balance },
colors = { account -> HexToColor.getColor(account.colorHEX) },
amountsTotal = accounts.map { account -> account.balance }.sum(),
circleLabel = stringResource(R.string.total),
buttonLabel = DialogScreen.NewAccount.route,
navController = navController,
onLongPress = { } // show alert
) { account, _ -> // this is important for the question
AccountRow(
name = account.name,
bank = account.bank,
amount = account.balance,
color = HexToColor.getColor(account.colorHEX),
account = account,
onClick = { },
onlongPress = { clickedItem ->
// Show alert dialog here and pass in clickedItem
}
)
}
}
}
#Composable
fun <T> StatementBody(
items: List<T>,
// NOT IMPORTANT
colors: (T) -> Color,
amounts: (T) -> Float,
amountsTotal: Float,
circleLabel: String,
buttonLabel: String,
navController: NavController,
onLongPress: (T) -> Unit,
rows: #Composable (T, (T) -> Unit) -> Unit
) {
Column {
// Animating circle and balance box
// NOT IMPORTANT - (see last few rows for the important part)
Box(Modifier.padding(16.dp)) {
val accountsProportion = items.extractProportions { amounts(it).absoluteValue }
val circleColors = items.map { colors(it) }
AnimatedCircle(
accountsProportion,
circleColors,
Modifier
.height(300.dp)
.align(Alignment.Center)
.fillMaxWidth()
)
Column(modifier = Modifier.align(Alignment.Center)) {
Text(
text = circleLabel,
style = MaterialTheme.typography.body1,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Text(
text = formatAmount(amountsTotal),
style = MaterialTheme.typography.h2,
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Button(onClick = { navController.navigate("${buttonLabel}/Nytt sparkonto") }) {
Text(text = buttonLabel)
}
}
}
Spacer(Modifier.height(10.dp))
// Recycler view
// THIS IS THE IMPORTANT PART
Card {
LazyColumn(modifier = Modifier.padding(12.dp)) {
itemsIndexed(items) { idx, item ->
rows(item, onLongPress) // rows is the Composable you pass in
}
}
}
}
}
#ExperimentalFoundationApi
#Composable
fun AccountRow(
name: String,
bank: String,
amount: Float,
color: Color,
account: AccountData,
onClick: () -> Unit,
onlongPress: (AccountData) -> Unit
) {
BaseRow(
color = color,
title = name,
subtitle = bank,
amount = amount,
rowType = account,
onClick = onClick,
onLongPress = onlongPress
)
}
#ExperimentalFoundationApi
#Composable
private fun <T> BaseRow(
color: Color,
title: String,
subtitle: String,
amount: Float,
rowType: T,
onClick: () -> Unit,
onLongPress: (T) -> Unit
) {
val formattedAmount = formatAmount(amount)
Row(
modifier = Modifier
.height(68.dp)
.combinedClickable(
onClick = onClick,
onLongClick = { onLongPress(rowType) } //HERE IS THE IMPORTANT PART
),
verticalAlignment = Alignment.CenterVertically
) {
val typography = MaterialTheme.typography
AccountIndicator(color = color, modifier = Modifier)
Spacer(Modifier.width(12.dp))
Column(Modifier) {
Text(text = title, style = typography.body1)
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Text(text = subtitle, style = typography.subtitle1)
}
}
Spacer(Modifier.weight(1f))
Row(horizontalArrangement = Arrangement.SpaceBetween) {
Text(
text = "$formattedAmount kr",
style = typography.h6,
modifier = Modifier.align(Alignment.CenterVertically)
)
}
Spacer(Modifier.width(16.dp))
}
RallyDivider()
}
Any help on how to accomplish this would be appreciated. I'm kind of new to android development and programming in general so I have probably made this more complex than it has to be heh.
This can be easily done by saving the selected item as a MutableState and whether to show the dialog or not is another MutableState.
Here is simplified example that could work as a starting point:
val items = emptyList<String>()
val currentSelectedItem = remember { mutableStateOf(items[0]) }
val showDialog = remember { mutableStateOf(false) }
if (showDialog.value) ShowDialog(currentSelectedItem.value)
Card {
LazyColumn(modifier = Modifier.padding(12.dp)) {
itemsIndexed(items) { idx, item ->
Row() {
Text(
text = item,
Modifier.clickable {
currentSelectedItem.value = item
showDialog.value=true
}
)
} // rows is the Composable you pass in
}
}
}
Related
I want to make a background color change when user select a item on Surface. I am using Column in my parent, so I can't make LazyColumn. So I am using Foreach to make a list of view. By default no view will be selected, when user click on item then I want change the color. Note: Only one item will select at a time.
ScanDeviceList
#Composable
fun ColumnScope.ScanDeviceList(
scanDeviceList: List<ScanResult>,
modifier: Modifier = Modifier,
pairSelectedDevice: () -> Unit
) {
Spacer()
AnimatedVisibility() {
Column {
Text()
Spacer()
scanDeviceList.forEachIndexed { index, scanResult ->
ClickableItemContainer(
rippleColor = AquaLightOpacity10,
content = {
ScanDeviceItem(index, scanResult, scanDeviceList)
}
){}
}
AvailableWarningText()
PairSelectedDevice(pairSelectedDevice)
}
}
}
ScanDeviceItem
#Composable
fun ScanDeviceItem(
index: Int,
scanResult: ScanResult,
scanDeviceList: List<ScanResult>
) {
Column {
if (index == 0) {
Divider(color = Cloudy, thickness = 1.dp)
}
Text(
text = scanResult.device.name,
modifier = Modifier.padding(vertical = 10.dp)
)
if (index <= scanDeviceList.lastIndex) {
Divider(color = Cloudy, thickness = 1.dp)
}
}
}
ClickableItemContainer
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun ClickableItemContainer(
rippleColor: Color = TealLight,
content: #Composable (MutableInteractionSource) -> Unit,
clickAction: () -> Unit
) {
val interactionSource = remember { MutableInteractionSource() }
CompositionLocalProvider(
LocalRippleTheme provides AbcRippleTheme(rippleColor),
content = {
Surface(
onClick = { clickAction() },
interactionSource = interactionSource,
color = White
) {
content(interactionSource)
}
}
)
}
I want to something like this
Now above solution is only working on ripple effect, Now I want to extend my function to select a single item at a time. Many Thanks
You can store the selected index and use the clickAction to update its value when the user clicks on each item.
Something like:
#Composable
fun MyList(
var selectedIndex by remember { mutableStateOf(-1) }
Column() {
itemsList.forEachIndexed() { index, item ->
MyItem(
selected = selectedIndex == index,
clickAction = { selectedIndex = index }
)
}
}
#Composable
fun MyItem(
selected : Boolean = false,
clickAction: () -> Unit
){
Surface(
onClick = { clickAction() },
color = if (selected) Color.Red else Color.Yellow
) {
Text("Item...")
}
}
To have only item selected at a time, you can follow a behavior that is similar. You save your item selected in a mutableState variable, like:
var itemSelected by remember { mutableState(ScanDeviceResult()) }
After that, when you click the item, you update the itemSelected, something like:
ClickableItemContainer(
rippleColor = AquaLightOpacity10,
content = {
ScanDeviceItem(index, scanResult, scanDeviceList)
}
){ newItemSelected ->
itemSelected = newItemSelected
}
Now, each ClickableItemContainer needs to have an associated ScanResult.
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun ClickableItemContainer(
itemSelected: ScanResult,
scanResult: ScanResult,
rippleColor: Color = TealLight,
content: #Composable (MutableInteractionSource) -> Unit,
clickAction: (ScanResult) -> Unit
) {
val interactionSource = remember { MutableInteractionSource() }
CompositionLocalProvider(
LocalRippleTheme provides AbcRippleTheme(rippleColor),
content = {
Surface(
onClick = { clickAction.invoke(scanResult) },
interactionSource = interactionSource,
color = if (itemSelected == scanResult) YOUR_BACKGROUND-SELECTED_COLOR else White
) {
content(interactionSource)
}
}
)
}
Now, on your ScanDeviceList method:
#Composable
fun ColumnScope.ScanDeviceList(
itemSelected: MutableState<ScanResult>,
scanDeviceList: List<ScanResult>,
modifier: Modifier = Modifier,
pairSelectedDevice: () -> Unit
) {
Spacer()
AnimatedVisibility() {
Column {
Text()
Spacer()
scanDeviceList.forEachIndexed { index, scanResult ->
ClickableItemContainer(
itemSelected = itemSelected,
scanResult = scanResult,
rippleColor = AquaLightOpacity10,
content = {
ScanDeviceItem(index, scanResult, scanDeviceList)
}
){ scanResultToSelect ->
itemSelected = scanResultToSelect
}
}
AvailableWarningText()
PairSelectedDevice(pairSelectedDevice)
}
}
}
How can a selected option from a single choice menu be passed to a different composable to that it is displayed in a Text object? Would I need to modify the selectedOption value in some way?
#Composable
fun ScreenSettings(navController: NavController) {
Scaffold(
topBar = {...},
content = {
LazyColumn(...) {
item {
ComposableSettingTheme()
}
}
},
containerColor = ...
)
}
#Composable
fun ComposableSettingTheme() {
val singleDialog = remember { mutableStateOf(false)}
Column(modifier = Modifier
.fillMaxWidth()
.clickable(onClick = {
singleDialog.value = true
})) {
Text(text = "Theme")
Text(text = selectedOption) // selected theme name should be appearing here
if (singleDialog.value) {
AlertSingleChoiceView(state = singleDialog)
}
}
}
#Composable
fun CommonDialog(
title: String?,
state: MutableState<Boolean>,
content: #Composable (() -> Unit)? = null
) {
AlertDialog(
onDismissRequest = {
state.value = false
},
title = title?.let {
{
Column( Modifier.fillMaxWidth() ) {
Text(text = title)
}
}
},
text = content,
confirmButton = {
TextButton(onClick = { state.value = false }) { Text("OK") }
},
dismissButton = {
TextButton(onClick = { state.value = false }) { Text("Cancel") }
}
)
}
#Composable
fun AlertSingleChoiceView(state: MutableState<Boolean>) {
CommonDialog(title = "Theme", state = state) { SingleChoiceView(state = state) }
}
#Composable
fun SingleChoiceView(state: MutableState<Boolean>) {
val radioOptions = listOf("Day", "Night", "System default")
val (selectedOption, onOptionSelected) = remember { mutableStateOf(radioOptions[2]) }
Column(
Modifier.fillMaxWidth()
) {
radioOptions.forEach { themeOption ->
Row(
Modifier
.clickable(onClick = { })
.selectable(
selected = (text == selectedOption),
onClick = {onOptionSelected(text)}
)
) {
RadioButton(
selected = (text == selectedOption),
onClick = { onOptionSelected(text) }
)
Text(text = themeOption)
}
}
}
}
Update
According to official documentation, you should use state hoisting pattern.
Thus:
Just take out selectedOption local variable to the "highest point of it's usage" (you use it in SingleChoiceView and ComposableSettingTheme methods) - ScreenSettings method.
Then, add selectedOption: String and onSelectedOptionChange: (String) -> Unit parameters to SingleChoiceView and ComposableSettingTheme (You can get more info in documentation).
Refactor your code using this new parameters:
Pass selectedOption local variable from ScreenSettings
into SingleChoiceView and ComposableSettingTheme.
Write logic of onSelectedOptionChange - change local variable to new passed value
Hope I helped you!
I have a Slider which is placed inside a Column which is scrollable. When i scroll through the components sometimes accidentally slider value changes because of accidental touches. How can i avoid this?
Should i be disable taps on slider? If yes how can i do it?
Is there any alternate like Nested scroll instead of Column which can prevent this from happening?
#Composable
fun ColumnScope.FilterRange(
title: String,
range: ClosedFloatingPointRange<Float>,
rangeText: String,
valueRange: ClosedFloatingPointRange<Float>,
onValueChange: (ClosedFloatingPointRange<Float>) -> Unit,
) {
Spacer(modifier = Modifier.height(Size_Regular))
Text(
text = title,
style = MaterialTheme.typography.h6
)
Spacer(modifier = Modifier.height(Size_X_Small))
Text(
text = rangeText,
style = MaterialTheme.typography.subtitle1
)
RangeSlider(
modifier = Modifier.fillMaxWidth(),
values = range,
valueRange = valueRange,
onValueChange = {
onValueChange(it)
})
Spacer(modifier = Modifier.height(Size_Small))
Divider(thickness = DividerSize)
}
I would disable the RangeSlider and only enable it when you tap on it. You disable it by tapping anywhere else within the Column. This is a similar behavior used to mimic losing focus. Here's an example:
class MainActivity : ComponentActivity() {
#ExperimentalMaterialApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startActivity(intent)
setContent {
var rangeEndabled by remember { mutableStateOf(false)}.apply { this.value }
var sliderPosition by remember { mutableStateOf(0f..100f) }
Text(text = sliderPosition.toString())
Column(modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.pointerInput(Unit) {
detectTapGestures(
onTap = {
rangeEndabled = false
}
)
}) {
repeat(30) {
Text(it.toString())
}
RangeSlider(
enabled = rangeEndabled,
values = sliderPosition,
onValueChange = { sliderPosition = it },
valueRange = 0f..100f,
onValueChangeFinished = {
// launch some business logic update with the state you hold
// viewModel.updateSelectedSliderValue(sliderPosition)
},
modifier = Modifier.pointerInput(Unit) {
detectTapGestures(
onTap = {
rangeEndabled = true
}
)
}
)
repeat(30) {
Text(it.toString())
}
}
}
}
}
I've gone through this codelab. In step number 7, when clicking on single row's text it's changing its color, but function will not keep track of it, meaning it will disappear after re-composition.
I want list to remember color of single item thus I've move state hoisting to the NameList function level.
Unfortunately it's not working.
Where's the bug?
#Composable
fun NameList(names: List<String>, modifier: Modifier = Modifier) {
LazyColumn(modifier = modifier) {
items(items = names) { name, ->
val isSelected = remember { mutableStateOf(false)}
Greeting(name = name,isSelected.value){ newSelected -> isSelected.value = newSelected}
Divider(color = Color.Black)
}
}
}
#Composable
fun Greeting(name: String,isSelected : Boolean, updateSelected : (Boolean) -> Unit) {
val backgroundColor by animateColorAsState(if (isSelected) Color.Red else Color.Transparent)
Text(
modifier = Modifier
.padding(24.dp)
.background(color = backgroundColor)
.clickable(onClick = { updateSelected(!isSelected)}),
text = "Hello $name",
)
}
You should hoist your selection state to the caller of NameList function.
#Composable
fun MyScreen() {
// Fake list of names
val namesList = (1..100).map { "Item $it" }
// Here, we're keeping the selected positions.
// At the beginning, all names are not selected.
val selection = remember {
mutableStateListOf(*namesList.map { false }.toTypedArray())
}
NameList(
// list of names
names = namesList,
// list of selected items
selectedItems = selection,
// this function will update the list above
onSelected = { index, selected -> selection[index] = selected },
// just to occupy the whole screen
modifier = Modifier.fillMaxSize()
)
}
Then, your NameList will look like this:
#Composable
fun NameList(
names: List<String>,
selectedItems: List<Boolean>,
onSelected: (index: Int, selected: Boolean) -> Unit,
modifier: Modifier = Modifier
) {
LazyColumn(modifier = modifier) {
itemsIndexed(items = names) { index, name ->
Greeting(
name = name,
isSelected = selectedItems[index],
updateSelected = { onSelected(index, it) }
)
Divider(color = Color.Black)
}
}
}
Nothing changes on Greeting function.
Here is the result:
I have this composable function that a button will toggle show the text and hide it
#Composable
fun Greeting() {
Column {
val toggleState = remember {
mutableStateOf(false)
}
AnimatedVisibility(visible = toggleState.value) {
Text(text = "Edit", fontSize = 64.sp)
}
ToggleButton(toggleState = toggleState) {}
}
}
#Composable
fun ToggleButton(modifier: Modifier = Modifier,
toggleState: MutableState<Boolean>,
onToggle: (Boolean) -> Unit) {
TextButton(
modifier = modifier,
onClick = {
toggleState.value = !toggleState.value
onToggle(toggleState.value)
})
{ Text(text = if (toggleState.value) "Stop" else "Start") }
}
One thing I didn't like the code is val toggleState = remember { ... }.
I prefer val toggleState by remember {...}
However, if I do that, as shown below, I cannot pass the toggleState over to ToggleButton, as ToggleButton wanted mutableState<Boolean> and not Boolean. Hence it will error out.
#Composable
fun Greeting() {
Column {
val toggleState by remember {
mutableStateOf(false)
}
AnimatedVisibility(visible = toggleState) {
Text(text = "Edit", fontSize = 64.sp)
}
ToggleButton(toggleState = toggleState) {} // Here will have error
}
}
#Composable
fun ToggleButton(modifier: Modifier = Modifier,
toggleState: MutableState<Boolean>,
onToggle: (Boolean) -> Unit) {
TextButton(
modifier = modifier,
onClick = {
toggleState.value = !toggleState.value
onToggle(toggleState.value)
})
{ Text(text = if (toggleState.value) "Stop" else "Start") }
}
How can I fix the above error while still using val toggleState by remember {...}?
State hoisting in Compose is a pattern of moving state to a composable's caller to make a composable stateless. The general pattern for state hoisting in Jetpack Compose is to replace the state variable with two parameters:
value: T: the current value to display
onValueChange: (T) -> Unit: an event that requests the value to change, where T is the proposed new value
You can do something like
// stateless composable is responsible
#Composable
fun ToggleButton(modifier: Modifier = Modifier,
toggle: Boolean,
onToggleChange: () -> Unit) {
TextButton(
onClick = onToggleChange,
modifier = modifier
)
{ Text(text = if (toggle) "Stop" else "Start") }
}
and
#Composable
fun Greeting() {
var toggleState by remember { mutableStateOf(false) }
AnimatedVisibility(visible = toggleState) {
Text(text = "Edit", fontSize = 64.sp)
}
ToggleButton(toggle = toggleState,
onToggleChange = { toggleState = !toggleState }
)
}
You can also add the same stateful composable which is only responsible for holding internal state:
#Composable
fun ToggleButton(modifier: Modifier = Modifier) {
var toggleState by remember { mutableStateOf(false) }
ToggleButton(modifier,
toggleState,
onToggleChange = {
toggleState = !toggleState
},
)
}