hello getting my feet wet with compose for desktop...
# compose-desktop
n
hello getting my feet wet with compose for desktop how do you subscribe to changes in
MutableState
? (i’ve close to zero experience in android)
k
You don't really subscribe to them... They just get used...
n
ok but how do i handle the use-case where i want a variable updated when other variables are changed?
k
I'm looking at your example and don't think it's a normal thing to do... You'd probably move that stuff into a viewmodel and do the normal coroutine thing with sharedflows
The auto change handling magic is for UI
n
viewmodel
seems to be an android thing 😅
k
MVI/Redux then
You could have UI events change the remember mutable state but unless it's a self contained widget, it's mixing business logic with UI...
n
funny... the observer pattern is in the stdlib via
delegate by observable
i just want binding between two variables but because compose seems to frown on classes, i’m stuck 🤔
a
derivedStateOf
might be what you're looking for if you want snapshot state that depends on other snapshot state
compose doesn't frown on classes; you can write classes that back their fields with snapshot state
n
@Adam Powell thanks for the hint about
derivedStateOf
the class thing is what i’m deducing after a couple of hours perhaps i’m completely mistaken
a
definitely open to feedback on how to better position some of the docs; most of them are trying to showcase the compose-specific concepts
for example, you can write classes like this:
Copy code
class MyState(
  initialFoo: Foo
) {
  var foo by mutableStateOf(initialFoo)
  val bar by derivedStateOf { foo.computeBar() }
}
n
i come from a swing background... from what i see, the idea is to let android developers move to the desktop
wrong persona for me 😉
a
yes, much of the docs are indeed from the perspective of Android developers so far 🙂
👍 1
independent of platform though, the intention is for
@Composable
functions to act as transformations of app state to UI. The input state is generally hoisted out of the
@Composable
functions themselves as code scales
and that state often ends up looking like the snapshot state-backed example class above
if you would like to observe snapshot state changes from outside of compose APIs, there is
snapshotFlow {}
n
the hoisting stuff is completely different from what i’m used to i believe it will take some time to wrap my head around it
a
yes, it's a very different way of thinking about UI than Swing
(or Android Views, for that matter)
it solves a lot of long-standing challenges though, especially around async responses and validation
often lends itself to easier testability
n
i need to read more about
@Composable
i think it’s the main building block
the fact that most of compose documentation uses android doesn’t help for sure
a
You might find some of Leland's blog posts helpful, he writes mostly from a platform-agnostic perspective about the compose runtime itself, which all applies to compose desktop: https://medium.com/androiddevelopers/understanding-jetpack-compose-part-1-of-2-ca316fe39050
👍 1
n
thanks for your help!
a
you're welcome! Happy to 🙂
n
Let’s look at this more practically in the context of Android development today and take the example of a view model and an XML layout.
😂
a
it's a short section on that part, I promise 🙂
👍 1
n
so the framework will actually call a
@Composable
function if any of its
@Composable
parameters has been changed correct?
c
Reading some docs on react (like lifting state up) has helped me conceptually get prepared for compose. if you wish to take a look at those docs. They are really good!
k
https://kotlinlang.slack.com/archives/C01D6HTPATV/p1609730449296100?thread_ts=1609726620.287000&cid=C01D6HTPATV Yes... Think of it as rerendering children as needed and every param/remember is "state". It very much is like React and Flutter.
n
Yes
Thanks 🙂
It very much is like React and Flutter
well, those references are lost on me as i know none of them 😅
k
React and Flutter are much more mature though so the same concepts usually apply. E.g. they work best with MVI/Redux architectures to manage state.
c
@nfrankel I don't know react either. Just saying their docs are mature and super simple. So it's worth taking a peek there.
☝️ 1
👍 1
n
i managed to get something working though i’m not sure whether it’s “the way”
a
happy to give some feedback on idiomatic patterns if you'd like to post a gist or similar
n
i appreciate the proposal it’s a bit more than a gist but not gigantic either just a simple batch file renamer https://github.com/ajavageek/renamer-compose
thanks in advance
👍 1
a
by and large I'd say you're getting the hang of it. There's the style nit of
@Composable
functions that return
Unit
being
PascalCased
as types/entities, but that one's minor and the lint hinting for it will probably make its way to an idea plugin/config eventually 🙂
🎉 1
Structurally I think you're seeing the design pressure that leads to
by mutableStateOf
-backed classes
composable functions that accept
MutableState<T>
parameters are usually a canary that signals that you want to establish a more specific API with the UI element
applyButton
is a good example: this
rename
inner function used as the
onClick
handler would be a great fit as a method on such a class, or an extension function with such a class as its receiver: https://github.com/ajavageek/renamer-compose/blob/master/src/main/kotlin/ApplyButton.kt#L17
Some of the work with the file chooser using swing directly is a good example of things that can be wrapped into the declarative style and then used that way, but there's nothing inherently wrong with doing it the way it's done here. It's very similar to using
suspendCancellableCoroutine
to bridge
suspend
functions and callback-driven async code
the equivalent APIs for bridging such things in compose are the
*Effect
composables:
SideEffect
,
DisposableEffect
and
LaunchedEffect
, depending on what you're up to
👀 1
What is the
changed
state intending to model?
Some of the
SwingPanel
bits look suspicious but I think that might be on our end rather than yours 🙂
n
• swing: i don’t see any table component, so i had to use the swing one • same for filechooser •
changed
is interesting
i want a “refresh” feature something that tells the framework to recall everything: the table needs to read from the fs again
i tried to achieve that by using “derived” mutable state but it seems those functions are not called
a
It looks like you don't need mutable/mutableStateLists in there, as they're only being consumed downstream
so I don't think the `.toMutableStateList()`s are heading in the direction you want
You might be looking for something more like:
Copy code
val candidates = remember {
    derivedStateOf {
        val regex = pattern.value.toRegex()
        val replace = replacement.value
        files.map {
            if (pattern.value.isBlank()) it
            else {
                val newName = regex.replace(it.name, replace)
                File(newName, it.parent)
            }
        }
    }
}
so you get one
State<List<File>>
instance that will automatically update whenever the things it reads change
though I think there might be something structurally problematic with the
SwingPanel
API being used to present the file table, and I'm having trouble locating the source code for it to verify
from the call site it's accepting direct parameters of initialized-once Swing components; this means if the content lambda of the containing
Row
recomposes, new swing components are created instead of updating existing ones, which seems suspect
for this to work the way you're intending, it looks like you would need to observe snapshot changes of your list and notify your AbstractTableModel listeners of the changes
a good illustration of the kinds of things compose aims to do transparently 🙂
n
i was missing
derivedStateOf
thanks for letting me know 👍
though I think there might be something structurally problematic with the 
SwingPanel
 API being used to present the file table, and I’m having trouble locating the source code for it to verify
