As seen in code below I have a callback where I want to collect the VerticalScroller's position, but if I do so, the VerticalScrollers widget will not scroll anymore, because in the source code to VerticalScroller, the initial callback has a call to VerticalScroller 's scrollerPosition. This value is not reachable from the callback where I want to use it. Is there a quick way to call the scrollerPosition or make the scrollbehavior continue ? (without creating a recursive "race" condition ?)
#Composable
fun StationsScreen(deviceLocation: LocationData, openDrawer: () -> Unit)
{
var scrollPosition = ScrollPosition(0.px,0.px)
FlexColumn {
inflexible {
TopAppBar(
title = {Text(text = "Stations")},
navigationIcon = {
VectorImageButton(id = R.drawable.ic_baseline_menu_24) {
openDrawer()
}
}
)
}
inflexible {
Column (
mainAxisSize = LayoutSize.Expand,
crossAxisSize = LayoutSize.Expand
){
LocationWidget(deviceLocation)
}
}
inflexible {
Column(
mainAxisSize = LayoutSize.Expand,
crossAxisSize = LayoutSize.Expand
){
PushWidget(){
deviceLocation.lat++
deviceLocation.lng++
}
}
}
inflexible{
Column(
mainAxisSize = LayoutSize.Expand,
crossAxisSize = LayoutSize.Expand
) {
ScrollPosWidget(scrollPosition = scrollPosition)
}
}
flexible(flex = 1f)
{
VerticalScroller (onScrollPositionChanged = { px: Px, px1: Px ->
scrollPosition.posX = px
scrollPosition.maxX = px1
}){
Column {
for(i in 0..20) {
HeightSpacer(16.dp)
imageBank.forEach { imageItem: ImageItem ->
Text(text = imageItem.title ?: "<Empty>")
Divider()
}
}
}
}
}
}
}
--- definition of the VerticalScroller Widget in compose source code:
#Composable
fun VerticalScroller(
scrollerPosition: ScrollerPosition = +memo { ScrollerPosition() },
onScrollPositionChanged: (position: Px, maxPosition: Px) -> Unit = { position, _ ->
scrollerPosition.value = position
},
isScrollable: Boolean = true,
#Children child: #Composable() () -> Unit
) {
Scroller(scrollerPosition, onScrollPositionChanged, true, isScrollable, child)
}
Actually I found a solution for my problem... (I was sometime taught to think out of the box...) So...
In my #Compose function I actually need to create my own scrollerPosition instance and use that in my call to VerticalScroller to change the poisition. I was afraid that this could cause a recursive "race" call situation, but that dependends on when the onScrolledPostionChanged is called in the Scroller's logic. If onScrolledPositionChanged is called when scrollerPosition.position changes value, you may get the recursive "race" condition. But apparently not.
So my changes are basicly like this:
#Composable
fun StationsScreen(deviceLocation: LocationData, openDrawer: () -> Unit)
{
var scrollPosition = ScrollPosition(0.px,0.px)
var myScrollerPosition = +memo{ ScrollerPosition ()} //<--- make my own scrollerPosition
FlexColumn {
inflexible {
TopAppBar(
title = {Text(text = "Stations")},
navigationIcon = {
VectorImageButton(id = R.drawable.ic_baseline_menu_24) {
openDrawer()
}
}
)
}
inflexible {
Column (
mainAxisSize = LayoutSize.Expand,
crossAxisSize = LayoutSize.Expand
){
LocationWidget(deviceLocation)
}
}
inflexible {
Column(
mainAxisSize = LayoutSize.Expand,
crossAxisSize = LayoutSize.Expand
){
PushWidget(){
deviceLocation.lat++
deviceLocation.lng++
}
}
}
inflexible{
Column(
mainAxisSize = LayoutSize.Expand,
crossAxisSize = LayoutSize.Expand
) {
ScrollPosWidget(scrollPosition = scrollPosition)
}
}
flexible(flex = 1f)
{
VerticalScroller (
scrollerPosition = myScrollerPosition,
onScrollPositionChanged = { px: Px, px1: Px ->
scrollPosition.posX = px
scrollPosition.maxX = px1
myScrollerPosition.value = px //<-- remember to set the new value here
// You can now use the difference of maxX and posX to load
// new items into the list...
}){
Column {
for(i in 0..20) {
HeightSpacer(16.dp)
imageBank.forEach { imageItem: ImageItem ->
Text(text = imageItem.title ?: "<Empty>")
Divider()
}
}
}
}
}
}
}
#Composable
fun LocationWidget(locationData: LocationData){
Surface {
Padding(padding = 8.dp) {
Text(text = "${locationData.lat}, ${locationData.lng}")
}
}
}
#Composable
fun PushWidget(action: () -> Unit){
Surface {
Padding(padding = 8.dp) {
Button(text = "Click Me!", onClick = action)
}
}
}
#Composable
fun ScrollPosWidget(scrollPosition: ScrollPosition){
Surface {
Padding(padding = 8.dp) {
Text(text = "posX=${scrollPosition.posX}, maxX=${scrollPosition.maxX}")
}
}
}
RG
Related
In order to practice Jetpack Compose I wanted to create a MultiComboBox component for later use. It's basically standard ComboBox that allows to pick multiple options. Something like below:
I did prepare a piece of code that IMO should work fine and generally it does, but there's one case when it doesn't and I cannot figure it out what's wrong.
Here's my code:
data class ComboOption(
override val text: String,
val id: Int,
) : SelectableOption
interface SelectableOption {
val text: String
}
#Composable
fun MultiComboBox(
labelText: String,
options: List<ComboOption>,
onOptionsChosen: (Set<ComboOption>) -> Unit,
modifier: Modifier = Modifier,
selectedIds: Set<Int> = emptySet(),
) {
var expanded by remember { mutableStateOf(false) }
// when no options available, I want ComboBox to be disabled
val isEnabled by rememberUpdatedState { options.isNotEmpty() }
var currentlySelected by remember(options, selectedIds) {
mutableStateOf(options.filter { it.id in selectedIds }.toSet())
}
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = {
if (isEnabled()) {
expanded = !expanded
if (!expanded) {
onOptionsChosen(currentlySelected)
}
}
},
modifier = modifier,
) {
val selectedSummary = when (selectedIds.size) {
0 -> ""
1 -> options.first { it.id == selectedIds.first() }.text
else -> "Wybrano ${selectedIds.size}"
}
TextField(
enabled = isEnabled(),
modifier = Modifier.menuAnchor(),
readOnly = true,
value = selectedSummary,
onValueChange = {},
label = { Text(text = labelText) },
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded)
},
colors = ExposedDropdownMenuDefaults.textFieldColors(),
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = {
expanded = false
onOptionsChosen(currentlySelected)
},
) {
for (option in options) {
DropdownMenuItem(
text = {
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(
checked = option in currentlySelected,
onCheckedChange = { newCheckedState ->
if (newCheckedState) {
currentlySelected += option
} else {
currentlySelected -= option
}
},
)
Text(text = option.text)
}
},
onClick = {
val isChecked = option in currentlySelected
if (isChecked) {
currentlySelected -= option
} else {
currentlySelected += option
}
},
contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
)
}
}
}
}
When I pick options and then dismiss the combo by clicking somewhere outside of it - it works fine. The problem is with onExpandedChange. currentlySelected inside of that lambda is always the same as first value of selectedIds. So for example, when no options are preselected it always calls onOptionsChosen with empty set, hence regardless of what I select - it always sets empty value. Any ideas why it happens an how can it be fixed?
You can use:
#Composable
fun MultiComboBox(
labelText: String,
options: List<ComboOption>,
onOptionsChosen: (List<ComboOption>) -> Unit,
modifier: Modifier = Modifier,
selectedIds: List<Int> = emptyList(),
) {
var expanded by remember { mutableStateOf(false) }
// when no options available, I want ComboBox to be disabled
val isEnabled by rememberUpdatedState { options.isNotEmpty() }
var selectedOptionsList = remember { mutableStateListOf<Int>()}
//Initial setup of selected ids
selectedIds.forEach{
selectedOptionsList.add(it)
}
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = {
if (isEnabled()) {
expanded = !expanded
if (!expanded) {
onOptionsChosen(options.filter { it.id in selectedOptionsList }.toList())
}
}
},
modifier = modifier,
) {
val selectedSummary = when (selectedOptionsList.size) {
0 -> ""
1 -> options.first { it.id == selectedOptionsList.first() }.text
else -> "Wybrano ${selectedOptionsList.size}"
}
TextField(
enabled = isEnabled(),
modifier = Modifier.menuAnchor(),
readOnly = true,
value = selectedSummary,
onValueChange = {},
label = { Text(text = labelText) },
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded)
},
colors = ExposedDropdownMenuDefaults.textFieldColors(),
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = {
expanded = false
onOptionsChosen(options.filter { it.id in selectedOptionsList }.toList())
},
) {
for (option in options) {
//use derivedStateOf to evaluate if it is checked
var checked = remember {
derivedStateOf{option.id in selectedOptionsList}
}.value
DropdownMenuItem(
text = {
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(
checked = checked,
onCheckedChange = { newCheckedState ->
if (newCheckedState) {
selectedOptionsList.add(option.id)
} else {
selectedOptionsList.remove(option.id)
}
},
)
Text(text = option.text)
}
},
onClick = {
if (!checked) {
selectedOptionsList.add(option.id)
} else {
selectedOptionsList.remove(option.id)
}
},
contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
)
}
}
}
}
I have the next code, everything is fine except that I need to put my second AnswerOption in a lazyverticalGrid.
Which i cant do it, because its inside a lazycolumn this is the code
#Composable
fun AssessmentScreen(
onClose: (String, String) -> Unit,
relatedSubSkillIdsJson: String,
uiState: AssessmentUiState,
onSelectedOption: (String) -> Unit,
onShowAnswerFeedback: (Boolean) -> Unit,
onNextQuestion: () -> Unit,
onCloseAssessment: () -> Unit,
navigateToAndPop: (Pair<String, String>) -> Unit,
goBack: () -> Unit,
assessmentType: String,
) {
val context = LocalContext.current
val activity = context.findActivity()
val navigationBarHeightDp = activity?.getNavigationBarHeightInDp() ?: 0.dp
val blur =
if (uiState.isLoading) dimensionResource(dimen.default_screen_blur) else 0.dp
val currentArtiIndex = remember { mutableStateOf(0) }
if (uiState.questions.isNotEmpty()) {
LazyColumn(
Modifier
.fillMaxSize()
.padding(
top = dimensionResource(dimen.default_screen_padding),
start = dimensionResource(dimen.default_screen_padding),
end = dimensionResource(dimen.default_screen_padding)
)
.blur(blur)
) {
item {
CloseAssessment(
relatedSubSkillIdsJson = relatedSubSkillIdsJson,
uiState = uiState,
questionIndex = uiState.currentQuestionIndex,
isAnsweredQuestion = uiState.showAnswerFeedback,
onCloseAssessment = { onCloseAssessment() },
navigateToAndPop = navigateToAndPop,
goBack = goBack
)
AssessmentTitle(artiImages[currentArtiIndex.value], assessmentType)
QuestionDotsIndicator(uiState.questionAnswersStatus)
AssessmentQuestion(
thumbnail = uiState.getCurrentQuestionItem().thumbnail,
question = uiState.getCurrentQuestionItem().question
)
Spacer(modifier = Modifier.height(20.dp))
}
val optionsAbcLetterDescription = ('a'..'z').toList()
itemsIndexed(uiState.currentAnswerOptions) { index, option ->
if (uiState.currentAnswerOptions[index].thumbnail == ""){
AnswerOption(
selectedOptionId = uiState.selectedAnswerId,
showFeedback = uiState.showAnswerFeedback,
optionLetter = circularCharIteration(optionsAbcLetterDescription, index),
option = option,
onSelectedOption = { onSelectedOption(it) }
)
}else{
AnswerOption(
selectedOptionId = uiState.selectedAnswerId,
showFeedback = uiState.showAnswerFeedback,
optionLetter = circularCharIteration(optionsAbcLetterDescription, index),
option = option,
onSelectedOption = { onSelectedOption(it) }
)
}
}
item {
ChallengeBtn(
modifier = Modifier
.padding(bottom = navigationBarHeightDp + dimensionResource(dimen.default_screen_padding)),
uiState = uiState,
navigateToAndPop = navigateToAndPop,
onShowAnswerFeedback = { onShowAnswerFeedback(it) },
onCloseAssessment = { onCloseAssessment() },
onNextQuestion = {
if (!uiState.isLastQuestion) {
currentArtiIndex.value =
nextArtiImage(currentArtiIndex.value)
onNextQuestion()
}
} ,
relatedSubSkillIdsJson = relatedSubSkillIdsJson,
)
}
}
}
ProgressBarComponentComposable(isLoading = uiState.isLoading)
any idea on how to do it?
wrap it inside item{…} block
...
val optionsAbcLetterDescription = ('a'..'z').toList()
itemsIndexed(uiState.currentAnswerOptions) { index, option ->
if (uiState.currentAnswerOptions[index].thumbnail == "") {
this#LazyColumn.item { // item block
AnswerOption(
...
)
}
} else {
this#LazyColumn.item { // item block
AnswerOption(
...
)
}
}
...
or you can make the AnswerOption an extension of LazyItemScope,
#Composable
fun LazyItemScope.AnswerOption() {...}
and you can simply call it like this
...
val optionsAbcLetterDescription = ('a'..'z').toList()
itemsIndexed(uiState.currentAnswerOptions) { index, option ->
if (uiState.currentAnswerOptions[index].thumbnail == "") {
AnswerOption(
...
)
} else {
AnswerOption(
...
)
}
...
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 composable button, which can display a text or a loader, depending on state
enum class State { NORMAL, LOADING }
#Composable
fun MyButton(onClick: () -> Unit, text: String, state: State) {
Button(onClick, Modifier.height(60.dp)) {
if (state == State.NORMAL) {
Text(text, fontSize = 32.sp)
} else {
CircularProgressIndicator(color = Color.Yellow)
}
}
}
and I use it like this:
MyButton(
onClick = {
state = if (state == State.NORMAL) State.LOADING else State.NORMAL
},
"hello",
state
)
But the button shrinks when going to loading state. How can I measure it in normal state, to assign this measured width in loading state later?
Assuming that the normal state comes before the loading state, you can use the onGloballyPositioned modifier to get the size of Text and apply it to the CircularProgressIndicator.
Something like:
#Composable
fun MyButton(onClick: () -> Unit, text: String, state: State) {
var sizeText by remember { mutableStateOf(IntSize.Zero) }
Button(onClick, Modifier.height(60.dp)) {
if (state == State.NORMAL) {
Text(text, fontSize = 32.sp,
modifier = Modifier.onGloballyPositioned {
sizeText = it.size
})
} else {
Box(Modifier.size(with(LocalDensity.current){ (sizeText.width).toDp()},with(LocalDensity.current){ (sizeText.height).toDp()}),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(color = Color.Yellow)
}
}
}
}
You can use a custom layout - this works both when you start with Loading or Normal:
#Composable
fun CustomContainer(
modifier: Modifier = Modifier,
state: State,
content: #Composable () -> Unit,
) {
Layout(
modifier = modifier,
content = content,
) { measurables, constraints ->
check(measurables.size == 2) { "This composable requires 2 children" }
val first = measurables[0]
val second = measurables[1]
val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
val firstPlaceable = first.measure(looseConstraints)
val secondPlaceable = second.measure(looseConstraints)
val requiredWidth = max(firstPlaceable.width, secondPlaceable.width)
val requiredHeight = max(firstPlaceable.height, secondPlaceable.height)
layout(
requiredWidth,
requiredHeight
) {
when (state) {
State.LOADING -> {
firstPlaceable.place(
x = (requiredWidth - firstPlaceable.width) / 2,
y = (requiredHeight - firstPlaceable.height) / 2,
)
}
State.NORMAL -> {
secondPlaceable.place(
x = (requiredWidth - secondPlaceable.width) / 2,
y = (requiredHeight - secondPlaceable.height) / 2,
)
}
}
}
}
}
#Preview
#Composable
fun CustomButtonPreview() {
SampleTheme {
var state by remember {
mutableStateOf(State.LOADING)
}
Button(
onClick = {
state = when (state) {
State.NORMAL -> State.LOADING
State.LOADING -> State.NORMAL
}
},
Modifier.height(60.dp)
) {
CustomContainer(state = state) {
CircularProgressIndicator(color = Color.Yellow)
Text("Text here", fontSize = 32.sp)
}
}
}
}
You could create a variable (nullable) named size. Just wrap your Text in a BoxWithConstraints, then assign the size to the variable. Remeber this value through recompositions, and you should be good.
you could check the width of the button after it's been displayed and save it to use for later, here is a working solution using that way.
#Composable
fun MyButton(onClick: () -> Unit, text: String, state: State) {
val currentContext= LocalContext.current
var textWidth by remember{ mutableStateOf(0)}
val textModifier= if(textWidth ==0)Modifier else Modifier.width(textWidth.dp)
Button(onClick, textModifier.height(60.dp).onGloballyPositioned {
textWidth= it.size.width.toDp(currentContext)
}) {
if (state == State.NORMAL) {
Text(text, fontSize = 32.sp)
} else {
CircularProgressIndicator(color = Color.Yellow)
}
}
}
fun Int.toDp(context: Context): Int = (this / context.resources.displayMetrics.density).toInt()
I have a LazyVerticalGrid with 2 cells.
LazyVerticalGrid(
cells = GridCells.Fixed(2),
content = {
items(moviePagingItems.itemCount) { index ->
val movie = moviePagingItems[index] ?: return#items
MovieItem(movie, Modifier.preferredHeight(320.dp))
}
renderLoading(moviePagingItems.loadState)
}
)
I am trying to show full width loading with LazyGridScope's fillParentMaxSize modifier.
fun LazyGridScope.renderLoading(loadState: CombinedLoadStates) {
when {
loadState.refresh is LoadState.Loading -> {
item {
LoadingColumn("Fetching movies", Modifier.fillParentMaxSize())
}
}
loadState.append is LoadState.Loading -> {
item {
LoadingRow(title = "Fetching more movies")
}
}
}
}
But since we have 2 cells, the loading can occupy half of the screen. Like this:
Is there a way my loading view can occupy full width?
Jetpack Compose 1.1.0-beta03 version includes horizontal span support for LazyVerticalGrid.
Here is the example usage:
private const val CELL_COUNT = 2
private val span: (LazyGridItemSpanScope) -> GridItemSpan = { GridItemSpan(CELL_COUNT) }
LazyVerticalGrid(
cells = GridCells.Fixed(CELL_COUNT),
content = {
items(moviePagingItems.itemCount) { index ->
val movie = moviePagingItems.peek(index) ?: return#items
Movie(movie)
}
renderLoading(moviePagingItems.loadState)
}
}
private fun LazyGridScope.renderLoading(loadState: CombinedLoadStates) {
if (loadState.append !is LoadState.Loading) return
item(span = span) {
val title = stringResource(R.string.fetching_more_movies)
LoadingRow(title = title)
}
}
Code examples of this answer can be found at: Jetflix/MoviesGrid.kt
LazyVerticalGrid has a span strategy built into items() and itemsIndexed()
#Composable
fun SpanLazyVerticalGrid(cols: Int, itemList: List<String>) {
val lazyGridState = rememberLazyGridState()
LazyVerticalGrid(
columns = GridCells.Fixed(cols),
state = lazyGridState
) {
items(itemList, span = { item ->
val lowercase = item.lowercase()
val span = if (lowercase.startsWith("a") || lowercase.lowercase().startsWith("b") || lowercase.lowercase().startsWith("d")) {
cols
}
else {
1
}
GridItemSpan(span)
}) { item ->
Box(modifier = Modifier
.fillMaxWidth()
.height(150.dp)
.padding(10.dp)
.background(Color.Black)
.padding(2.dp)
.background(Color.White)
) {
Text(
modifier = Modifier.align(Alignment.Center),
text = item,
fontSize = 18.sp
)
}
}
}
}
'
val names = listOf("Alice", "Bob", "Cindy", "Doug", "Ernie", "Fred", "George", "Harris")
SpanLazyVerticalGrid(
cols = 3,
itemList = names
)
Try something like:
var cellState by remember { mutableStateOf(2) }
LazyVerticalGrid(
cells = GridCells.Fixed(cellState),
content = {
items(moviePagingItems.itemCount) { index ->
val movie = moviePagingItems[index] ?: return#items
MovieItem(movie, Modifier.preferredHeight(320.dp))
}
renderLoading(moviePagingItems.loadState) {
cellState = it
}
}
)
The renderLoading function:
fun LazyGridScope.renderLoading(loadState: CombinedLoadStates, span: (Int) -> Unit) {
when {
loadState.refresh is LoadState.Loading -> {
item {
LoadingColumn("Fetching movies", Modifier.fillParentMaxSize())
}
span(1)
}
...
else -> span(2)
}
}
I have created an issue for it: https://issuetracker.google.com/u/1/issues/176758183
Current workaround I have is to use LazyColumn and implement items or header.
override val content: #Composable () -> Unit = {
LazyColumn(
contentPadding = PaddingValues(8.dp),
content = {
items(colors.chunked(3), itemContent = {
Row(horizontalArrangement = Arrangement.SpaceEvenly) {
val modifier = Modifier.weight(1f)
it.forEach {
ColorItem(modifier, it)
}
for (i in 1..(3 - it.size)) {
Spacer(modifier)
}
}
})
item {
Text(
text = stringResource(R.string.themed_colors),
style = MaterialTheme.typography.h3
)
}
items(themedColors.chunked(3), itemContent = {
Row(horizontalArrangement = Arrangement.SpaceEvenly) {
val modifier = Modifier.weight(1f)
it.forEach {
ColorItem(modifier, it)
}
for (i in 1..(3 - it.size)) {
Spacer(modifier)
}
}
})
})
}