This is a Kotlin Compose Multiplatform Desktop toy I'm playing around with. I'm new to Kotlin (and Compose).
I have a library filled with books. I have a view where users can specify a filter criteria and I list a subset of the entire library for them based on the filter. I would like for users to be able to click on those books and alter a view-specific aspect of them (like the book is Open or Closed). Users may have multiple lists with different filters, and Openning/Closing a book in one list does not change that value in another view. However, if the user changes the name of a book, I want it reflected in all lists.
Here's where I'm stuck.
So, I have a Library
class that has a mutableStateMapOf<ID,Book>()
which has the entire library.
I have MyLibraryView
which takes a private val library: Library
and has a mutableStateOf
a filter predicate. With that in hand, I'm using derivedStateOf
to have myBooks
...
class MyLibraryView(private val library: Library) {
private val filterPredicate =
mutableStateOf<(Book) -> Boolean>({ true /* updated when user updates filter */ })
val myBooks: State<List<Book>> = derivedStateOf {
library.allBooks.values.filter(predicate = filterPredicate.value)
}
}
I have a @Composable
with a LazyColumn
to list my books:
@Composable
fun MyLibraryList(
myBooks: State<List<Book>>,
) {
val listState = rememberLazyListState()
LazyColumn(
state = listState,
) {
items(
items = myBooks.value,
key = { book -> book.id },
) { book ->
BookRow(book)
}
}
}
This works as expected (I've understood State<List<Book>>
is Stable
, but List<Book>
would not be?).
I'm not sure how to best add the display details (the open/closed, but also maybe if it's selected, etc).
I could alter the data class Book
to include DisplayDetails
. This doesn't work since the Open/Closed becomes part of Book
and then is shared across views.
Make a BookWithDisplayDetails
data class and have the derivedStateOf
lambda for myBooks
return that instead of Book
. This doesn't work because (afaict), you can't reference myBooks
from within the lambda (I get an error about it needing to be initialized first), so each time myBooks
is recalculated, the existing DisplayDetails is lost (so if a Book
changes title, the DisplayDetails
would be wiped out).
Have a separate Map<ID,DisplayDetails>
in MyLibraryView
, and a fun toggleOpenClosed(book: Book)
which does as you'd expect.
This doesn't work directly, so it'd have to be a mutableStateMapOf
. But also, if the filterPredicate
changes, then the derivedStateOf
myBooks
will need to change, and it should remove any Entry
from that map that is filtered away. This is a mess and causes a full recomposition of the entire list.
I'm missing something obvious and simple, it could be because I'm new to this.
If you only need the details to exist in the UI without any persistence I think the easiest solution is to just introduce the details as variables in BookRow
, like this:
var isOpen by remember { mutableStateOf(false) }
Each BookRow
, that is, each book in its filtered list, now has its own isOpen
variable that is local to this specific BookRow
. Setting this will only affect one book in one list.
Changing the name should be done at the source where the Book objects come from so it will affect all Books.