I am trying to use TinyDB from Github to save a list (mustableListOf), however when I try to save a string to the List, it puts an error on the 2nd parameter.
val tinydb = TinyDB(this)
var todoList = mutableListOf(
Todo("Write your first TODO statement", false)
)
val adapter = TodoAdapter(todoList)
binding.rvTodos.adapter = adapter
binding.rvTodos.layoutManager = LinearLayoutManager(this)
binding.btnAddTodo.setOnClickListener {
val title = binding.etTodo.text.toString()
val todo = Todo(title, false)
todoList.add(todo)
//puts an error on the "mutableListOf(todoList)"
tinydb.putListString("TodoList", mutableListOf(todoList))
adapter.notifyItemInserted(todoList.size - 1)
}
How could I resolve this?
Github TinyDB Link: https://github.com/kcochibili/TinyDB--Android-Shared-Preferences-Turbo
Updated code: however when the app is launched on my phone, the app keeps stopping. It only works the first time when nothing is stored in TinyDB. It doesn't put an error on the 2nd parameter anymore, but I feel like it's not storing/working in its intended way. Thanks.
val tinydb = TinyDB(this)
var todoList = mutableListOf(
Todo("Write your first TODO statement", false)
)
// line below is to get the list and set it to recyclerview; however app keeps stopping when invoked
tinydb.getListObject("TodoList", Todo::class.java)
val adapter = TodoAdapter(todoList)
binding.rvTodos.adapter = adapter
binding.rvTodos.layoutManager = LinearLayoutManager(this)
binding.btnAddTodo.setOnClickListener {
val title = binding.etTodo.text.toString()
val todo = Todo(title, false)
todoList.add(todo)
//changed to putListObject
tinydb.putListObject("TodoList", arrayListOf(todoList))
adapter.notifyItemInserted(todoList.size - 1)
}
Related
I have a MutableStateFlow<List<AttendanceState>>,
var attendanceStates = MutableStateFlow<List<AttendanceState>>(arrayListOf())
private set
My AttendanceState data class.
data class AttendanceState (
var memberId: Int,
var memberName: String = "",
var isPresent: Boolean = false,
var leaveApplied: Boolean = false
)
The list is rendered by a LazyColumn
The LazyColumn contains Checkboxes.
If i update the checkbox, the event is propagated to the ViewModel and from there I'm changing the value in the list
attendanceStates.value[event.index].copy(leaveApplied = event.hasApplied)
attendanceStates.value = attendanceStates.value.toList()
But this is not updating the LazyColumn.
Snippet of my implementation:
val attendances by viewModel.attendanceStates.collectAsState()
LazyColumn(modifier = Modifier.fillMaxWidth().padding(top = 24.dp)) {
Log.e("Attendance","Lazy Column Recomposition")
items(attendances.size) { index ->
AttendanceCheckBox(attendanceState = attendances[index], modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), onAttendanceStatusChangeListener = { viewModel.onEvent(AttendanceEvent.IsPresentStatusChanged(index, it)) }, onLeaveAppliedStatusChangeListener = { viewModel.onEvent(AttendanceEvent.IsLeaveAppliedStatusChanged(index, it)) })
}
}
Re-composition is not happening.
Try this:
viewModelScope.launch {
val helper = ArrayList(attendanceStates.value)
helper[event.index] = helper[event.index].copy(leaveApplied = event.hasApplied)
attendanceStates.emit(helper)
}
Changing an item's properties will not trigger a StateFlow, you have to replace the whole item with the changed item and emit a new list.
I would recommend SnapshotStateList instead of a standard List, this will guarantee an update without having to create a new instance of it like what you would do with an ordinary List, assuming you call AttendanceState instance copy() and updating at least one of its properties with a different value.
var attendanceStates = MutableStateFlow<SnapshotStateList>(mutableStateListOf())
private set
I would also recommend changing the way you use your LazyColumn where items are mapped by their keys not just by their index position,
LazyColumn(modifier = Modifier.fillMaxWidth().padding(top = 24.dp)) {
items(attendances, key = {it.memberId}) {
AttendanceCheckBox(...)
}
}
and if you still need the index position.
LazyColumn(modifier = Modifier.fillMaxWidth().padding(top = 24.dp)) {
itemsIndexed(attendances, key = { index, item ->
item.memberId
}) { item, index ->
AttendanceCheckBox(...)
}
}
You should also use update when updating your StateFlow instead of modifying its value directly to make it concurrently safe.
attendanceStates.update { list ->
val idx = event.idx
list[idx] = list[idx].copy(leaveApplied = event.hasApplied)
list
}
I have a TodoList App and I have a reminder feature in the fragment where you add your Todo, Meanwhile in another fragment the set Reminder is shown for that particular Task that was created. I want to be able to change the color of the Reminder to red when the Alarm set by the USER is Overdue or has been triggered. I was able to do the first but not the second. Is there a way I can check if the Alarm for that Todo has been Triggered(because the color for the second situation only changes when I recreate the view) in REAL TIME and then update it in the ListFragment changing the color of the Alarm to red. The Solutions I have tried in Stack Overflow here don't seem to work and is in a different language.
A picture of it.
My Alarm and Notifications code in my AddFragment
private fun scheduleNotification() {
val title = binding.edTaskTitle.text.toString()
val intent = Intent(requireContext().applicationContext , Notifications::class.java).apply {
putExtra(TITLE_EXTRA, title)
}
val pendingIntent = PendingIntent.getBroadcast(
requireContext().applicationContext,
NOTIFICATION_ID,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
val alarmManager = requireContext().getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmManager.setAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
setDateTime,
pendingIntent
)
}
This is my bind function in my Adapter(My Adapter is in a separate file from my Fragment). In my Adapter is where I wrote the Implementation for changing the color of the reminder text if the Initial reminder set by the User is past the current Time(Overdue).
#SuppressLint("DiscouragedPrivateApi")
fun bind(todo : Todo) {
val dateLocales = SimpleDateFormat(SIMPLE_DATE_FORMAT, Locale.getDefault())
val timeLocales = SimpleDateFormat(SIMPLE_TIME_FORMAT, Locale.getDefault())
binding.apply {
tvTaskTitle.text = todo.title
tvTaskDate.text = dateLocales.format(todo.date)
tvTaskTime.text = timeLocales.format(todo.time)
cbTask.isChecked = todo.completed
tvTaskTitle.paint.isStrikeThruText = todo.completed
tvResultsReminder.isVisible = todo.important
// Will only show the resultsReminder if important is true
if (todo.important) {
tvResultsReminder.text = DateUtils.getRelativeDateTimeString(_context, todo.reminder.time, DateUtils.DAY_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, 0)
val date = Date()
val drawable : Drawable? = ContextCompat.getDrawable(_context, R.drawable.ic_alarm_reminder)
if (todo.reminder.time < date.time) {
tvResultsReminder.setTextColor(ContextCompat.getColor(_context, R.color.red))
}
}
// Implementing our PopupMenus to Edit and Delete a Task
iMenus.setOnClickListener { view ->
val popupMenus = PopupMenu(_context, view)
popupMenus.inflate(R.menu.show_menu)
popupMenus.setOnMenuItemClickListener {
when(it.itemId) {
R.id.itEditTask -> {
val action = ListFragmentDirections.actionListFragmentToUpdateFragment(todo)
itemView.findNavController().navigate(action)
true
}
R.id.itDeleteTask -> {
val position = adapterPosition// this represents the position of any item in the root layout
// NO_POSITION means that an item is invalid and out of this list, so this is a safe check because-
// we don't want to call a listener on an invalid item
if (position != RecyclerView.NO_POSITION) {
val curTodo = getItem(position)
listener.onItemDelete(curTodo)
}
Toast.makeText(_context, "Task has been deleted.", Toast.LENGTH_LONG).show()
true
}
else -> true
}
}
popupMenus.show()
val popup = PopupMenu::class.java.getDeclaredField("mPopup")
popup.isAccessible = true
val menu = popup.get(popupMenus)
menu.javaClass.getDeclaredMethod("setForceShowIcon", Boolean::class.java)
.invoke(menu, true)
}
}
}
}
And then I passed in a Column "triggered" in my Todo Class to record the state of the triggered Alarm to know what to do as the supposed Solution.
#Parcelize
#Entity(tableName = "todo_table")
data class Todo(
#PrimaryKey (autoGenerate = true) // here "Room" will autoGenerate the id for us instead of assigning a randomUUID value
val id : Int = 0,
val title : String = "",
var date : Date = Date(),
var time : Date = Date(),
var reminder : Date = Date(),
var important : Boolean = false,
var completed : Boolean = false,
val triggered : Boolean = false,
val created : Long = System.currentTimeMillis()
) : Parcelable
Another issue is that even I were able to do this. How would I in REAL TIME without recreating the view or doing something else be aware that the state of the Triggered alarm has been changed to then change the color of the text. It just seems so difficult.
in my ViewModel:
private val _itemList = mutableStateListOf<Post>()
val itemList: List<Post> = _itemList
fun likePost(newPost: Post){
val index = _itemList.indexOf(newPost)
_itemList[index] = _itemList[index].copy(isLiked = true)
}
Here my Post data class:
data class Post(
val id: Int,
val name: String,
val isLiked: Boolean = false,
)
And here my Composable:
val postList = viewModel.itemList
LazyRow(content = {
items(postList.size) { i ->
val postItem = postList[i]
PostItem(
name = postItem.name,
isLiked = postItem.isLiked,
likePost = { viewModel.likePost(postItem)}
)
}
})
The change does not update in the UI instantly, I first have to scroll the updated item out of the screen so it recomposes or switch to another Screen and go back to see the change.
For some reason it doesn't like updating, it will add and delete and update instantly. You have to do it this way when updating for our to update the state.
fun likePost(newPost: Post){
val index = _itemList.indexOf(newPost)
_itemList[index] = _itemList[index].copy()
_itemList[index].isLiked = true
}
You are returning a List<> effectively and not MutableStateList from your ViewModel.
If you want the list to not be mutable from the view, I happen to use MutableStateFlow<List<>> and return StateFlow<List<>>. You could also just convert it to a list in your composable.
Edit:
//backing cached list, or could be data source like database
private val deviceList = mutableListOf<Device>()
private val _deviceListState = MutableStateFlow<List<Device>>(emptyList())
val deviceListState: StateFlow<List<BluetoothDevice>> = _deviceListState
//manipulate and publish
fun doSomething() {
_deviceListState.value = deviceList.filter ...
}
In your UI
val deviceListState = viewModel.deviceListState.collectAsState().value
I'm trying to display a 4x4 grid with values that change depending on user input. To achieve that, I created mutableStateListOf that I use in a ViewModel to survive configuration changes. However, when I try to replace a value in that particular list using button onClick, it keeps doing that until app crashes. I can't understand why is onReplaceGridContent looping after clicking the button once. Currently, my code looks like this:
ViewModel:
class GameViewModel : ViewModel(){
var gameGridContent = mutableStateListOf<Int>()
private set // Restrict writes to this state object to private setter only inside view model
fun replaceGridContent(int: Int, index: Int){
gameGridContent[index] = int
}
fun removeGridContent(index: Int){
gameGridContent[index] = -1
}
fun initialize(){
for(i in 0..15){
gameGridContent.add(-1)
}
val firstEmptyGridTile = GameUtils.getRandomTilePosition(gameGridContent)
val firstGridNumber = GameUtils.getRandomTileNumber()
gameGridContent[firstEmptyGridTile] = firstGridNumber
}
}
Button:
Button(
onClick = {
onReplaceGridContent(GameUtils.getRandomTileNumber(),GameUtils.getRandomTilePosition(gameGridContent))},
colors = Color.DarkGray
){
Text(text = "Add number to tile")
}
Activity Composable:
#Composable
fun gameScreen(gameViewModel: GameViewModel){
gameViewModel.initialize()
MainStage(
gameGridContent = gameViewModel.gameGridContent,
onReplaceGridContent = gameViewModel::replaceGridContent,
onRemoveGridContent = gameViewModel::removeGridContent
)
}
Your initialize will actually run on every recomposition of gameScreen:
You click on a tile - state changes causing recomposition.
initializa is called and changes the state again causing recomposition.
Step 2 happens again and again.
You should initialize your view model in its constructor instead (or use boolean flag to force one tim initialization) to make it inly once.
Simply change it to constructor:
class GameViewModel : ViewModel(){
var gameGridContent = mutableStateListOf<Int>()
private set // Restrict writes to this state object to private setter only inside view model
fun replaceGridContent(int: Int, index: Int){
gameGridContent[index] = int
}
fun removeGridContent(index: Int){
gameGridContent[index] = -1
}
init {
for(i in 0..15){
gameGridContent.add(-1)
}
val firstEmptyGridTile = GameUtils.getRandomTilePosition(gameGridContent)
val firstGridNumber = GameUtils.getRandomTileNumber()
gameGridContent[firstEmptyGridTile] = firstGridNumber
}
}
Now you don't need to call initialize in the composable:
#Composable
fun gameScreen(gameViewModel: GameViewModel){
MainStage(
gameGridContent = gameViewModel.gameGridContent,
onReplaceGridContent = gameViewModel::replaceGridContent,
onRemoveGridContent = gameViewModel::removeGridContent
)
}
For example, I load data into a List, it`s wrapped by MutableStateFlow, and I collect these as State in UI Component.
The trouble is, when I change an item in the MutableStateFlow<List>, such as modifying attribute, but don`t add or delete, the UI will not change.
So how can I change the UI when I modify an item of the MutableStateFlow?
These are codes:
ViewModel:
data class TestBean(val id: Int, var name: String)
class VM: ViewModel() {
val testList = MutableStateFlow<List<TestBean>>(emptyList())
fun createTestData() {
val result = mutableListOf<TestBean>()
(0 .. 10).forEach {
result.add(TestBean(it, it.toString()))
}
testList.value = result
}
fun changeTestData(index: Int) {
// first way to change data
testList.value[index].name = System.currentTimeMillis().toString()
// second way to change data
val p = testList.value[index]
p.name = System.currentTimeMillis().toString()
val tmplist = testList.value.toMutableList()
tmplist[index].name = p.name
testList.update { tmplist }
}
}
UI:
setContent {
LaunchedEffect(key1 = Unit) {
vm.createTestData()
}
Column {
vm.testList.collectAsState().value.forEachIndexed { index, it ->
Text(text = it.name, modifier = Modifier.padding(16.dp).clickable {
vm.changeTestData(index)
Log.d("TAG", "click: ${index}")
})
}
}
}
Both Flow and Compose mutable state cannot track changes made inside of containing objects.
But you can replace an object with an updated object. data class is a nice tool to be used, which will provide you all copy out of the box, but you should emit using var and only use val for your fields to avoid mistakes.
Check out Why is immutability important in functional programming?
testList.value[index] = testList.value[index].copy(name = System.currentTimeMillis().toString())