Rearranging LazyColumn Recompositing issues - android

I have a LazyColumn that takes a list from my Room database.
I am creating a button that can re arrange the list from newest first, or oldest first. The problem I'm having is that when I rearrange the list, the LazyColumns view drops to the bottom of the LazyColumn. I do NOT want the list view to change during the list change. I am using a key for the list which is where I suspect my issue is coming from.
When I disable the key, this is not an issue however, that comes with its own issues so I cannot disable it permanently. Does anyone know and easy fix to this?
my composable ->
#OptIn(ExperimentalFoundationApi::class)
#Composable
fun MainScreen(navController: NavController, notesViewModel: NotesViewModel) {
val myUiState by notesViewModel.uiState.collectAsState()
val multiDelete = remember { mutableStateListOf<Note>() }
val scope = rememberCoroutineScope()
val state = rememberLazyListState()
Surface {
Column {
Row {
FloatingActionButton(onClick = { notesViewModel.updateStates(true) }) {}
FloatingActionButton(onClick = { notesViewModel.updateStates(false) }) {}
NewNote(navController)
if(multiDelete.isNotEmpty()){
FloatingActionButton(
onClick = {
scope.launch {
notesViewModel.deleteSelected(multiDelete)
delay(50)
multiDelete.clear()
}
}
) { Image(imageVector = Icons.Filled.Delete, contentDescription = "this") }
}
}
LazyColumn(
state = state,
horizontalAlignment = Alignment.CenterHorizontally,
contentPadding = PaddingValues(vertical = 10.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
modifier = Modifier
.background(color = Color.Gray)
.fillMaxSize()
.focusRequester(FocusRequester()),
) {
items(
if(myUiState.toggle) myUiState.allNotes else myUiState.allNotes.reversed(),
key = {notes -> notes.uid!!}
) {
notes ->
Column(
modifier = Modifier.animateItemPlacement()
) {
ConsoleCards(
note = notes,
onDeleteClick = {
notesViewModel.delete(notes)
},
onLongPress = {
if(multiDelete.contains(notes)) multiDelete.remove(notes) else multiDelete.add(notes)
},
onEditClick = {
notesViewModel.uid(notes.uid!!)
notesViewModel.header(notes.header!!)
notesViewModel.note(notes.note!!)
navController.navigate(route = PageNav.AddNote.name)
}
)
}
}
}
}
}
}

This may not be the best solution. Theres also a similar issue like this and this
itemsIndexed(
items = checkItems.sortedBy { it.checked.value },
key = { index, item -> if (index == 0) index else item.id }
) { index, entry ->
...
}

Related

Scroll all screen with a lazy column and paging library inside

I'm new to Jetpack Compose and I'm not quite sure how to do what I need. In the screen below, I want to scroll the whole screen and not just the list at the bottom and when the scroll reaches the end of the list below, it still applies the paging library and goes to get more elements. I managed to get the Paging Library to work and the scroll in the list below too, but I can't make the rest of the page elements scroll as well - this is because only the list has scroll and not the rest of the page. Whenever I'm trying to do that, I get the following crash:
Vertically scrollable component was measured with an infinity maximum height constraints, which is disallowed. One of the common reasons is nesting layouts like LazyColumn and Column(Modifier.verticalScroll()). If you want to add a header before the list of items please add a header as a separate item() before the main items() inside the LazyColumn scope. There are could be other reasons for this to happen: your ComposeView was added into a LinearLayout with some weight, you applied Modifier.wrapContentSize(unbounded = true) or wrote a custom layout. Please try to remove the source of infinite constraints in the hierarchy above the scrolling container.
and I don't really know why.
I leave you the code below and two screenshots: the first is the current state, where I can only scroll through the list. The second is what I intend, which is to scroll the entire page.
#Edit: I was able to implement all screen scroll with fixed height on the children lazy column, but that is not what I want.
#Composable
#ExperimentalFoundationApi
private fun MainActivityLayout(navController: NavHostController) {
LazyColumn(
modifier = Modifier
.paint(
painter = painterResource(id = R.drawable.main_background),
contentScale = ContentScale.FillBounds
)
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
item {
HeightSpacer(Dimen40)
Image(
painter = painterResource(id = R.drawable.ic_clearjobs_logo_2x),
contentDescription = null
)
HeightSpacer(Dimen47)
Navigation(navController = navController)
}
}
}
#Composable
#ExperimentalFoundationApi
fun JobOpeningsScreen(viewModel: JobOpeningsViewModel = hiltViewModel()) {
val uiState by viewModel.uiState.collectAsState()
Column {
ClearJobsScreenTitle(
lightTitle = stringResource(id = R.string.job_openings_light_title),
boldTitle = stringResource(id = R.string.job_openings_bold_title)
)
HeightSpacer(Dimen60)
Row {
CategoryButton()
WidthSpacer(Dimen2)
OrderByButton()
}
HeightSpacer(Dimen30)
SearchTextField()
HeightSpacer(Dimen60)
when (uiState) {
is BaseViewState.Data -> JobOpeningsContent(
viewState = uiState.cast<BaseViewState.Data<JobOpeningsViewState>>().value
)
is BaseViewState.Loading -> {
LoadingView()
}
else -> {}
}
LaunchedEffect(key1 = viewModel, block = {
viewModel.onTriggerEvent(JobOpeningsEvent.LoadJobOffers)
})
}
}
#Composable
fun JobOpeningsContent(viewState: JobOpeningsViewState) {
val pagingItems = rememberFlowWithLifecycle(viewState.pagedData).collectAsLazyPagingItems()
SwipeRefresh(
state = rememberSwipeRefreshState(
isRefreshing = pagingItems.loadState.refresh == LoadState.Loading
),
onRefresh = { pagingItems.refresh() },
indicator = { state, trigger ->
SwipeRefreshIndicator(
state = state,
refreshTriggerDistance = trigger,
scale = true
)
},
content = {
LazyColumn(
modifier = Modifier.width(Dimen320),
verticalArrangement = Arrangement.spacedBy(Dimen30)
) {
items(pagingItems.itemCount) { index ->
pagingItems[index]?.let {
JobOpeningsRow(dto = it)
}
}
if (pagingItems.loadState.append == LoadState.Loading) {
item {
Box(
Modifier
.padding(24.dp)
) {
CircularProgressIndicator(Modifier.align(Alignment.Center))
}
}
}
}
}
)
}
I found the solution to this problem, although it is not 100% and in terms of code it is not as good as I would like.
The error speaks for itself, we can't have infinite vertical scroll, Jetpack Compose doesn't allow it. I had the option of putting a fixed height on the Lazy Column of my list, but it wasn't what I wanted and it didn't work properly. The solution was to put everything inside a single LazyColumn and remove the Column from MainActivity, using a Box element and contentAlignment. I leave you below the final code that I used to solve the problem.
MainScreen function that before was MainActivityLayout function:
#Preview
#Composable
#ExperimentalFoundationApi
fun MainScreen() {
val navController = rememberNavController()
val topLevelDestinations = listOf(
NavigationItem.JobOpenings,
NavigationItem.Profile,
NavigationItem.About
)
val isTopLevelDestination =
navController
.currentBackStackEntryAsState()
.value
?.destination
?.route in topLevelDestinations.map { it.route }
val backStackEntryState = navController.currentBackStackEntryAsState()
Scaffold(
bottomBar = {
if (isTopLevelDestination) {
BottomNavBar(
navController = navController,
backStackEntryState = backStackEntryState,
bottomNavItems = topLevelDestinations
)
}
}
) {
Box(
modifier = Modifier
.paint(
painter = painterResource(id = R.drawable.main_background),
contentScale = ContentScale.FillBounds
)
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Navigation(navController = navController)
}
}
}
New JobOpenings fun that is mixed with old JobOpeningsContent function:
#Composable
#ExperimentalFoundationApi
fun JobOpeningsScreen(viewModel: JobOpeningsViewModel = hiltViewModel()) {
val uiState by viewModel.uiState.collectAsState()
when (uiState) {
is BaseViewState.Data -> {
val pagedData = uiState.cast<BaseViewState.Data<JobOpeningsViewState>>().value.pagedData
val pagingItems = rememberFlowWithLifecycle(pagedData).collectAsLazyPagingItems()
SwipeRefresh(
state = rememberSwipeRefreshState(
isRefreshing = pagingItems.loadState.refresh == LoadState.Loading
),
onRefresh = { pagingItems.refresh() },
indicator = { state, trigger ->
SwipeRefreshIndicator(
state = state,
refreshTriggerDistance = trigger,
scale = true
)
},
content = {
LazyColumn(
modifier = Modifier
.width(Dimen320),
verticalArrangement = Arrangement.spacedBy(Dimen30)
) {
item {
ScreenHeader(
lightTitle = stringResource(id = R.string.job_openings_light_title),
boldTitle = stringResource(id = R.string.job_openings_bold_title)
)
HeightSpacer(Dimen60)
Row {
CategoryButton()
WidthSpacer(Dimen2)
OrderByButton()
}
HeightSpacer(Dimen30)
SearchTextField()
HeightSpacer(Dimen60)
}
items(pagingItems.itemCount) { index ->
pagingItems[index]?.let {
JobOpeningsRow(dto = it)
}
}
if (pagingItems.loadState.append == LoadState.Loading) {
item {
Box(Modifier.padding(Dimen24)) {
CircularProgressIndicator(Modifier.align(Alignment.Center))
}
}
}
}
}
)
}
is BaseViewState.Loading -> LoadingView()
else -> {}
}
LaunchedEffect(key1 = viewModel, block = {
viewModel.onTriggerEvent(JobOpeningsEvent.LoadJobOffers)
})
}
#ExperimentalFoundationApi
#Preview
#Composable
fun JobOpenings() {
JobOpeningsScreen()
}
Problems that I found with this solution:
LoadingView appears at the top of the screen instead at the top of the list.
If anyone has any suggestion to improve this, I am open to it. This works perfectly with Paging Library + Swipe Refresh (Accompanist) and full page scroll.

ordering a list on jetpack compose Android

I'm using a lazy column to show a list, the items in the list have the attribute isWeekTopic which is a Boolean, in the list I need to show first those elements that have isWeekTopic = true, and then show the ones that have isWeekTopic = false, the items that has isWeekTopic = true are going to change every week.
I managed to print the ones that has isWeekTopic = true with a header on the design, but I can't make them the first elements to show. this is my code
LazyColumn(
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(18.dp)
) {
items(
items = topicsskill,
key = { topicsSkill ->
topicsSkill.topicsId
}
) { topicsSkill ->
TopicsItemComposable(
modifier = Modifier
.fillMaxWidth(),
topicsItem = topicsSkill,
onItemClicked = { onItemClicked(it) }
)
}
}
I believe that sortedBy can do what you need.
Sample data class:
data class Something(
val title: String,
val isWeekTopic: Boolean
)
Extension to create dummy list:
fun createList(size: Int): List<Something> = List(size = size) { index ->
Something(
title = "something $index",
isWeekTopic = Random.nextBoolean()
)
}
Extension to order the list:
fun List<Something>.ordered(
weekTopicFirst: Boolean
): List<Something> =
if (weekTopicFirst) this.sortedBy { !it.isWeekTopic }
else this.sortedByDescending { !it.isWeekTopic }
Usage:
val items = createList(size = 15)
println("printing true first")
items.ordered(weekTopicFirst = true).forEach { println(it) }
println("printing false first")
items.ordered(weekTopicFirst = false).forEach { println(it) }
You can try this code here.
In Compose, just pass the list normally:
// just an example scenario, ideally it comes from a viewmodel
val dummyItems = createList(size = 15).ordered(weekTopicFirst = true)
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(all = 16.dp),
verticalArrangement = Arrangement.spacedBy(space = 16.dp)
) {
items(
items = dummyItems,
key = { it.title } // should be unique
) { something ->
Column(modifier = Modifier.fillMaxWidth()) {
Text(text = something.title)
Text(text = "week topic: ${something.isWeekTopic}")
}
}
}

