Why is LazyColumn not recomposing? - android

Link to the video of the problem: Video Link`
Sometimes many notes delete at once and the add functionality is not working. When I swipe the Lazy Column does not recompose and put everything in order again.
This is my code:
NoteButton:
#Composable
fun NoteButton(
modifier: Modifier = Modifier,
text: String,
enabled: Boolean = true,
shape: Shape = CircleShape,
onClick: () -> Unit
) {
Button(
modifier = modifier,
enabled = enabled,
shape = shape,
onClick = onClick
) {
Text(text)
}
}
NoteInputText:
#OptIn(ExperimentalComposeUiApi::class)
#Composable
fun NoteInputText(
modifier: Modifier = Modifier,
text: String,
label: String,
maxLines: Int = 1,
onTextChange: (String) -> Unit,
onImeAction: () -> Unit = {},
keyboardOptions: KeyboardOptions = KeyboardOptions.Default.copy(
imeAction = if (maxLines == 1) ImeAction.Done else ImeAction.None
)
) {
val keyboardController = LocalSoftwareKeyboardController.current
TextField(
modifier = modifier,
value = text,
onValueChange = onTextChange,
maxLines = maxLines,
label = { Text(text = label) },
keyboardOptions = keyboardOptions,
keyboardActions = KeyboardActions(
onDone = {
onImeAction()
keyboardController?.hide()
}
)
)
}
NoteItem:
private const val TAG = "NoteItem"
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun NoteItem(modifier: Modifier = Modifier, note: Note, onSwipe: (Note) -> Unit) {
val dismissState = rememberDismissState(initialValue = DismissValue.Default) {
if (it == DismissValue.DismissedToStart) {
Log.d(TAG, "NoteItem: ${note.id}")
onSwipe(note)
}
true
}
SwipeToDismiss(
state = dismissState,
directions = setOf(DismissDirection.EndToStart),
background = {}
) {
Card(
modifier = modifier
.padding(5.dp)
.fillMaxWidth(),
shape = RoundedCornerShape(topEnd = 20.dp, bottomEnd = 20.dp),
elevation = 5.dp
) {
Column(modifier = Modifier.padding(horizontal = 15.dp, vertical = 5.dp)) {
Text(text = note.title, style = MaterialTheme.typography.h5)
Text(text = note.description, style = MaterialTheme.typography.body1)
}
}
}
}
NoteList:
private const val TAG = "NoteList"
#Composable
fun NoteList(modifier: Modifier = Modifier, onSwipe: (Note) -> Unit) {
val noteViewModel: NoteViewModel = viewModel()
val notes = noteViewModel.getAllNotes()
if(notes.isNotEmpty()) {
LazyColumn(modifier = modifier.padding(5.dp)) {
items(notes) { note ->
NoteItem(note = note, onSwipe = { onSwipe(note) })
}
}
}
}
Note:
data class Note(
val id: Long = assignIdForNote(),
val title: String,
val description: String,
val entryDate: LocalDateTime = LocalDateTime.now()
)
fun getNoteData() = mutableListOf(
Note(title = "A good day", description = "We went on a vacation by the lake"),
Note(title = "Android Compose", description = "Working on Android Compose course today"),
Note(title = "Keep at it...", description = "Sometimes things just happen"),
Note(title = "A movie day", description = "Watching a movie with family today"),
Note(title = "A movie day", description = "Watching a movie with family today"),
Note(title = "A movie day", description = "Watching a movie with family today"),
Note(title = "A movie day", description = "Watching a movie with family today"),
Note(title = "A movie day", description = "Watching a movie with family today"),
Note(title = "A movie day", description = "Watching a movie with family today"),
Note(title = "A movie day", description = "Watching a movie with family")
)
NoteViewModel:
class NoteViewModel: ViewModel() {
private var notesList = mutableStateListOf<Note>()
init {
notesList.addAll(getNoteData())
}
fun addNote(note: Note) {
notesList.add(note)
}
fun deleteNote(note: Note) {
notesList.removeIf { it.id == note.id }
}
fun getAllNotes() = notesList
}
Navigation:
#Composable
fun Navigation() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = Screen.NotesScreen.name) {
composable(Screen.NotesScreen.name) {
NotesScreen(navController)
}
composable(Screen.AddScreen.name) {
AddScreen(navController)
}
}
}
AddScreen:
private const val TAG = "AddScreen"
#Composable
fun AddScreen(navController: NavController) {
val noteViewModel: NoteViewModel = viewModel()
var title by remember {
mutableStateOf("")
}
var description by remember {
mutableStateOf("")
}
Log.d(TAG, "AddScreen: Title: $title")
Log.d(TAG, "AddScreen: Description: $description")
Column {
TopAppBar(title = { Text(text = stringResource(id = R.string.app_name)) },
navigationIcon = {
IconButton(onClick = { navController.popBackStack() }) {
Icon(imageVector = Icons.Filled.ArrowBack, contentDescription = "Go back")
}
})
Column(
modifier = Modifier
.padding(10.dp)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
NoteInputText(
modifier = Modifier.background(Color.White),
text = title,
label = "Title",
onTextChange = {
title = it
}
)
NoteInputText(
modifier = Modifier.background(Color.White),
text = description,
label = "Description",
onTextChange = {
description = it
}
)
NoteButton(text = "Add") {
if(title.isNotBlank() && description.isNotBlank()) {
Log.d(TAG, "AddScreen: Cool, $title, $description")
noteViewModel.addNote(Note(title = title, description = description))
navController.popBackStack()
}
}
}
}
}
NotesScreen:
private const val TAG = "NotesScreen"
#Composable
fun NotesScreen(navController: NavController) {
val noteViewModel: NoteViewModel = viewModel()
val notes = noteViewModel.getAllNotes()
Column {
TopAppBar(
title = { Text(text = stringResource(id = R.string.app_name)) },
actions = {
IconButton(onClick = { navController.navigate(Screen.AddScreen.name) }) {
Icon(imageVector = Icons.Filled.Add, contentDescription = "Add")
}
}
)
if (notes.isNotEmpty()) {
NoteList(onSwipe = {
noteViewModel.deleteNote(it)
})
} else {
Text(
modifier = Modifier.padding(10.dp),
text = "Click on the + icon to add a note",
style = MaterialTheme.typography.h5
)
}
}
}
Screen:
enum class Screen {
NotesScreen, AddScreen
}
Util:
private var previousId = 0L
fun assignIdForNote() = previousId++
MainActivity:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
NoteAppTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
Navigation()
}
}
}
}
}
#Preview(showBackground = true)
#Composable
fun DefaultPreview() {
NoteAppTheme {
Navigation()
}
}

The reason your add functionality is not working is you were operating on a independent NoteViewModel on AddScreen, which is different with NotesScreen's NoteViewModel. You can check both noteViewModel's hashCode to verify.

Related

Jetpack compose firebase auth, why is ui updating only after reloging?

In my application I'm using FirebaseAuth as the logging system. In HomeScreen I'm displaying currently reading books based on User's firestore database. The user can update this list by adding the book to firestore database in DetailsScreen. The problem is that even though the database is being updated, it is being shown on the UI only if the user sign out and then log in again, it is not being shown while the user is logged in. How can i fix this? I'm providing necessarry code down below. I know, that's a lot but I simply can't find the issue.
HomeScreen
#Composable
fun HomeScreen(
navController: NavController,
viewModel: HomeViewModel = hiltViewModel(),
commonViewModel: CommonViewModel
) {
val currentUser = FirebaseAuth.getInstance().currentUser
val context = LocalContext.current
val userBooks = viewModel.booksFromFB.value.filter {
it.userId == currentUser?.uid.toString()
}
Log.d("Test", "${userBooks}")
if (viewModel.isLoading.value) {
CircularProgressIndicator()
} else {
Box(
modifier = Modifier
.fillMaxSize()
.background(gradient(colors = listOf(AppColors.mBackground, AppColors.mBackgroundSec)))
) {
Log.d("Test", currentUser?.uid.toString())
Column(
modifier = Modifier.padding(start = 8.dp, end = 8.dp, top = 16.dp, bottom = 16.dp)
) {
Header(nick = currentUser?.email?.split('#')?.first().toString()) {
navController.navigate(Screen.Profile.route)
}
Spacer(modifier = Modifier.height(20.dp))
CurrentlyReadingSection(
context = context,
navController = navController,
userBooks = userBooks,
commonViewModel = commonViewModel
)
CurrentlyReadingSection
#Composable
fun CurrentlyReadingSection(
userBooks: List<BookFB>,
context: Context = LocalContext.current,
commonViewModel: CommonViewModel,
navController: NavController
) {
Column(
modifier = Modifier.padding()
) {
Text(
modifier = Modifier.fillMaxWidth(),
text = "Currently reading",
color = AppColors.mTextWhite,
fontStyle = FontStyle.Italic,
fontWeight = FontWeight.Bold,
fontSize = 30.sp,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(10.dp))
LazyRow() {
items(userBooks) { book ->
BookRow(
modifier = Modifier.padding(4.dp),
onItemClicked = { }
) {
val isExpanded = remember {
mutableStateOf(false)
}
val isRead = rememberSaveable {
mutableStateOf(false)
}
Box(
modifier = Modifier
.fillMaxSize()
.background(AppColors.mBackgroundSec),
contentAlignment = BottomCenter
) {
Row {
AnimatedVisibility(visible = isExpanded.value) {
Box(
modifier = Modifier.fillMaxSize()
.clickable { isExpanded.value = !isExpanded.value },
contentAlignment = Center
) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
MyButton(
modifier = Modifier
.fillMaxWidth(0.7f)
.fillMaxHeight(0.2f)
.clip(RoundedCornerShape(12.dp)),
text = "READ",
fontSize = 12,
contentPadding = 8
) {
isRead.value = !isRead.value
isExpanded.value = !isExpanded.value
}
Spacer(modifier = Modifier.height(4.dp))
MyButton(
modifier = Modifier
.fillMaxWidth(0.7f)
.fillMaxHeight(0.24f)
.clip(RoundedCornerShape(12.dp)),
text = "RATE",
fontSize = 12,
contentPadding = 8
) {
commonViewModel.currentBook.value = book
navController.navigate(Screen.Rate.route)
}
}
}
}
AsyncImage(
modifier = Modifier
.fillMaxSize()
.clip(RoundedCornerShape(12.dp))
.clickable { isExpanded.value = !isExpanded.value },
model = ImageRequest.Builder(context)
.data(
if (isValid(book.image))
book.image
else
com.example.read.R.drawable.imagenotfound
)
.crossfade(true)
.build(),
contentScale = ContentScale.FillBounds,
contentDescription = "Book image"
)
}
Box(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(0.1f)
.background(if (isRead.value) AppColors.mGreen else AppColors.mRed)
)
}
}
}
}
}
}
BookRow - for CurrentlyReadingSection
#Composable
fun BookRow(
modifier: Modifier = Modifier,
book: BookFB = BookFB(),
category: MyCategory = MyCategory("", ""),
shapeDp: Int = 12,
heightSize: Int = 170,
widthSize: Int = 120,
isForYou: Boolean = false,
isYourCollection: Boolean = false,
onItemClicked: () -> Unit = {},
content: #Composable() () -> Unit,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = if (isYourCollection) Modifier.widthIn(max = widthSize.dp) else Modifier.fillMaxWidth()
) {
Surface(
shape = RoundedCornerShape(shapeDp.dp),
elevation = 4.dp,
modifier = modifier.border(2.dp, color = Color.Black, shape = RoundedCornerShape(shapeDp.dp))
) {
Box(
modifier = Modifier
.clickable {
onItemClicked()
}
.height(heightSize.dp)
.width(widthSize.dp)
.background(AppColors.mBackgroundSec))
{
content()
}
Adding and getting books from firestore database in repository
class FirebaseRepositoryImpl #Inject constructor(
private val queryBook: CollectionReference
): FirebaseRepository {
override suspend fun addToFirebase(book: BookFB) {
if (book.toString().isNotEmpty()) {
queryBook.add(book)
.addOnSuccessListener { documentRef ->
val documentId = documentRef.id
queryBook.document(documentId)
.update(hashMapOf("id" to documentId) as Map<String, Any>)
.addOnFailureListener {
Log.w("Error", "AddToFirebase: Failed updating doc", it)
}
}
}
}
override suspend fun getBooksFromFB(): Resource<List<BookFB>> {
return try {
Resource.Loading(true)
val response = queryBook.get().await().documents.map { documentSnapshot ->
documentSnapshot.toObject(BookFB::class.java)!!
}
if (response.isNotEmpty())
Resource.Loading(false)
Resource.Success(response)
} catch (exception: FirebaseFirestoreException) {
Resource.Error(exception.message.toString())
}
}
}
Getting the books in ViewModel
#HiltViewModel
class HomeViewModel #Inject constructor(
private val repository: FirebaseRepository
): ViewModel() {
private val _booksFromFB = mutableStateOf(listOf<BookFB>())
val booksFromFB = _booksFromFB
private val _isLoading = mutableStateOf(true)
val isLoading = _isLoading
init {
getBooksFromFB()
}
fun getBooksFromFB() {
viewModelScope.launch {
_isLoading.value = true
_booksFromFB.value = repository.getBooksFromFB().data!!
if (_booksFromFB.value.isNotEmpty())
_isLoading.value = false
}
}
}

Avoid recompositions of other elements when changing TextField value

I have basic Login screen in my app, which looks like this:
LoginScreen.kt:
#Composable
fun LoginScreen(navController: NavController, viewModel: LoginViewModel) {
val userEmail = viewModel.userEmail.collectAsState()
val userPassword = viewModel.userPassword.collectAsState()
val isLoginPending = viewModel.isLoginPending.collectAsState()
val isLoginButtonEnabled = viewModel.isLoginButtonEnabled.collectAsState()
val scaffoldState = rememberScaffoldState()
if (viewModel.getOnboardingStatus() == OnboardingStatus.NOT_COMPLETED) {
navController.navigate(Screen.Onboarding.route)
}
LaunchedEffect(key1 = true) {
viewModel.loginError.collectLatest {
it?.let {
scaffoldState.snackbarHostState.showSnackbar(message = it)
}
}
}
Scaffold(
modifier = Modifier
.fillMaxSize()
.recomposeHighlighter(),
scaffoldState = scaffoldState
) {
Box(modifier = Modifier
.fillMaxSize()
.recomposeHighlighter()) {
if (isLoginPending.value) {
ProgressBar(
modifier = Modifier
.align(Alignment.Center)
.recomposeHighlighter(),
)
}
Column(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.Center)
.recomposeHighlighter(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "Login",
fontWeight = FontWeight.Bold,
fontSize = 36.sp
)
Spacer(modifier = Modifier
.height(64.dp)
.recomposeHighlighter())
UserInputTextField(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp)
.recomposeHighlighter(),
label = "Email",
inputState = userEmail.value,
onValueChange = { viewModel.onEvent(LoginEvent.EnteredEmail(it)) },
isLoginPending = isLoginPending.value
)
Spacer(modifier = Modifier
.height(8.dp)
.recomposeHighlighter())
UserInputTextField(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp)
.recomposeHighlighter(),
label = "Password",
inputState = userPassword.value,
onValueChange = { viewModel.onEvent(LoginEvent.EnteredPassword(it)) },
isLoginPending = isLoginPending.value,
keyboard = KeyboardOptions(keyboardType = KeyboardType.Password),
visualTransformation = PasswordVisualTransformation()
)
Spacer(modifier = Modifier
.height(32.dp)
.recomposeHighlighter())
Button(
modifier = Modifier
.fillMaxWidth()
.padding(
start = 16.dp,
end = 16.dp
)
.recomposeHighlighter(),
enabled = isLoginButtonEnabled.value,
onClick = {
viewModel.onEvent(LoginEvent.Login)
}
) {
Text(text = "Login")
}
}
ClickableButton(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 32.dp)
.recomposeHighlighter(),
text = "No account? Click to register",
onTextClicked = { navController.navigate(Screen.Register.route) }
)
}
}
}
#Composable
private fun UserInputTextField(
modifier: Modifier,
label: String,
inputState: String,
onValueChange: (String) -> Unit,
isLoginPending: Boolean,
keyboard: KeyboardOptions? = null,
visualTransformation: VisualTransformation? = null
) {
OutlinedTextField(
value = inputState,
onValueChange = { onValueChange(it) },
label = { Text(text = label) },
placeholder = { Text(text = label) },
singleLine = true,
enabled = !isLoginPending,
modifier = modifier,
keyboardOptions = keyboard?.let { keyboard } ?: KeyboardOptions.Default,
visualTransformation = visualTransformation?.let { visualTransformation } ?: VisualTransformation.None
)
}
And I am using it with view model to decide if login button should be enabled, to login user etc.
LoginViewModel:
#HiltViewModel
class LoginViewModel #Inject constructor(private val repository: LoginRepository): ViewModel() {
private val _userEmail = MutableStateFlow<String>(value = String())
val userEmail: StateFlow<String> = _userEmail.asStateFlow()
private val _userPassword = MutableStateFlow<String>(value = String())
val userPassword: StateFlow<String> = _userPassword.asStateFlow()
private val _isLoginPending = MutableStateFlow<Boolean>(value = false)
val isLoginPending: StateFlow<Boolean> = _isLoginPending.asStateFlow()
private val _loginError = MutableSharedFlow<String?>()
val loginError: SharedFlow<String?> = _loginError.asSharedFlow()
private val _isLoginButtonEnabled = MutableStateFlow<Boolean>(value = false)
val isLoginButtonEnabled: StateFlow<Boolean> = _isLoginButtonEnabled.asStateFlow()
fun getOnboardingStatus(): OnboardingStatus {
return repository.isOnboardingCompleted()
}
fun onEvent(event: LoginEvent) {
when (event) {
is LoginEvent.EnteredEmail -> {
_userEmail.value = event.email
_isLoginButtonEnabled.value = _userEmail.value.isNotEmpty() && _userPassword.value.isNotEmpty()
}
is LoginEvent.EnteredPassword -> {
_userPassword.value = event.password
_isLoginButtonEnabled.value = _userEmail.value.isNotEmpty() && _userPassword.value.isNotEmpty()
}
is LoginEvent.Login -> {
viewModelScope.launch {
_isLoginPending.value = true
_isLoginButtonEnabled.value = false
val result = repository.loginUser(_userEmail.value, _userPassword.value)
when (result) {
is AuthRequestState.Success -> { /* nothing to do */ }
is AuthRequestState.Fail -> {
_loginError.emit(result.msg)
}
}
_isLoginPending.value = false
_isLoginButtonEnabled.value = true
}
}
}
}
}
However the problem is that when I am typing something in TextFields in layout inspector I see that other elements of UI (like Button and ClickableButton) are recomposed:
How to avoid such behavior?

How to connect reusable button with text field in Jetpack Compose

I'm trying to hide a reusable floating action button (on click - then show it again when the keyboard is not visible) and request focus for a text field I made but I'm not sure what I'm doing wrong. I used focusRequester.requestFocus() and AnimatedVisibility but nothing happens.
#file:OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MySearchViewDemoTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
MainScreen(showSearchButton = true)
}
}
}
}
sealed interface Shoes { ... }
// Reusable component
#Composable
fun MyReusableScaffold(scaffoldTitle: String,
scaffoldFab: #Composable () -> Unit,
scaffoldContent: #Composable (contentPadding: PaddingValues) -> Unit) {
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
LargeTopAppBar(
title = { Text(text = scaffoldTitle) },
scrollBehavior = scrollBehavior
)
},
floatingActionButton = scaffoldFab,
content = { contentPadding ->
scaffoldContent(contentPadding = contentPadding)
}
)
}
// Reusable component
#Composable
fun MyExtendedFAB(expandedFab: Boolean,
onFabClick: () -> Unit
) {
ExtendedFloatingActionButton(
text = { Text(text = "Search") },
icon = { Icon(Icons.Rounded.Search, "") },
onClick = onFabClick,
expanded = expandedFab
)
}
// Reusable component
#Composable
fun <T> MyLazyColumn(modifier: Modifier,
lazyItems: Array<T>,
onClickItem: (T) -> Unit,
item: #Composable RowScope.(item: T) -> Unit,
listState: LazyListState
) {
var mText by remember { mutableStateOf("") }
val focusRequester = remember { FocusRequester() }
val focusManager = LocalFocusManager.current
val keyboardController = LocalSoftwareKeyboardController.current
Row {
LazyColumn(
modifier = modifier,
state = listState
) {
item {
if (showNotice){
Text(
text = textNotice,
modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
fontStyle = FontStyle.Italic,
style = MaterialTheme.typography.bodyLarge
)
}
}
item {
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester)
value = mText,
onValueChange = {
mText = it
},
label = {
Text(text = "Search")
},
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
keyboardActions = KeyboardActions(
onSearch = {
keyboardController?.hide()
focusManager.clearFocus()
}),
)
}
items(lazyItems) { choice ->
Row(modifier = Modifier
.fillMaxWidth()
.clickable {
onClickItem(choice)
}) {
item(choice)
}
}
}
}
}
#Composable
fun MainScreen(
showSearchButton: Boolean
) {
val keyboardController = LocalSoftwareKeyboardController.current
val focusManager = LocalFocusManager.current
val listState = rememberLazyListState()
val expandedFab by remember {
derivedStateOf {
listState.firstVisibleItemIndex == 0
}
}
val focusRequester = remember { FocusRequester() }
MyReusableScaffold(
scaffoldTitle = "Shoes",
scaffoldIcon = {},
scaffoldFab = {
AnimatedVisibility(visible = showSearchButton) {
MyExtendedFAB(expandedFab) {
focusRequester.requestFocus()
}
}
},
scaffoldContent = { HomeScreenContent(listState = listState, contentPadding = it) }
)
}
#Composable
fun HomeScreenContent(
modifier: Modifier = Modifier,
listState: LazyListState,
contentPadding: PaddingValues = PaddingValues()
) {
val keyboardController = LocalSoftwareKeyboardController.current
val focusManager = LocalFocusManager.current
val shoeItems = remember { arrayOf( ... ) }
MyLazyColumn(
modifier = Modifier.padding(contentPadding),
lazyItems = shoeItems,
onClickItem = {},
item = { ReusableTitleSubtitle(it) },
listState = listState
)
}
}

How to add more items to a static list in Jetpack Compose?

How can I add more elements to the static list in the jetpack compose
#OptIn(ExperimentalFoundationApi::class)
#Composable
fun AddNotesToList(notesList: List<String>) {
val listState = rememberScrollState()
Log.d("TAG", notesList.toString())
LazyColumn() {
items(notesList.size) {
Box(contentAlignment = Alignment.Center,
modifier = Modifier
.padding(start = 15.dp, top = 15.dp, bottom = 1.dp, end = 15.dp)
.fillMaxSize() .horizontalScroll(listState)
.background(Color.White)
.clip(RoundedCornerShape(10.dp)) .padding(15.dp)
.animateItemPlacement(animationSpec = tween(1000))) {
Text(text = notesList[it],
color = Color.Black,
modifier = Modifier.align( Alignment.BottomCenter)
.animateItemPlacement(animationSpec = tween(10000)))
}
}
}
}
this is my addition to the Ui function, this is now I add elements
AddNotesToList(notesList = listOf(
"Drink water",
"Read Books",
"Eat fruits",
"Go for a Walk",
"Drink water",
"Read Books",
"Eat fruits",
"Go for## Heading ## a Walk",
"Go for a Walk",
"Drink water",
"Read Books",
"Eat fruits",
"Go for a Walk"))
now I want to add one more element and I am trying this
function
#Composable
fun AddNewNote(noteDescription: String) {
Log.d("noteDescription", noteDescription)
AddNotesToList(notesList = listOf(noteDescription))
}
Solution:
val _noteList = remember { MutableStateFlow(listOf<String>()) }
val noteList by remember { _noteList }.collectAsState()
// Add note
fun addItem(item: String) {
val newList = ArrayList(noteList)
newList.add(yourItem)
_noteList.value = newList
}
And then you can pass noteList to your LazyColumn
This is how i call my function
AddNewNote { item -> //updating state with added item noteListState = noteListState + listOf(item) }
This is my function
#OptIn(ExperimentalAnimationApi::class)
#Composable
fun AddNewNote(onNewNoteAdded: (String) -> Unit) {
val openDialog = remember { mutableStateOf(true) }
val (visible) = remember { mutableStateOf(true) }
var text by remember { mutableStateOf("") }
AnimatedVisibility(
visible = visible,
enter = slideInVertically(initialOffsetY = { 9000 * it }),
exit = fadeOut()
) {
if (openDialog.value) {
AlertDialog(
onDismissRequest = {
openDialog.value = false
},
title = {
Text(
modifier = Modifier.animateEnterExit(
enter = slideInVertically(
initialOffsetY = { 9000 * it },
),
exit = slideOutVertically()
),
text = "Add Note Description"
)
},
text = {
Column() {
TextField(
value = text,
onValueChange = { text = it }
)
Text("Note description")
}
},
buttons = {
Row(
modifier = Modifier.padding(all = 8.dp),
horizontalArrangement = Arrangement.Center
) {
val addNoteButtonState by remember { mutableStateOf(false) }
if (addNoteButtonState) {
onNewNoteAdded(text)
} else {
Box(contentAlignment = Alignment.Center) {
Button(
modifier = Modifier.fillMaxWidth(),
onClick = {
if (text != "") {
onNewNoteAdded(text)
}
// addNoteButtonState = true
openDialog.value = false
}
) {
Text(
"Add Note To The List",
)
}
}
}
}
},
)
}
}
}

ConcurrentModificationException during mutableStateListOf reverse

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)

Categories

Resources