key { ... } composable vs LazyList's item key as composable key - kotlin

I have a Todo list laid out using LazyColumn, and each item is another Composable (CardView) that handles each TodoModel. I'm using rememberSaveable to manage the state of this CardView's own composable (e.g TextField, clickable images etc), so as to also survive orientation changes.
I have learned to use LazyList's key to tell the structure that each item is unique, in this case I'm using the model's id.
The way this todolist works is its a single screen that shows a list of saved todo (saved by means of persisting via room), and a simple FAB button that when clicked, it adds a temporary todo item(a new todo model with an id of -1L) in the list (it will show on top of the todos in the screen), this is where a user can edit the title and the content of the todo. To summarize, you can delete/edit/create new Todo in a single screen, all saved Todo has positive ID's and for a new one I always set it to -1. Once saved, the id of the new todo model which is -1L will be modified in a list (id that is returned by Room) hoisted in a view model of the entire screen (DashboardScreen's ViewModel)
The thing I noticed is when I define a key in the lazylist this way (LazyDSL)
LazyColumn {
items(key = { it.id },items = todoList) { todo ->
TodoCardItem(
todoModel = todo,
...,
...
and with the following sequence of actions
Click Fab, New todo (id of -1L), empty title
Save this new Todo (once saved, id will be modified by a positive number), title = "My First Todo"
Click Fab, New Todo (id of -1L), title is not empty, displays = "My First Todo"
it references the old state of the card.
But when I define the key this way (Composable key)
LazyColumn {
items(items = todoList) { todo ->
key (todo.id) {
TodoCardItem(
todoModel = todo,
...,
...
it works as I expect it to, every new todo is entirely clean, also it survives configuration changes such as rotation (though Im crashing when I minimize the app, a thing I need to deal with later).
for the rememberSaveable, it is initialized with a TodoModel
.....
companion object {
val Saver = Saver<TodoItemState, Map<String, Any>>(
save = { instanceOfOriginal ->
mapOf(
KEY_MODEL to instanceOfOriginal.todoModel,
KEY_TITLE to instanceOfOriginal.title
)
},
restore = { restorer ->
val state = TodoItemState(restorer[KEY_MODEL] as TodoModel)
state.title = restorer[KEY_TITLE] as String
state
}
)
private const val KEY_MODEL = "key_model"
private const val KEY_TITLE = "key_title"
}
}
#Composable
internal fun rememberTodoItemState(todoModel: TodoModel) =
rememberSaveable(
saver = TodoItemState.Saver
) {
TodoItemState(todoModel)
}
It's a big code so if there are further concerns about some part of it like showing the viewmodel code, how the model's id is modified, I'll paste it upon further questions.

By doing this:
items(items = todoList) { todo ->
key (todo.id) {
you're not only resetting the -1L view, but also all your other cells below the inserted one.
When you're not specifying key, by default it's equal to cell index, so there're kind of two keys on inside an other one, and when the index shifts all the inner keys become invalid and had to create a new view. You can check this behaviour by adding LaunchedEffect with a log inside your key.
I think the most correct way it creating a unique key for each new item, and store a map with new item ids: this will make sure that the tmp cell was reused with the new content.
private val newItemIdsMap = mutableMapOf<Long, Long>()
fun addTmpItem() {
var tmpId: Long
do {
tmpId = Random.nextLong(Long.MIN_VALUE, -1)
} while (newItemIdsMap.containsValue(tmpId))
val tmpItem = Item(id = tmpId)
items.add(0, tmpItem)
}
fun saveItem(tmpItem: Item) {
val savedItem = saveToDb(tmpItem)
newItemIdsMap[savedItem.id] = tmpItem.id
items[0] = savedItem
}
fun getItemId(item: Item) : Long {
val savedId = newItemIdsMap[item.id]
return savedId ?: item.id
}

Related

onSensorChanged function is called in an other app but not in mine (Kotlin)

i want to implement a step counter in my app, so i search how to make that and i found lot of differents implementations.
I notably found an app on GitHub which works. I have tried to implement this code in my app and in an other "test" app but any of them works and i don't no why.
The problem is caused by the onSensorChanged function of my STEP_COUNTER which is not called.
I have search in all the files of the app and i don't found the problem.
If somebody have a solution...
(I'm french so sorry if it's badly written)
the code i use:
private var sensorManager: SensorManager? = null
// Creating a variable which will give the running status
// and initially given the boolean value as false
private var running = false
// Creating a variable which will counts total steps
// and it has been given the value of 0 float
private var totalSteps = 0f
// Creating a variable which counts previous total
// steps and it has also been given the value of 0 float
private var previousTotalSteps = 0f
//in the onCreate
loadData()
resetSteps()
// Adding a context of SENSOR_SERVICE as Sensor Manager
sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
override fun onResume() {
super.onResume()
mainHandler.post(pingRunnable)
binding.map.onResume()
running = true
// Returns the number of steps taken by the user since the last reboot while activated
// This sensor requires permission android.permission.ACTIVITY_RECOGNITION.
// So don't forget to add the following permission in AndroidManifest.xml present in manifest folder of the app.
val stepSensor = sensorManager?.getDefaultSensor(Sensor.TYPE_STEP_COUNTER)
if (stepSensor == null) {
// This will give a toast message to the user if there is no sensor in the device
Toast.makeText(this, "No sensor detected on this device", Toast.LENGTH_SHORT).show()
} else {
// Rate suitable for the user interface
sensorManager?.registerListener(this, stepSensor, SensorManager.SENSOR_DELAY_UI)
}
}
override fun onSensorChanged(event: SensorEvent?) {
// Calling the TextView that we made in activity_main.xml
// by the id given to that TextView
var tvStepsTaken = findViewById<TextView>(R.id.step)
if (running) {
totalSteps = event!!.values[0]
// Current steps are calculated by taking the difference of total steps
// and previous steps
val currentSteps = totalSteps.toInt() - previousTotalSteps.toInt()
// It will show the current steps to the user
tvStepsTaken.text = ("$currentSteps")
}
}
private fun resetSteps() {
var resetButton = findViewById<Button>(R.id.reset)
resetButton.setOnClickListener {
// This will give a toast message if the user want to reset the steps
previousTotalSteps = totalSteps
// When the user will click long tap on the screen,
// the steps will be reset to 0
testFragment?.binding?.step?.text = 0.toString()
// This will save the data
saveData()
true
}
}
private fun saveData() {
// Shared Preferences will allow us to save
// and retrieve data in the form of key,value pair.
// In this function we will save data
val sharedPreferences = getSharedPreferences("myPrefs", Context.MODE_PRIVATE)
val editor = sharedPreferences.edit()
editor.putFloat("key1", previousTotalSteps)
editor.apply()
}
private fun loadData() {
// In this function we will retrieve data
val sharedPreferences = getSharedPreferences("myPrefs", Context.MODE_PRIVATE)
val savedNumber = sharedPreferences.getFloat("key1", 0f)
// Log.d is used for debugging purposes
Log.d("MainActivity", "$savedNumber")
previousTotalSteps = savedNumber
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
// We do not have to write anything in this function for this app
}

Why does the author add key(task) for the items function in LazyColumn?

The Code A is from the offical sample project.
I don't understand why the author need to add key(task) for the items function in LazyColumn, could you tell me?
I think the Code B can work well and it's simple.
Code A
val allTasks = stringArrayResource(R.array.tasks)
val tasks = remember { mutableStateListOf(*allTasks) }
...
items(count = tasks.size) { i ->
val task = tasks.getOrNull(i)
if (task != null) {
key(task) {
TaskRow(
task = task,
onRemove = { tasks.remove(task) }
)
}
}
}
Code B
...
items(tasks) { task ->
TaskRow(
task = task,
onRemove = { tasks.remove(task) }
)
}
Keys in general are used to avoid unnecessary recompositions. If you have a list of items, and some items are reordered, Compose will find existing components in the composition based on their key and reorder them accordingly instead of recomposing all of them.
That said, items() already supports a key parameter. So code A could indeed be simplified this way if the tasks in the list are non-nullable:
items(tasks, key = { it }) { task ->
TaskRow(
task = task,
onRemove = { tasks.remove(task) },
)
}
But code B would use the items' positions instead of the items themselves as key by default, which is not desirable.
Disclaimer: I'm no expert in JetPack Compose

FastObjectListView UpdateObject() randomly reorders rows within primary sort

Data is a generic List of domain objects.
I click the "Deploy Status" column header to sort on that column.
I have a button that does nothing more than folv.UpdateObject(someObject) .
Every time I press that button, the Deploy Status column maintains its sort, but all rows within the sorted blocks are randomly reordered, as per screenshot.
I have commented out everything in the form's code beyond loading the data, the test button, and the FastObjectListView's column.Add() and .SetObjects(). There are no event handlers wired up for the FastObjectListView. I am not setting PrimarySort or SecondarySort in code; only by clicking with the mouse.
You should be able to fix this problem by either calling Sort after your button's call to UpdateObject or changing your usage of UpdateObject to RefreshObject
Reproducing the problem (C# Repro for the issue in the API)
This seems to reproduce the problem you are having. Run the code, sort the Other column ascending. Click the update button.
public class MainForm : Form
{
public MainForm()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm));
//
// MainForm
//
this.ClientSize = new System.Drawing.Size(300, 300);
this.Name = "MainForm";
this.ResumeLayout(false);
this.PerformLayout();
var OLVa = new FastObjectListView();
OLVa.Width = 250;
OLVa.Height = 250;
OLVa.Columns.Add(new OLVColumn("ID", "ID"));
OLVa.Columns.Add(new OLVColumn("Other", "Other"));
var l1 = new lolz(1, 3);
OLVa.AddObject(l1);
OLVa.AddObject(new lolz(2,3));
this.Controls.Add(OLVa);
var btn = new Button()
{
Text = "Update",
Top = OLVa.Bottom
};
btn.Click += (s,e)=>OLVa.UpdateObject(l1);
this.Controls.Add(btn);
}
private class lolz
{
public int ID;
public int Other;
public lolz(int id, int other)
{
ID = id;
Other = other;
}
}
}
Fixing the problem
The following would fix it for the above example:
btn.Click += (s,e)=>
{
OLVa.BeginUpdate();
try
{
OLVa.UpdateObject(l1);
OLVa.Sort();
}
finally
{
OLVa.EndUpdate();
}
};

View not updating after changing value of component array

I need to update my view on changing array in my *.component.ts
I use
public getFolders() : void {
this.webService.getFolders({client_id : this.local.get('clientUser').client_id}).subscribe( this.processSkills.bind(this, this.local.get('clientUser')))
}
processSkills(res: any, myobj): void {
if(res.status){
myobj.folders = res.folders;
this.local.set('clientUser', myobj);
this.userObj = this.local.get('clientUser');
}
}
It updates my array i saw in console it update my session value which i saw after pressing F5 but it doesn't update my view
Initially i am assigning my array to variable from my session object.
import { BehaviorSubject } from 'rxjs';
private messageSource = new BehaviorSubject(this.local.get('clientUser'));
currentMessage = this.messageSource.asObservable();
I resolved it and found a solution to pass our array into session and make the code into our provider which works as observable to my array and then recieve
currentMessage to our receiver function to update on view.
this.webService.currentMessage.subscribe(message => {
this.userObj = message;
})
will receive updated value and will reflect on view.

TornadoFX - remove item with ContextMenu right click option

So I have a table view that displays an observedArrayList of AccountsAccount(name, login, pass), those are data classes. When I right click a cell there pops an option of delete. What I want to do is delete that Account from the observedArrayList
Only I can not find any way to do this. I am not experienced with JavaFX or TornadoFX and I also can't find the answer with google or in the TornadoFX guides and docs.
This is my code:
class ToolView : View() {
override val root = VBox()
companion object handler {
//val account1 = Account("Google", "martvdham#gmail.com", "kkk")
//val account2 = Account("Google", "martvdham#gmail.com", "Password")
var accounts = FXCollections.observableArrayList<Account>(
)
var gson = GsonBuilder().setPrettyPrinting().create()
val ggson = Gson()
fun writeData(){
FileWriter("accounts.json").use {
ggson.toJson(accounts, it)
}
}
fun readData(){
accounts.clear()
FileReader("accounts.json").use{
var account = gson.fromJson(it, Array<Account>::class.java)
if(account == null){return}
for(i in account){
accounts.add(i)
}
}
}
}
init {
readData()
borderpane {
center {
tableview<Account>{
items = accounts
column("Name", Account::name)
column("Login", Account::login)
column("Password", Account::password)
contextMenu = ContextMenu().apply{
menuitem("Delete"){
selectedItem?.apply{// HERE IS WHERE THE ITEM DELETE CODE SHOULD BE}
}
}
}
}
bottom{
button("Add account").setOnAction{
replaceWith(AddView::class, ViewTransition.SlideIn)
}
}
}
}
}
Thanks!
To clarify #Martacus's answer, in your case you only need to replace // HERE IS WHERE THE ITEM DELETE CODE SHOULD BE with accounts.remove(this) and you're in business.
You could also replace the line
selectedItem?.apply{ accounts.remove(this) }
with
selectedItem?.let{ accounts.remove(it) }
From my experience, let is more common than apply when you are just using a value instead of setting up a receiver.
Note that the process will be different if the accounts list is constructed asynchronously and copied in, which is the default behavior of asyncItems { accounts }.
selectedItem is the item you have selected/rightclicked.
Then you can use arraylist.remove(selectedItem)