Jetpack Compose LazyColumn inside Scrollabe Column

here's my situation: I have to show in my app a detail of a record I receive from API. Inside this view, I may or may not need to show some data coming from another viewmodel, based on a field.
Here my code:
#OptIn(ExperimentalMaterial3Api::class)
#Composable
fun ViewDetail(viewModel: MainViewModel, alias: String?, otherViewModel: OtherViewModel) {
viewModel.get(alias)
Scaffold {
val isLoading by viewModel.isLoading.collectAsState()
val details by viewModel.details.collectAsState()
when {
isLoading -> LoadingUi()
else -> Details(details, otherViewModel)
}
}
}
#OptIn(ExperimentalMaterial3Api::class)
#Composable
private fun Details(details: Foo?, otherViewModel: OtherViewModel) {
details?.let { sh ->
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState),
) {
Text(sh.title, fontSize = 24.sp, lineHeight = 30.sp)
Text(text = sh.description)
if (sh.other.isNotEmpty()) {
otherViewModel.load(sh.other)
val others by otherViewModel.list.collectAsState()
Others(others)
}
}
}
}
#OptIn(ExperimentalMaterial3Api::class)
#Composable
private fun Others(others: Flow<PagingData<Other>>) {
val items: LazyPagingItems<Other> = others.collectAsLazyPagingItems()
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
contentPadding = PaddingValues(16.dp),
) {
items(items = items) { item ->
if (item != null) {
Text(text = item.title, fontSize = 24.sp)
Spacer(modifier = Modifier.height(4.dp))
Text(text = item.description)
}
}
if (items.itemCount == 0) {
item { EmptyContent() }
}
}
}
All the description here may be very long, both on the main Details body or in the Others (when present), so here's why the scroll behaviour requested.
Problem: I get this error:
Vertically scrollable component was measured with an infinity maximum height constraints, which is disallowed. One of the common reasons is nesting layouts like LazyColumn and Column(Modifier.verticalScroll()).
I hoped that .wrapContentHeight() inside LazyColumn would do the trick, but to no avail.
Is this the right way to do it?
Context: all packages are updated to the latest versions available on maven
The main idea here is to merge your Column with LazyColumn.
As your code is not runnable, I'm giving more a pseudo code, which should theoretically work.
Also calling otherViewModel.load(sh.other) directly from Composable builder is a mistake. According to thinking in compose, to get best performance your view should be side effects free. To solve this issue Compose have special side effect functions. Right now your code is gonna be called on each recomposition.
if (sh.other.isNotEmpty()) {
LaunchedEffect(Unit) {
otherViewModel.load(sh.other)
}
}
val others by otherViewModel.list.collectAsState()
LazyColumn(
modifier = Modifier
.fillMaxSize()
.wrapContentHeight(),
contentPadding = PaddingValues(16.dp),
) {
item {
Text(sh.title, fontSize = 24.sp, lineHeight = 30.sp)
Text(text = sh.description)
}
items(items = items) { item ->
if (item != null) {
Text(text = item.title, fontSize = 24.sp)
Spacer(modifier = Modifier.height(4.dp))
Text(text = item.description)
}
}
if (items.itemCount == 0) {
item { EmptyContent() }
}
}
You can use a system like the following
#Composable
fun Test() {
Box(Modifier.systemBarsPadding()) {
Details()
}
}
#Composable
fun Details() {
LazyColumn(Modifier.fillMaxSize()) {
item {
Box(Modifier.background(Color.Cyan).padding(16.dp)) {
Text(text = "Hello World!")
}
}
item {
Box(Modifier.background(Color.Yellow).padding(16.dp)) {
Text(text = "Another data")
}
}
item {
Others()
}
}
}
#Composable
fun Others() {
val values = MutableList(50) { it }
values.forEach {
Box(
Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(text = "Value = $it")
}
}
}
The result with scroll is:

