itemList.reverse()
itemList is mutableStateListOf() object inside viewModel, above line throws below given exception:
java.util.ConcurrentModificationException
at androidx.compose.runtime.snapshots.StateListIterator.validateModification(SnapshotStateList.kt:278)
at androidx.compose.runtime.snapshots.StateListIterator.set(SnapshotStateList.kt:271)
at java.util.Collections.reverse(Collections.java:435)
at kotlin.collections.CollectionsKt___CollectionsJvmKt.reverse(_CollectionsJvm.kt:43)
at in.rachika.composetest2.Tests.LazyColumnHeaderTest$MainScreenWithChildList$1$2.invoke(LazyColumnHeaderTest.kt:114)
at in.rachika.composetest2.Tests.LazyColumnHeaderTest$MainScreenWithChildList$1$2.invoke(LazyColumnHeaderTest.kt:111)
I am unable to figure out how to reverse or shuffle a mutableStateListOf() Object
reverse() works well in isolated situation but my LazyColumn has stickyHeader() and SwipeToDismiss(), don't know may be that is creating problem.
Model
data class TestModel(
val isHeader: Boolean,
val UniqueKey: UUID,
val GroupId: UUID,
val GroupName: String,
val ItemName: String,
val children: MutableList<TestModel>,
var isExpanded: Boolean = false)
ViewModel
class TestViewModel: ViewModel() {
var itemList = mutableStateListOf<TestModel>()
init {
viewModelScope.launch(Dispatchers.IO){
loadList()
}
}
private fun loadList() {
for(i in 0..20){
val groupName = "${i + 1}. STICKY HEADER #"
val groupUUID = UUID.randomUUID()
val childList = mutableListOf<TestModel>()
for(t in 0..Random.nextInt(10, 20)){
childList.add(TestModel(
isHeader = false,
UniqueKey = UUID.randomUUID(),
GroupId = groupUUID,
GroupName = groupName,
ItemName = "${t + 1}. This is an CHILD ITEM... #${i + 1} - ${Random.nextInt(1001, 5001)}",
children = ArrayList()
)
)
}
viewModelScope.launch(Dispatchers.Main){
itemList.add(TestModel(
isHeader = true,
UniqueKey = UUID.randomUUID(),
GroupId = groupUUID,
GroupName = groupName,
ItemName = "",
children = childList
))
}
}
}
fun addChildren(testModel: TestModel, onCompleted: (startIndex: Int) -> Unit){
if(testModel.children.count() > 0){
var index = itemList.indexOf(testModel)
testModel.children.forEach { tItem ->
itemList.add(index + 1, tItem)
index++
}
testModel.apply {
isExpanded = true
children.clear()
}
onCompleted(index)
}
}
fun removeChildren(testModel: TestModel, onCompleted: (startIndex: Int) -> Unit){
val startIndex = itemList.indexOf(testModel) + 1
while (startIndex < itemList.size && !itemList[startIndex].isHeader){
testModel.children.add(itemList.removeAt(startIndex))
}
if(testModel.children.count() > 0){
testModel.isExpanded = false
onCompleted(startIndex - 1)
}
}}
Composable functions
#Composable
fun MainScreenWithChildList(testViewModel: TestViewModel = viewModel()) {
val lazyColumnState = rememberLazyListState()
var reverseList by remember { mutableStateOf(false) }
if (reverseList) {
LaunchedEffect(Unit) {
delay(1000)
testViewModel.itemList.reverse()
}
}
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(
state = lazyColumnState,
modifier = Modifier.fillMaxSize()
) {
testViewModel.itemList.forEach { testModel ->
when (testModel.isHeader) {
true -> {
stickyHeader(key = testModel.UniqueKey) {
HeaderLayout(testModel = testModel) { testModel ->
if (testModel.isExpanded) {
testViewModel.removeChildren(testModel) {}
} else {
testViewModel.addChildren(testModel) {}
}
}
}
}
false -> {
item(key = testModel.UniqueKey) {
val dismissState = rememberDismissState()
if (dismissState.isDismissed(DismissDirection.EndToStart) || dismissState.isDismissed(
DismissDirection.StartToEnd
)
) {
if (dismissState.currentValue != DismissValue.Default) {
LaunchedEffect(Unit) {
dismissState.reset()
}
}
}
SwipeToDismiss(
state = dismissState,
directions = setOf(
DismissDirection.StartToEnd,
DismissDirection.EndToStart
),
dismissThresholds = { direction ->
FractionalThreshold(if (direction == DismissDirection.StartToEnd || direction == DismissDirection.EndToStart) 0.25f else 0.5f)
},
background = { SwipedItemBackground(dismissState = dismissState) },
dismissContent = {
ItemScreen(
modifier = Modifier
.fillMaxWidth()
.animateItemPlacement(animationSpec = tween(600)),
elevation = if (dismissState.dismissDirection != null) 16 else 0,
testModel = testModel
)
}
)
}
}
}
}
}
Button(
onClick = {
reverseList = true
},
modifier = Modifier.align(Alignment.BottomCenter)
) {
Text(text = "Reverse")
}
}
}
#Composable
fun ItemScreen(modifier: Modifier, elevation: Int, testModel: TestModel) {
Card(
modifier = modifier,
//elevation = animateDpAsState(elevation.dp).value
) {
Text(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
text = testModel.ItemName + " ="
)
}
}
#Composable
fun SwipedItemBackground(dismissState: DismissState) {
val direction = dismissState.dismissDirection ?: return
val color by animateColorAsState(
targetValue = when (dismissState.targetValue) {
DismissValue.Default -> Color.LightGray
DismissValue.DismissedToEnd -> Color.Green
DismissValue.DismissedToStart -> Color.Red
}
)
val icon = when (direction) {
DismissDirection.StartToEnd -> Icons.Default.Done
DismissDirection.EndToStart -> Icons.Default.Delete
}
val scale by animateFloatAsState(
if (dismissState.targetValue == DismissValue.Default) 0.75f else 1.5f
)
val alignment = when (direction) {
DismissDirection.StartToEnd -> Alignment.CenterStart
DismissDirection.EndToStart -> Alignment.CenterEnd
}
Box(
modifier = Modifier
.fillMaxSize()
.background(color)
.padding(start = 12.dp, end = 12.dp),
contentAlignment = alignment
) {
Icon(icon, contentDescription = "Icon", modifier = Modifier.scale(scale))
}
}
#Composable
fun HeaderLayout(testModel: TestModel, onExpandClicked: (testModel: TestModel) -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
.background(
color = Color.LightGray,
shape = RoundedCornerShape(8.dp)
)
.padding(horizontal = 32.dp, vertical = 16.dp),
horizontalArrangement = SpaceBetween
) {
Text(text = testModel.GroupName)
TextButton(onClick = { onExpandClicked(testModel) }) {
Text(text = if (testModel.isExpanded) "Collapse" else "Expand")
}
}
}
Above is the complete reproducible code. Please copy paste and try
The reverse seems to work fine when the list size is small, but crashes when the number of items is large. I was able to reproduce this with the following MRE:
val list = remember { (1..100).toList().toMutableStateList() }
LaunchedEffect(Unit) {
delay(1.seconds)
list.reverse()
}
Text(list.toList().toString())
And reported this to compose issue tracker, star it so it can be solved faster.
Until then, you can reverse it manually as follows:
val newItems = list.reversed()
list.clear()
list.addAll(newItems)
Related
I am trying to make a new app with Jetpack Compose and in this app will be a LazyRow with different items in a Box. With every item that is fully visible the background of the box should be changed from the color saved in the data model.
This is the code:
fun MainScreen(navHostController: NavHostController) {
var bgColor: Color = Color.Red
val state = rememberLazyListState()
val fullyVisibleIndices: List<Int> by remember {
derivedStateOf {
val layoutInfo = state.layoutInfo
val visibleItemsInfo = layoutInfo.visibleItemsInfo
if (visibleItemsInfo.isEmpty()) {
emptyList()
} else {
val fullyVisibleItemsInfo = visibleItemsInfo.toMutableList()
val lastItem = fullyVisibleItemsInfo.last()
val viewportHeight = layoutInfo.viewportEndOffset + layoutInfo.viewportStartOffset
if (lastItem.offset + lastItem.size > viewportHeight) {
fullyVisibleItemsInfo.removeLast()
}
val firstItemIfLeft = fullyVisibleItemsInfo.firstOrNull()
if (firstItemIfLeft != null && firstItemIfLeft.offset < layoutInfo.viewportStartOffset) {
fullyVisibleItemsInfo.removeFirst()
}
fullyVisibleItemsInfo.map { it.index }
}
}
}
addGrunges()
Box(
modifier = Modifier
.fillMaxSize()
.background(bgColor)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(top = 100.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = "Devlet seçin", style = MaterialTheme.typography.h3
)
LazyRow(
state = state,
modifier = Modifier
.fillMaxWidth()
.padding(top = 100.dp, bottom = 250.dp)
) {
itemsIndexed(grungesList) { index, items ->
bgColor = if (fullyVisibleIndices.contains(index)) items.color else Color.Red
ListColumn(model = items)
}
}
}
}
}
Thanks for your help!
Only fully visible items? Naive implementation
const val contentPadding = 10
#Composable
fun Greeting(models: List<Int> = remember { (0..20).toList() }) {
val state = rememberLazyListState()
val highlightState = remember { HighlightState() }
val hightlightIndices by remember {
derivedStateOf {
val layoutInfo = state.layoutInfo
val visibleItemsInfo = layoutInfo.visibleItemsInfo
val viewportWidth = layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset
if (visibleItemsInfo.isNotEmpty()) {
val indicies = ArrayList<Int>(visibleItemsInfo.size)
for (ix in visibleItemsInfo.indices) {
val item = visibleItemsInfo[ix]
when (ix) {
0 -> if (item.offset + contentPadding >= 0) indicies.add(item.index)
visibleItemsInfo.size - 1 -> {
if (item.offset + item.size + contentPadding < viewportWidth) indicies.add(item.index)
}
else -> indicies.add(item.index)
}
}
highlightState.update(indicies)
}
highlightState.indicies
}
}
LazyRow(
modifier = Modifier.fillMaxWidth(),
//.mouseScroll(state), // for desktop only
state = state,
contentPadding = PaddingValues(contentPadding.dp, 0.dp),
horizontalArrangement = Arrangement.spacedBy(30.dp)
) {
itemsIndexed(models) { index, model ->
MyListItem(model, hightlightIndices.contains(index))
}
}
}
#Composable
fun MyListItem(value: Int, highlight: Boolean) {
Box(
modifier = Modifier.height(100.dp)
.aspectRatio(1f)
.background(if (highlight) Color.Red else Color.Yellow),
contentAlignment = Alignment.Center
) {
Text(value.toString())
}
}
fun <T : Comparable<T>> Iterable<T>.compareTo(other: Iterable<T>): Int {
val otherI = other.iterator()
for (e in this) {
if (!otherI.hasNext()) return 1 // other has run out of elements, so `this` is larger
val c = e.compareTo(otherI.next())
if (c != 0) return c // found a position with a difference
}
if (otherI.hasNext()) return -1 // `this` has run out of elements, but other has some more, so other is larger
return 0 // they're the same
}
class HighlightState {
var indicies: List<Int> = emptyList()
private set
fun update(newIndicies: List<Int>) {
if (indicies.compareTo(newIndicies) != 0)
indicies = newIndicies
}
}
result
I'm using room database with jetpack compose,upon deleting all items the ui is not recomposing unless i move to another screen and come back again , any help would be appreciated , Thank you
This is my code
fun CheckOutScreen(shopViewModel: ShopViewModel,
navHostController: NavHostController,
list : SnapshotStateList<PurchaseModel> ) {
val firebaseAuth = FirebaseAuth.getInstance()
val databaseReference = FirebaseDatabase.getInstance().reference
val context = LocalContext.current
val totalAmount = remember { mutableStateOf(0.0) }
val count = remember { mutableStateOf(0) }
val isDialogShowing = remember { mutableStateOf(false) }
val isProgressShowing = remember { mutableStateOf(false) }
val isDataSaving = remember { mutableStateOf(false) }
val isMovingToAnotherScreen = remember { mutableStateOf(false)}
list.forEach {
if(count.value < list.size){
totalAmount.value += it.totalPrice
count.value++
}
}
Scaffold{
Column {
Row(modifier = Modifier.fillMaxWidth()) {
Text(text = "Cart", modifier = Modifier
.weight(1f)
.padding(10.dp), color = Color.DarkGray,
style = MaterialTheme.typography.h1, fontSize = 17.sp)
IconButton(onClick = {
isDialogShowing.value = true
}) {
Icon(Icons.Filled.Delete,"")
}
}
if(list.size == 0){
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
Text("No Items In The Cart", style = MaterialTheme.typography.h1,
color = Color.DarkGray , fontSize = 17.sp)
}
}
else {
Box(contentAlignment = Alignment.Center){
Column() {
Text(text = "Purchase To : " + list[0].fullName, modifier = Modifier.padding(start = 10.dp,
end = 10.dp, top = 5.dp), fontSize = 17.sp,style = MaterialTheme.typography.body1)
LazyColumn(modifier = Modifier
.weight(1f)
.padding(top = 10.dp)){
items(list.size, key = { item -> item.hashCode()}){ pos ->
val dismissState = rememberDismissState(confirmStateChange = {
if(it == DismissValue.DismissedToStart || it == DismissValue.DismissedToEnd){
shopViewModel.deletePurchase(list[pos].purchaseId!!)
Toast.makeText(context,"Item Successfully Dismissed",Toast.LENGTH_SHORT).show()
}
true
})
if(!isMovingToAnotherScreen.value){
SwipeToDismiss(
state = dismissState,
background = {},
dismissContent = {
Card(elevation = 10.dp, modifier = Modifier
.clip(shape = RoundedCornerShape(6.dp))
.padding(5.dp)) {
Row(modifier = Modifier.fillMaxWidth()) {
Box(contentAlignment = Alignment.Center) {
AsyncImage(model = ImageRequest.Builder(context).data(list[pos].productImage).build(),
contentScale = ContentScale.Crop,
modifier = Modifier
.width(80.dp)
.height(80.dp)
.padding(10.dp),
contentDescription = "")
}
Column(modifier = Modifier.weight(1f)) {
Text(list[pos].title, modifier = Modifier.padding(5.dp), fontWeight = FontWeight.Bold)
Text("Quantity : " + list[pos].totalQuantity)
Text("Due To : " + list[pos].totalPrice)
}
}
}
},
directions = setOf(DismissDirection.StartToEnd,DismissDirection.EndToStart))
}
}
}
Card(modifier = Modifier
.fillMaxWidth()
.clip(shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp))) {
Button(modifier = Modifier.padding(start = 20.dp, end = 20.dp),onClick = {
isProgressShowing.value = true
}) {
Text("Pay ${totalAmount.value}")
}
}
}
}
AnimatedVisibility(visible = isProgressShowing.value) {
CircularProgressIndicator(color = Color.DarkGray)
}
}
}
if(isProgressShowing.value){
val map = hashMapOf<String,Any>()
map["list"] = list
map["uid"] = firebaseAuth.currentUser!!.uid
map["totalPrice"] = totalAmount.value.toString()
val db = databaseReference.child("Purchases")
db.child(firebaseAuth.currentUser!!.uid)
.child(Calendar.getInstance().timeInMillis.toString())
.setValue(map)
.addOnSuccessListener {
isDataSaving.value = true
}
.addOnFailureListener {
}
}
if(isDialogShowing.value){
AlertDialog(
onDismissRequest = { isDialogShowing.value = false },
confirmButton = {
TextButton(onClick = {
shopViewModel.deletePurchases()
isDialogShowing.value = false
Toast.makeText(context,"All items are successfully removed",Toast.LENGTH_SHORT).show()
}) {
Text("Proceed")
}
},
dismissButton = {
TextButton(onClick = { isDialogShowing.value = false }) {
Text("Cancel")
}
},
title = { Text("Removing Items ") },
text = { Text("Do you want to remove items from cart ! ") }
)
}
if(isDataSaving.value){
LaunchedEffect(Unit){
delay(3000)
isMovingToAnotherScreen.value = true
shopViewModel.deletePurchases()
isProgressShowing.value = false
isDataSaving.value = false
navHostController.navigate(AppRouting.Payment.route)
}
}
}
BackHandler {
isMovingToAnotherScreen.value = true
navHostController.popBackStack()
}
}
It's most likely because of not updating value of your State. In you example it's always the same list. You either need to set value with new instance of list or use SnapshotStateList. You can check this answer also.
I have a list of products. On the ProductsScreen I have some filter options, search box, also filter chips :
I'm using paging 3 library for the pagination. I want to scroll to the top when the search query or selected filter chip has changed. Here is my code :
ProductScreen
#Composable
fun ProductsScreen(
modifier: Modifier = Modifier,
navController: NavController,
viewModel: ProductsScreenViewModel = hiltViewModel(),
onProductItemClick: (String) -> Unit = {},
onProductFilterButtonClick: (ProductsFilterPreferences) -> Unit = {},
) {
//products
val products = viewModel.products.collectAsLazyPagingItems()
//load state
val isLoading = products.loadState.refresh is LoadState.Loading
val scrollToTop = viewModel.scrollToTop.observeAsState()
val scaffoldState = rememberScaffoldState()
val listState = rememberLazyListState()
val scope = rememberCoroutineScope()
val query by viewModel.query.collectAsState()
val context = LocalContext.current
//not working
LaunchedEffect(key1 = Unit) {
scrollToTop.value?.getContentIfNotHandled()?.let { scroll ->
if (scroll) {
listState.scrollToItem(0)
}
}
}
RubiBrandsScaffold(
modifier = modifier,
scaffoldState = scaffoldState,
topBar = {
ProductsScreenToolbar(
showFilterBadge = viewModel.showFilterBadge,
onFilterButtonClick = { onProductFilterButtonClick(viewModel.productsFilterPreferences.value) },
searchQuery = query,
onTrailingIconClick = {
viewModel.setQuery("")
},
onQueryChange = { query ->
viewModel.setQuery(query)
},
onSearchButtonClick = { query ->
viewModel.onSearch(query)
},
onChipClick = { chip ->
viewModel.onChipSelected(chip)
},
selectedChip = viewModel.selectedChip,
)
},
floatingActionButton = {
AnimatedVisibility(
visible = showFab,
enter = scaleIn(),
exit = scaleOut()
) {
FloatingActionButton(
backgroundColor = MaterialTheme.colors.primary,
onClick = {
scope.launch {
//working
listState.scrollToItem(0)
}
},
modifier = Modifier
.padding(
dimensionResource(id = R.dimen.dimen_16)
)
) {
Icon(
painter = painterResource(id = R.drawable.ic_up),
contentDescription = null,
tint = MaterialTheme.colors.onPrimary
)
}
}
}
) { paddingValues ->
SwipeRefresh(
indicator = { state, trigger ->
RubiBrandsSwipeRefreshIndicator(state = state, trigger = trigger)
}, state = swipeRefreshState, onRefresh = products::refresh
) {
LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize()
) {
items(products) { product ->
product?.let {
ProductItem(
modifier = Modifier.clickable {
if (!isLoading) onProductItemClick(
product.id
)
},
childModifier = Modifier.shimmerModifier(isLoading),
productImage = product.productImage?.get(0),
productTitle = product.productTitle,
productCount = product.productCount,
productStatus = product.productStatus?.asString(context)
)
}
}
products.apply {
when {
loadState.source.refresh is LoadState.Loading -> {
items(10) {
ProductItem(
childModifier = Modifier.shimmerModifier(true),
productImage = null,
productTitle = "",
productCount = "",
productStatus = ""
)
}
}
loadState.source.append is LoadState.Loading -> {
item {
CircularProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.wrapContentWidth(Alignment.CenterHorizontally)
)
}
}
loadState.source.refresh is LoadState.Error -> {
val e = products.loadState.refresh as LoadState.Error
item {
ErrorItem(
modifier = Modifier.fillParentMaxSize(),
message = e.error.localizedMessage
?: stringResource(id = R.string.something_went_wrong),
onRetryClick = products::retry
)
}
}
loadState.source.append is LoadState.Error -> {
val e = products.loadState.append as LoadState.Error
item {
ErrorItem(
modifier = Modifier
.fillParentMaxWidth()
.wrapContentHeight(),
message = e.error.localizedMessage
?: stringResource(id = R.string.something_went_wrong),
onRetryClick = products::retry
)
}
}
loadState.source.refresh is LoadState.NotLoading && loadState.source.refresh !is LoadState.Error && products.itemCount < 1 -> {
item {
RubiBrandsEmptyListView(modifier = Modifier.fillParentMaxSize())
}
}
}
}
}
}
}
}
Here is also my ProductsScreenViewModel :
#HiltViewModel
class ProductsScreenViewModel #Inject constructor(
private val productsScreenUseCase: ProductsScreenUseCase
) : ViewModel() {
var showFilterBadge by mutableStateOf(false)
private set
private val _query = MutableStateFlow<String>("")
val query: StateFlow<String> = _query
var selectedChip by mutableStateOf<ProductsChips>(ProductsChips.ALL)
private val _productsFilterPreferences = MutableStateFlow(ProductsFilterPreferences())
val productsFilterPreferences: StateFlow<ProductsFilterPreferences> = _productsFilterPreferences
private val _scrollToTop = MutableLiveData<Event<Boolean>>()
val scrollToTop: LiveData<Event<Boolean>> get() = _scrollToTop
val products =
_productsFilterPreferences.flatMapLatest { filterPreferences ->
showFilterBadge = filterPreferences.sort.value != null
|| filterPreferences.saleStatus.value != null
|| filterPreferences.stockStatus.value != null
|| filterPreferences.sku != null
|| filterPreferences.priceOptions.value != R.string.all
productsScreenUseCase.fetchProducts(params = filterPreferences)
}.cachedIn(viewModelScope)
fun setFilters(
filters: ProductsFilterPreferences
) {
_scrollToTop.value = Event(true)
_productsFilterPreferences.update {
it.copy(
sort = filters.sort,
state = filters.state,
saleStatus = filters.saleStatus,
stockStatus = filters.stockStatus,
sku = filters.sku,
priceOptions = filters.priceOptions
)
}
}
fun setQuery(query: String) {
if (query.isEmpty()) {
onSearch(query)
}
_query.value = query
}
fun onSearch(query: String) {
//if I press the search button I'm setting scroll to top to true
_scrollToTop.value = Event(true)
_productsFilterPreferences.update {
it.copy(query = query.trim().ifEmpty { null })
}
}
fun onChipSelected(chip: ProductsChips) {
selectedChip = chip
//if I change the chip I'm setting scroll to top to true
_scrollToTop.value = Event(true)
_productsFilterPreferences.update {
it.copy(state = chip.value)
}
}
}
There is a related question about that but it is valid for XML.
So whenever the search query or selected chip has changed I'm setting the scrollToTop value to true and then observing it from my ProductsScreen composable. But it is not working.
You can try using the LaunchedEffect and the scrollState of Lazycolumn to scroll it to top.
val scrollState = rememberLazyListState()
LazyColumn( state = scrollState){}
LaunchedEffect(true) {
scrollState.animateScrollToItem(0)
}
I've just put the LaunchEffect within the if block and the issue have been resolved. Here is the code:
scrollToTop?.getContentIfNotHandled()?.let { scroll ->
if (scroll) {
LaunchedEffect(key1 = Unit) {
listState.scrollToItem(0)
}
}
}
My problem is, that i can't manage to navigate to another Screen inside a foreach-loop altough it's possible in another Screen to route to the wanted Screen, but not in a foreach loop.
When i click on the button, it says, that he can't find the article with the id, but i checked and the id, which he says he can't find does exist
This is my Code (which produces the error):
Surface(
modifier = Modifier
.fillMaxSize()
)
{
Box()
{
shopping.forEach {
Button(
modifier = Modifier
.offset(it.coordinates.x.dp, it.coordinates.y.dp)
.size(10.dp),
onClick = { navController.navigate(
Screen.ArtikelDetailScreen.route
+ "/${it.id}" })
)
{
Text(
modifier = Modifier
.padding(
top = ProjectTheme.paddings.tiny,
bottom = ProjectTheme.paddings.tiny
),
style = ProjectTheme.typography.h6,
text = it.name
)
}
}
}
}
This is the code to which it should route:
#Composable
fun ArtikelDetailScreen(
id: Long,
navController: NavController,
viewModel: ArtikelViewModel = hiltViewModel()
) {
val artikel = viewModel.shoppingState.value.find { p -> p.id == id }
if(artikel == null) {
Log.e(tag, "Artikel mit der id = $id wurde nicht gefunden")
return
}
viewModel.artikel = artikel
viewModel.setState()
val scrollState = rememberScrollState()
ProjectTheme {
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
text = stringResource(R.string.ArtDetailScreenName))
}
)
}
) {
Column(
modifier = Modifier
.wrapContentWidth()
.padding(all = ProjectTheme.paddings.small)
.verticalScroll(
state = scrollState,
enabled = true,
reverseScrolling = true
)
) {
ArtikelInfos(viewModel)
Button(
modifier = Modifier
.fillMaxWidth()
.padding(top = ProjectTheme.paddings.default),
onClick = {
viewModel.remove(id)
viewModel.add()
navController.navigate(
route = Screen.ShoppingListScreen.route)
{
popUpTo(
route = Screen.ShoppingListScreen.route
)
{
inclusive = true
}
}
}
) {
Text(
modifier = Modifier
.padding(
top = ProjectTheme.paddings.tiny,
bottom = ProjectTheme.paddings.tiny
),
style = ProjectTheme.typography.h6,
text = stringResource(R.string.back)
)
}
}
}
}
}
And this is where it works:
LazyColumn {
items(
viewModel.shoppingState.value
)
{
artikel ->
var artikelRemoved: Artikel?
val dismissState = rememberDismissState(
confirmStateChange =
{
if (it == DismissValue.DismissedToEnd) {
logThread("==>SwipeToDismiss().", "-> Edit")
navController.navigate(
Screen.ArtikelDetailScreen.route
+ "/${artikel.id}"
)
return#rememberDismissState true
}
else if (it == DismissValue.DismissedToStart)
{
logThread("==>SwipeToDismiss().", "-> Delete")
artikelRemoved = artikel
viewModel.remove(artikel.id)
navController.navigate(
Screen.ShoppingListScreen.route
)
return#rememberDismissState true
}
return#rememberDismissState false
}
)
SwipeToDismiss(
state = dismissState,
modifier = Modifier.padding(vertical = 4.dp),
directions = setOf(
DismissDirection.StartToEnd,
DismissDirection.EndToStart
),
dismissThresholds =
{
direction ->
FractionalThreshold(
if (direction == DismissDirection.StartToEnd)
{
0.25f
}
else
{
0.25f
}
)
},
background =
{
val direction =
dismissState.dismissDirection ?: return#SwipeToDismiss
val colorBox by animateColorAsState(
when (dismissState.targetValue)
{
DismissValue.Default -> ProjectTheme.colors.secondary
DismissValue.DismissedToEnd -> Color.Green
DismissValue.DismissedToStart -> Color.Red
}
)
val colorIcon by animateColorAsState(
when (dismissState.targetValue)
{
DismissValue.Default -> Color.Black
DismissValue.DismissedToEnd -> Color.DarkGray
DismissValue.DismissedToStart -> Color.White
}
)
val alignment = when (direction)
{
DismissDirection.StartToEnd -> Alignment.CenterStart
DismissDirection.EndToStart -> Alignment.CenterEnd
}
val icon = when (direction)
{
DismissDirection.StartToEnd -> Icons.Default.Edit
DismissDirection.EndToStart -> Icons.Default.Delete
}
val scale by animateFloatAsState(
if (dismissState.targetValue == DismissValue.Default)
{
1.25f
}
else
{
2.0f
}
)
Box(
Modifier
.fillMaxSize()
.background(colorBox)
.padding(horizontal = 20.dp),
contentAlignment = alignment
) {
Icon(
icon,
contentDescription = "Localized description",
modifier = Modifier
.scale(scale),
tint = colorIcon
)
}
},
dismissContent = {
Column {
ArtikelCard(
artikel = artikel,
elevation = animateDpAsState(
if (dismissState.dismissDirection != null)
{
8.dp
}
else
{
0.dp
}
).value,
) {
// onClick
// THIS WORKS navController.navigate(
Screen.ArtikelDetailScreen.route
+ "/${artikel.id}"
)
}
Divider()
}
}
)
}
}
I want to scroll the lazyColumn when its child get focused.
I use rememberLazyListState's scrollToItem method to scroll the lazyColumn.
but when a item at top been scrolled out, it can't be focused again.
My sample code:
#Composable
fun ScrollableList(modifier: Modifier = Modifier) {
val scrollState = rememberLazyListState()
val scope = rememberCoroutineScope()
LazyColumn(
state = scrollState,
modifier = Modifier.padding(5.dp),
verticalArrangement = Arrangement.spacedBy(5.dp)
) {
itemsIndexed(items = (0..10).toList()) { index, item ->
FocusableBox(
title = "ScrollBox-$index",
onFocused = { focused ->
scope.launch {
if (focused) {
scrollState.scrollToItem(index)
}
}
})
}
}
}
#Composable
fun FocusableBox(
title: String,
onFocused: (Boolean) -> Unit = {},
requester: FocusRequester = FocusRequester(),
modifier: Modifier = Modifier
) {
var boxColor by remember { mutableStateOf(Color.White) }
var focused by remember { mutableStateOf(false) }
Box(
Modifier
.focusRequester(requester)
.onFocusChanged {
boxColor = if (it.isFocused) Color.Green else Color.Gray
focused = it.isFocused
onFocused(it.isFocused)
}
.focusable()
.background(boxColor)
.zIndex(if (focused) 1f else 0f)
) {
Text(
text = title,
modifier = Modifier.padding(30.dp),
color = Color.White,
style = MaterialTheme.typography.subtitle2
)
}
}
add
val scrollState = rememberLazyListState()
then after child been focused, use scrollState.layoutInfo.visibleItemsInfo to determine scroll up or scroll down.
#Composable
fun ScrollableList(modifier: Modifier = Modifier) {
val scrollState = rememberLazyListState()
val scope = rememberCoroutineScope()
LazyColumn(
state = scrollState,
modifier = Modifier.padding(5.dp),
verticalArrangement = Arrangement.spacedBy(5.dp)
) {
itemsIndexed(items = (0..10).toList()) { index, item ->
FocusableBox(
title = "ScrollBox-$index",
onFocused = { focused ->
scope.launch {
if (focused) {
val visibleItemsInfo = scrollState.layoutInfo.visibleItemsInfo
val visibleSet = visibleItemsInfo.map { it.index }.toSet()
if (index == visibleItemsInfo.last().index) {
scrollState.scrollToItem(index)
} else if (visibleSet.contains(index) && index != 0) {
scrollState.scrollToItem(index - 1)
}
}
}
})
}
}
}