😬
it looks like you would need to observe snapshot changes of your list and notify your AbstractTableModel listeners of the changes
so back to the old manual observer pattern again?
derivedStateOf
works perfectly! now i just need a way to “refresh” the state when i’ve renamed the files
any hint for that would be greatly appreciated (if you still have time of course)
a
If you connect a
snapshotFlow { theTableState }
collect call to send
AbstractTableModel
data change notifications I think you should be in business
👀 1
n
i’ll have a look, thanks
i’m afraid i don’t manage to get this... so far, i’ve: •
[path]
=(derived state)=>
files
[path, pattern, replacement]
=(derived state)=>
candidates
[files, candidates]
=(@Composable)=>
filemodel
the button renames files so the only origin i’ve is the filesystem itself which is outside the scope of compose 😅 with a legacy observer pattern, i just send an event from the button that is subscribed to by the model and that calls refresh
a
you should be able to do the same here if you want refresh to be manual
I presumed that you wanted the table to refresh automatically as the files change
the value of a
derivedStateOf
state object will always be up to date for instantaneous reads, like you would do from a button click handler
n
you should be able to do the same here if you want refresh to be manual
do you have a link to the API to explicitly call a refresh?
I presumed that you wanted the table to refresh automatically as the files change
that’s the case, i just don’t understand how
a
Since
AbstractTableModel
is outside compose, it looks like you'll want to update it outside of composition, but with a subscription scope driven by the composition of your inserted swing UI. Consider something like:
Copy code
Row(padding.fillMaxWidth().fillMaxHeight(0.85f)) {
    // fileModel now accepts the wrapped types, not State<T>.
    // We use the LaunchedEffect below to scope a subscription that pushes updates to it.
    val model = remember { fileModel(files.value, candidates.value) }
    // Monitor candidates and notify the model of updates
    LaunchedEffect(model) {
        // snapshotFlow runs the block and emits its result whenever
        // any snapshot state read by the block was changed.
        snapshotFlow { Pair(files.value, candidates.value) }
            .collect { (currentFiles, currentCandidates) ->
                // assume this call internally invokes fireDataTableChanged()
                model.updateFilesAndCandidates(currentFiles, currentCandidates)
            }
    }
    // Don't recreate the swing UI elements on every recomposition
    val pane = remember(model) { JScrollPane(FileTable(model)) }
    SwingPanel(pane)
}
n
thanks @Adam Powell i’ll look at it tomorrow (i’m in europe) i really appreciate all the time you give me 🙇‍♂️
👍 1
i’ve updated the code according to your snippet good stuff: i don’t need to move out of the replacement field to refresh the table still, it doesn’t handle the initial issue when i click on the button, it rename files so the “source” of any change is the filesytem itself which is of course out of the scope of compose from the button, is there any way to call the compose refresh behavior?
sorry, it seems i don’t get it 😞
a
which data do you want to refresh from the button?
n
the button rename the files so i want
files
to be refreshed
candidates
is derived data, so it will be updated automatically
and
files
is getting the children of the
path
which doesn’t change by itself
as a brute-force option, is there a function to tell compose to re-compose?
i managed to get it working by using a dummy variable, forcing recomposition
🎉
a
there is a function to tell it to recompose a scope, but reaching for it usually means there's a better way to structure the data flow.
Copy code
@Composable fun MyComposable() {
  val recomposeMe = invalidate // returns an invalidator for this current scope
  Button(onClick = { recomposeMe() }) { /* ... */ }
}
is how you use it.
👍 1
This is again where I would define a class with properties backed
by mutableStateOf
- this class would be your data model that holds
files
and the other derived properties, and you would
remember
an instance of it rather than several disjoint state objects. Then you can define a
refresh()
function on that class, which performs the refresh, sets
files.value
and then any necessary recomposition happens as a result of that data change.
☝️ 1
n
now i think i understand 🙂
🎉 1
👍 1
k
It'd help to read up on Redux or MVI architectures
👍 1
n
@Adam Powell i’ve pushed what i believe is a final version of the code following your advices feedback is welcome 🙂
a
Took a look, a few things: 1. All of the custom get/set property methods in StateModel can be skipped thanks to property delegation. There's an IDE bug that can hinder auto-import for
androidx.compose.runtime.getValue
and
androidx.compose.runtime.setValue
but you can import them manually. Then you can write
Copy code
var path by mutableStateOf(initialPath)
and skip all of the plumbing code. 2. I think you can avoid
tracker
entirely now. 3. You probably don't want filesystem operations in the
derivedStateOf
expression; those are intended to be very quick and that looks like a thread-blocking operation. Chances are you want something like this in StateModel:
Copy code
var files by mutableStateOf<List<File>>(emptyList())
  private set

// ...

suspend fun keepFilesUpdated() = withContext(<http://Dispatchers.IO|Dispatchers.IO>) {
  snapshotFlow { path }
    .mapNotNull {
      File(it).listFiles()
        ?.filter { !it.isHidden && it.isFile }
        ?.toList()
        ?.sortedBy { it.name }
    }
    .collect { files = it }
}
and then call that method in a
LaunchedEffect(state)
block similar to the other one that's there already. It's a little more complicated since now there's other async operations to manage the scope of, but
LaunchedEffect
is pretty effective for that.
n
1. good catch about the import! i didn’t understand why the samples didn’t work 😅 2.
tracker
is still necessary to let compose know that it should trigger the read from the FS again. if i remove it, it doesn’t work 3. i understand your point. i’ll check but for my current needs, it’s good enough thanks again for your help