Mutable State Checkbox Not Changing Appearance After Selection - Jetpack Compose

There is a similar question here. I tried these solutions along with some others and still not having any success.
I have a checklist for different options in a dialog in Jetpack Compose. When an optionItem is selected I can see that the state has changed, but the checkbox icon won't update.
val _optionItems by remember {
mutableStateOf(
optionItemArray!!.map {
OptionItem(
label = it.label,
value = it.value,
number = it.number,
type = it.type,
subtitle = it.subtitle,
group = it.group,
matchType = it.matchType,
isSelected = it.isSelected,
isHidden = it.isHidden,
)
}
)
}
val optionItems: MutableList<OptionItem> = _optionItems as MutableList<OptionItem>
fun setOptionSelectedAtIndex(index: Int, isSelected: Boolean){
(_optionItems as MutableList<OptionItem>)[index] = _optionItems[index].copy(isSelected = !isSelected)
}
LazyColumn {
items(optionItems.size) { i ->
Row(
modifier = Modifier
.fillMaxWidth()
.clickable
{
optionItems.mapIndexed { j, item ->
if(i == j){
setOptionSelectedAtIndex(i, item.isSelected)
} else item
}
}
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
optionItems[i].label?.let { Text(text = it) }
if (optionItems[i].isSelected) {
Icon(
Icons.Filled.CheckBox,
contentDescription = "Selected"
)
} else {
Icon(
Icons.Filled.CheckBoxOutlineBlank,
contentDescription = "Not Selected"
)
}
}
}
}
}
I know that the list is supposed to be called again, which is what I was assuming the setOptionSelectedAtIndex function was supposed to do, but I'm having quite a lot of trouble figuring this out.

