Following problem: I created a Compose View which should display a item list (it also should display more things in future development).
I created following view:
data class ItemHolder(
val header: String,
val subItems: List<String>,
val footer: String
)
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Create items
val items = (1..20).map { itemIndex ->
ItemHolder(
header = "Header of $itemIndex",
subItems = (1..30).map { subItemIndex ->
"Sub item $subItemIndex of $itemIndex"
},
footer = "Footer of $itemIndex"
)
}
setContent {
Column(
modifier = Modifier.verticalScroll(rememberScrollState())
) {
Text(text = "Items:")
ItemList(items = items)
}
}
}
}
// Displays the list of items
#Composable
fun ItemList(items: List<ItemHolder>) {
LazyColumn {
items(items = items) {
Item(item = it)
}
}
}
// Displays a single item
#Composable
fun Item(item: ItemHolder) {
var subItemsVisible by remember { mutableStateOf(false) }
// Displays the header of the item
Row {
Text(text = item.header)
Button(
onClick = { subItemsVisible = !subItemsVisible },
content = {
Text(text = if (subItemsVisible) "Hide" else "Show")
}
)
}
// Displays the sub items of the item
AnimatedVisibility(visible = subItemsVisible) {
Column {
for (subItem in item.subItems) {
Text(text = subItem)
}
}
}
// Displays the footer of the item
Text(text = item.footer)
}
I found out that the problem is, that the outer Column (which is scrollable) contains the LazyColumn which contains the actual items.
I get following error:
java.lang.IllegalStateException: Vertically scrollable component was measured with an infinity maximum height constraints, which is disallowed.
I was searching around for hours, but didn't find any suitable solution for my problem.
How can I fix this?
I think you have to remove modifier = Modifier.verticalScroll(rememberScrollState()) it will not work with nested lazy column
refer this link may be help you :https://proandroiddev.com/nested-scroll-with-jetpack-compose-9c3b054d2e12
I edit your code I hope it will help you
data class ItemHolder(
val header: String,
val subItems: List<String>,
val footer: String
)
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val items = (1..4).map { itemIndex ->
ItemHolder(
header = "Header of $itemIndex",
subItems = (1..30).map { subItemIndex ->
"Sub item $subItemIndex of $itemIndex"
},
footer = "Footer of $itemIndex"
)
}
setContent {
LazyColumn(
modifier = Modifier.padding(10.dp)
) {
item {
Text(text = "Items: Header", color = Color.Red, fontSize = 20.sp)
Spacer(modifier = Modifier.height(20.dp))
}
items(items = items) {
Item(item = it)
}
item {
Spacer(modifier = Modifier.height(20.dp))
Text(text = "Items: Footer", color = Color.Red, fontSize = 20.sp)
Spacer(modifier = Modifier.height(20.dp))
}
items(items = items) {
Item(item = it)
}
}
}
}
}
// Displays a single item
#Composable
fun Item(item: ItemHolder) {
var subItemsVisible by remember { mutableStateOf(false) }
// Displays the header of the item
Row {
Text(text = item.header)
Button(
onClick = { subItemsVisible = !subItemsVisible },
content = {
Text(text = if (subItemsVisible) "Hide" else "Show")
}
)
}
// Displays the sub items of the item
AnimatedVisibility(visible = subItemsVisible) {
Column {
for (subItem in item.subItems) {
Text(text = subItem)
}
}
}
// Displays the footer of the item
Text(text = item.footer)
}
Related
I have a LazyColumn that displays a list of shopping list items retrieved from the database in the ViewModel. If the retrieved list of items is empty, the LazyColumn shows the following message: "You don't have any items in this shopping list." The problem is that this message displays briefly for 1 second before the items are displayed. To solve the problem I implemented a circular progress bar while the items are being retrieved, but it does not even appear, and the message is still displayed. How can I fix this?
ViewModel
#HiltViewModel
class ShoppingListScreenViewModel #Inject constructor(
private val getAllShoppingListItemsUseCase: GetAllShoppingListItemsUseCase
) {
private val _shoppingListItemsState = mutableStateOf<Flow<PagingData<ShoppingListItem>>?>(null)
val shoppingListItemsState: State<Flow<PagingData<ShoppingListItem>>?> get() = _shoppingListItemsState
val loading = mutableStateOf(false)
init {
loading.value = true
getAllShoppingListItemsFromDb()
}
private fun getAllShoppingListItemsFromDb() {
viewModelScope.launch {
_shoppingListItemsState.value = getAllShoppingListItemsUseCase().distinctUntilChanged()
loading.value = false
}
}
}
ShoppingListScreen Composable
fun ShoppingListScreen(
navController: NavHostController,
shoppingListScreenViewModel: ShoppingListScreenViewModel,
sharedViewModel: SharedViewModel
) {
val scope = rememberCoroutineScope()
val focusManager = LocalFocusManager.current
val screenHeight = LocalConfiguration.current.screenHeightDp.dp
val allItems = shoppingListScreenViewModel.shoppingListItemsState.value?.collectAsLazyPagingItems()
val showProgressBar = shoppingListScreenViewModel.loading.value
Scaffold(
topBar = {
CustomAppBar(
title = "Shopping List Screen",
titleFontSize = 20.sp,
appBarElevation = 4.dp,
navController = navController
)
},
floatingActionButton = {
FloatingActionButton(
onClick = {
shoppingListScreenViewModel.setStateValue(SHOW_ADD_ITEM_DIALOG_STR, true)
},
backgroundColor = Color.Blue,
contentColor = Color.White
) {
Icon(Icons.Filled.Add, "")
}
},
backgroundColor = Color.White,
// Defaults to false
isFloatingActionButtonDocked = false,
bottomBar = { BottomNavigationBar(navController = navController) }
) {
Box {
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.height(screenHeight)
) {
item {
if (allItems != null && allItems.itemCount == 0) {
Text("You don't have any items in this shopping list.")
}
}
items(
items = allItems!!,
key = { item ->
item.id
}
) { item ->
ShoppingListScreenItem(
navController = navController,
item = item,
sharedViewModel = sharedViewModel
) { isChecked ->
scope.launch {
shoppingListScreenViewModel.changeItemChecked(item!!, isChecked)
}
}
}
item { Spacer(modifier = Modifier.padding(screenHeight - (screenHeight - 70.dp))) }
}
ConditionalCircularProgressBar(isDisplayed = showProgressBar)
}
}
}
Quick solution:
val showProgressBar = shoppingListScreenViewModel.loading.collectAsState() //Use collectAsState, with Flow state in the ViewModel
and add this condition to the "You don't have any items..." if like so:
if (allItems != null && allItems.itemCount == 0 && !showProgressBar) { ... }
The better solution would be to implement this with sealed class(es), where you would return different class for different state (e.g. Loading, Error, Empty, Data). And on the UI side, you just need to when over the possible types of data. Here you can find a perfect example.
I'm currently trying to show a list of EventType (custom class) objects as DropdownMenuItems in a DropdownMenu.
The code I'm trying is:
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
items(plannerViewModel.eventTypeList) { eventType ->
DropdownMenuItem(onClick = { /*TODO*/ }) {
TypeOfEvent(eventType.color, eventType.name, openDialog)
}
}
}
The problem is the items() function is not being recognized and I don't know how else it could be done.
items is for LazyColumn. It doesn't exist on DropdownMenu.
You can just use a for loop in this case
plannerViewModel.eventTypeList.forEach { eventType ->
DropdownMenuItem(...)
}
Below is syntax of Drop Down menu.There is no scope to add items in syntax.
#Composable
fun DropdownMenu(
expanded: Boolean,
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
offset: DpOffset = DpOffset(0.dp, 0.dp),
properties: PopupProperties = PopupProperties(focusable = true),
content: #Composable #ExtensionFunctionType ColumnScope.() -> Unit
): Unit
If you want to add multiple items than you can use for loop like below.
#Preview
#Composable
fun DropdownDemo() {
var expanded by remember { mutableStateOf(false) }
val items = listOf("A", "B", "C", "D", "E", "F")
val disabledValue = "B"
var selectedIndex by remember { mutableStateOf(0) }
Box(modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.TopStart)) {
Text(items[selectedIndex],modifier = Modifier.fillMaxWidth().clickable(onClick = { expanded = true }).background(
Color.Gray))
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
modifier = Modifier.fillMaxWidth().background(
Color.Red)
) {
items.forEachIndexed { index, s ->
DropdownMenuItem(onClick = {
selectedIndex = index
expanded = false
}) {
val disabledText = if (s == disabledValue) {
" (Disabled)"
} else {
""
}
Text(text = s + disabledText)
}
}
}
}
}
My app contains 2 different layouts for different device types. I want different actions to be performed e.g. 1st, 2nd and 3rd items to navigate to different screens, 4th item to show a Toast, the 5th item to launch an email composer intent, etc. Is there way to find out which index of the array was clicked and making these click events reusable rather than creating the same click event twice?
array in strings.xml
<string-array name="array_main">
<item>#string/breads</item>
<item>#string/cakes</item>
<item>#string/condiments</item>
<item>#string/desserts</item>
<item>#string/snacks</item>
<item>#string/contact us</item>
</string-array>
MainActivity.kt
class ActivityMain : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyAppTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
HomeScreen(navController = rememberNavController())
}
}
}
}
#Composable
fun ComposeNavigation() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "home_screen"
) {
composable("home_screen") {
HomeScreen(navController = navController)
}
composable("second_screen") {
SecondScreen(navController = navController)
}
composable("third_screen") {
ThirdScreen(navController = navController)
}
}
}
#Composable
fun HomeScreen(navController: NavController) {
val windowInfo = rememberWindowInfo()
if (windowInfo.screenWidthInfo is WindowInfo.WindowType.Compact) {
Scaffold(
topBar = {...},
content = {
MyLazyColumn(lazyItems = resources.getStringArray(R.array.array_main))
},
)
} else {
Scaffold(
topBar = {...},
content = {
MyLazyVerticalGrid(lazyItems = resources.getStringArray(R.array.array_main))
},
)
}
}
}
MyLazyColumn.kt
#Composable
fun MyLazyColumn(lazyItems: Array<String>) {
val listState = rememberLazyListState()
LazyColumn(
state = listState,
) {
items(lazyItems) { item ->
Row(modifier = Modifier
.fillMaxWidth()
.clickable {
}) {
Text(
text = item,
)
}
}
}
}
MyLazyVerticalGrid.kt
fun MyLazyVerticalGrid(lazyItems: Array<String>) {
val lazyGridState = rememberLazyGridState()
LazyVerticalGrid(
state = lazyGridState,
columns = GridCells.Fixed(2),
content = {
items(lazyItems.size) { index ->
OutlinedButton(
modifier = Modifier.padding(7.dp),
onClick = {/*TODO*/ }) {
Text(text = lazyItems[index]
)
}
}
}
)
}
Since we're using Compose, we can take advantage of Kotlin.
Instead of creating a string-array resource, we can create an enum class with all the choices like this:
enum class Choices(#StringRes val textResId: Int) {
Breads(R.string.breads),
Cakes(R.string.cakes),
Condiments(R.string.condiments),
Desserts(R.string.desserts),
Snacks(R.string.snacks),
ContactUs(R.string.contact_us),
}
Then, we can setup your LazyColumn and LazyGrid like this:
#Composable
fun MyLazyColumn(
lazyItems : Array<Choice>,
onClickItem: (Choice) -> Unit
) {
//...
items(lazyItems) { choice ->
//...
Text(text = stringResource(choice.textResId))
Button(
//...
onClick = { onClickItem(choice) }
)
//...
}
//...
}
#Composable
fun MyLazyGrid(
lazyItems : Array<Choice>,
onClickItem: (Choice) -> Unit
) {
// ...
items(lazyItems) { choice ->
Text(text = stringResource(choice.textResId))
Button(
onClick = { onClickItem(choice) }
)
}
// ...
}
To reuse the ClickListener, we can create a lambda object and reuse it:
#Composable
fun HomeScreen(/* ... */) {
val listener: (Choice) -> Unit = { choice ->
when(choice) {
Breads -> {}
Cakes -> {}
Condiments -> {}
Desserts -> {}
Snacks -> {}
ContactUs -> {}
}
}
MyLazyGrid(
lazyItems = arrayOf(
/* Choice items you'd add to you list */
),
onClickItem = listener
)
MyLazyColumn(
lazyItems = arrayOf(
/* Choice items you'd add to you list */
),
onClickItem = listener
)
}
Just store your click-codeblocks as variables.
val firstItemClicker = { /* Add Actions Here */ }
val secondItemClicker = { /* and So on */ }
Use it like
FirstItem(
Modifier.clickable { firstItemClicker() }
)
Or,
Button(
onclick = { firstItemClicker() }
){
Text("Click Recyler")
}
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)
}
}
})
})
}
I have an issue with Jetpack compose displaying a model containing a ModelList of items. When new items are added, the order of the UI elements becomes incorrect.
Here's a very simple CounterModel containing a ModelList of ItemModels:
#Model
data class CounterModel(
var counter: Int = 0,
var name: String = "",
val items: ModelList<ItemModel> = ModelList()
)
#Model
data class ItemModel(
var name: String
)
The screen shows two card rows for each ItemModel: RowA and RowB.
When I create this screen initialised with the following CounterModel:
val model = CounterModel()
model.name="hi"
model.items.add(ItemModel("Item 1"))
model.items.add(ItemModel("Item 2"))
CounterModelScreen(model)
...it displays as expected like this:
Item 1
Row A
Item 1
Row B
Item 2
Row A
Item 2
Row B
When I click my 'add' button, to insert a new ItemModel, I simply expect to see
Item 3
Row A
Item 3
Row B
At the bottom. But instead, the order is jumbled, and I see two rowAs then two rowBs:
Item 1
Row A
Item 1
Row B
Item 2
Row A
Item 3
Row A
Item 3
Row B
Item 2
Row B
I don't really understand how this is possible. The UI code is extremely simple: loop through the items and emit RowA and RowB for each one:
for (i in counterModel.items.indices) {
RowA(counterModel, i)
RowB(counterModel, i)
}
Using Android Studio 4.0C6
Here's the complete code:
#Composable
fun CounterModelScreen(counterModel: CounterModel) {
Column {
TopAppBar(title = {
Text(
text = "Counter Model"
)
})
CounterHeader(counterModel)
for (i in counterModel.items.indices) {
RowA(counterModel, i)
RowB(counterModel, i)
}
Button(
text = "Add",
onClick = {
counterModel.items.add(ItemModel("Item " + (counterModel.items.size + 1)))
})
}
}
#Composable
fun CounterHeader(counterModel: CounterModel) {
Text(text = counterModel.name)
}
#Composable
fun RowA(counterModel: CounterModel, index: Int) {
Padding(padding = 8.dp) {
Card(color = Color.White, shape = RoundedCornerShape(4.dp)) {
Column(crossAxisSize = LayoutSize.Expand) {
Text(
text = counterModel.items[index].name
)
Text(text = "Row A")
}
}
}
}
#Composable
fun RowB(counterModel: CounterModel, index: Int) {
Padding(padding = 8.dp) {
Card(color = Color.Gray, shape = RoundedCornerShape(4.dp)) {
Column(crossAxisSize = LayoutSize.Expand) {
Text(
text = counterModel.items[index].name
)
Text(text = "Row B")
}
}
}
}
I have tested it using compose-1.0.0-alpha07 and making some changes to adapt the code to the changed APIs. Everything works flawlessly so my guess is that something was broken in an older version of compose as the code looks correct and works in more recent versions with the mentioned changes.
I have also modified your code to use states as recommended in the docs and added a ViewModel that will help you decouple the Views from the data management:
ViewModel
class CounterModelViewModel : ViewModel() {
private val myBaseModel = CounterModel().apply {
name = "hi"
items.add(ItemModel("Item 1"))
items.add(ItemModel("Item 2"))
}
private val _modelLiveData = MutableLiveData(myBaseModel)
val modelLiveData: LiveData<CounterModel> = _modelLiveData
fun addNewItem() {
val oldCounterModel = modelLiveData.value ?: CounterModel()
// Items is casted to a new MutableList because the new state won't be notified if the new
// counter model content is the same one as the old one. You can also change any other
// properties instead like the name or the counter
val newItemsList = oldCounterModel.items.toMutableList()
newItemsList.add(ItemModel("Item " + (newItemsList.size + 1)))
// Pass a new instance of CounterModel to the LiveData
val newCounterModel = oldCounterModel.copy(items = newItemsList)
_modelLiveData.value = newCounterModel
}
}
Composable Views updated:
#Composable
fun CounterModelScreen(counterModel: CounterModel, onAddNewItem: () -> Unit) {
ScrollableColumn {
TopAppBar(title = {
Text(
text = "Counter Model"
)
})
CounterHeader(counterModel)
counterModel.items.forEachIndexed { index, item ->
RowA(counterModel, index)
RowB(counterModel, index)
}
Button(
onClick = onAddNewItem
) {
Text(text = "Add")
}
}
}
#Composable
fun CounterHeader(counterModel: CounterModel) {
Text(text = counterModel.name)
}
#Composable
fun RowA(counterModel: CounterModel, index: Int) {
Card(
backgroundColor = Color.White,
shape = RoundedCornerShape(4.dp),
modifier = Modifier.padding(8.dp)
) {
Column(modifier = Modifier.fillMaxWidth()) {
Text(
text = counterModel.items[index].name
)
Text(text = "Row A")
}
}
}
#Composable
fun RowB(counterModel: CounterModel, index: Int) {
Card(
backgroundColor = Color.Gray,
shape = RoundedCornerShape(4.dp),
modifier = Modifier.padding(8.dp)
) {
Column(modifier = Modifier.fillMaxWidth()) {
Text(
text = counterModel.items[index].name
)
Text(text = "Row B")
}
}
}
This previous code is called from another composable function that contains the instance of the ViewModel, however you can change this to an activity or a fragment with an instance of the mentioned ViewModel, it's up to your preference.
#Composable
fun MyCustomScreen(viewModel: CounterModelViewModel = viewModel()) {
val modelState: CounterModel by viewModel.modelLiveData.observeAsState(CounterModel())
CounterModelScreen(
counterModel = modelState,
onAddNewItem = {
viewModel.addNewItem()
}
)
}