Swipe to refresh is not working if LazyGridView has no child in compose

I'm using SwipeRefresh view in compose (from accompanist). I have LazyVerticalGrid inside it, and grid populates paging data from network. If there's no content in paging data, i'm showing empty state view. When there's some data, swipe-to-refresh is working. Issue is, i'm not able to do swipe-to-refesh in LazyVerticalGrid if there's no data, but same is working in LazyColumn (both case has NoContentView shown).
#Composable
fun GridItems(
searchViewModel: SearchViewModel
) {
var isRefreshing by remember { mutableStateOf(false) }
val posts = remember { searchViewModel.posts }.collectAsLazyPagingItems()
Scaffold(
topBar = { MyTopBar() }
) { innerPadding ->
SwipeRefresh(
state = rememberSwipeRefreshState(isRefreshing = isRefreshing),
onRefresh = { searchViewModel.getPosts() }
) {
Column(modifier = Modifier.padding(innerPadding)) {
LazyVerticalGrid(
cells = GridCells.Fixed(3),
modifier = modifier.padding(horizontal = 3.dp)
) {
items() {
MySinglePostItem()
}
posts.apply {
when {
//Other loadState here
// Show below view if no item is present in paging data
loadState.refresh is LoadState.NotLoading && loadState.append.endOfPaginationReached && posts.itemCount < 1 -> {
NoContentView(Modifier.fillMaxSize())
}
}
}
}
}
}
}
}
#Composable
fun NoContentView(modifier: Modifier = Modifier) {
Box(
modifier = modifier,
contentAlignment = Alignment.Center
) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
modifier = Modifier.size(48.dp),
painter = painterResource(id = R.drawable.ic_outlined_image_no_content)
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = stringResource(id = R.string.empty_posts_message),
fontSize = 15.sp
)
}
}
}
As Accompanist states in its documentation SwipeRefresh needs some content that is scrollable in order to be able to react to swipe gestures.
So perhaps having too few elements Compose automatically makes a LazyColumn/LazyVerticalGrid non-scrollable in order to optimize things?
My best guess would be to show a Scrollable-Column when you have no items to show and a LazyVerticalGrid otherwise.
SwipeRefresh(...) {
if (loadState.refresh is LoadState.NotLoading && loadState.append.endOfPaginationReached && posts.itemCount < 1) {
Column(Modifier.verticalScroll(rememberScrollState())) {
NoContentView(Modifier.fillMaxSize())
}
} else {
LazyVerticalGrid(...) {
// ...
}
}
}

Categories

Resources