I tried to implement a chat application using socket connection in Kotlin and TornadoFX library to make the GUI.
The problem comes when I try to launch the client because it keeps waiting a message from the Server although I put that code that update the label and receive the message inside a runAsync. I red the TornadoFX documentation and saw youtube videos but I cannot come to the solution.
I know that the issue is that the program is stuck in that block but can't figure how to do it.
class MyFirstView: View("Chat"){
var input: TextField by singleAssign()
var test = SimpleStringProperty()
val client: Client by inject()
init {
client.connect()
val t = thread(true) {
while (true) {
random = client.getMessage()
println(random)
Platform.runLater { test.set(random) }
}
}
}
override val root = vbox {
hbox {
label(test) {
bind(test)
}
}
hbox {
label("Write here some text")
input = textfield()
}
hbox {
button("Send") {
action{
client.writer.println(input.text)
}
}
}
}
}
You can only update UI elements on the UI thread, so if you want to manipulate the UI from a background thread, you need to wrap that particular code in runLater { }.
On another note, you shouldn't manipulate the text of the textfield or store ui element references with singleAssign. Instead you should bind your textfield to a StringProperty and manipulate the value instead. This is covered in the guide, so check it out :)
Related
I have created a custom UI for player an audio file and I am using Exoplayer to get the file and play it. I am not using the custom controller for exoplayer and my UI has a Slider that needs to update based on the current audio position. How can I achieve this? Please help.
var currentValue by remember { mutableStateOf(0f) }
currentValue = mediaPlayer.getCurrentPosition()
Slider(
modifier = Modifier.weight(1f),
value = currentValue ,
onValueChange = {currentValue = it },
valueRange = 0f.. mediaPlayer.contentDuration()
)
According to your code, you're expecting mediaPlayer.getCurrentPosition() to trigger recomposition somehow.
Compose can only track State object changes, which a special type created to trigger recompositions.
When you need to work with some non Compose library, you need to search for the way to track changes. In most of old libraries there're some listeners for such case.
In case of ExoPlayer there's no direct listener. In this issue you can see suggestion to use the listener to track isPlaying state. In Compose to work with listeners you can use DisposableEffect so when the view disappears you can remove the listener.
And then when it's playing - call currentPosition repeatedly with some interval. As Compose is built around Coroutines, it's pretty easy to do it with LaunchedEffect:
var currentValue by remember { mutableStateOf(0L) }
var isPlaying by remember { mutableStateOf(false) }
DisposableEffect(Unit) {
val listener = object : Player.Listener {
override fun onIsPlayingChanged(isPlaying_: Boolean) {
isPlaying = isPlaying_
}
}
mediaPlayer.addListener(listener)
onDispose {
mediaPlayer.removeListener(listener)
}
}
if (isPlaying) {
LaunchedEffect(Unit) {
while(true) {
currentValue = mediaPlayer.currentPosition
delay(1.seconds / 30)
}
}
}
I set up navigation, pagination and use flow to connect ui with model. If simplify, my screen code looks like this:
#Composable
MainScreen() {
val listState = rememberLazyListState()
val lazyItems = Pager(PagingConfig(...)) { ... }
.flow
.cachedIn(viewModelScope)
.collectAsLazyPagingItems()
LazyColumn(state = listState) {
items(lazyItems, key = { it.id }) { ... }
}
}
And here is my NavHost code:
NavHost(navController, startDestination = "mainScreen") {
composable("mainScreen") {
MainScreen()
}
}
But when i navigate back to MainScreen from another screen or just opening the drawer, data is loaded from DataSource again and i see noticeable blink of LazyColumn.
How to avoid reloading data?
Your code gives me the following error for cachedIn:
Flow operator functions should not be invoked within composition
You shouldn't ignore such warnings.
During transition Compose Navigation recomposes both disappearing and appearing views many times. This is the expected behavior.
And your code creates a new Pager with a new flow on each recomposition, which is causing the problem.
The easiest way to solve it is using remember: it'll cache the pager flow between recompositions:
val lazyItems = remember {
Pager(PagingConfig(/* ... */)) { /* ... */ }
.flow
.cachedIn(viewModelScope)
.collectAsLazyPagingItems()
}
But it'll still be reset during configuration change, e.g. device rotation. The best way to prevent this is moving this logic into a view model:
class MainScreenViewModel : ViewModel() {
val pagingFlow = Pager(PagingConfig(/* ... */)) { /* ... */ }
.flow
.cachedIn(viewModelScope)
}
#Composable
fun MainScreen(
viewModel = viewModel<MainScreenViewModel>()
) {
val lazyItems = viewModel.pagingFlow.collectAsLazyPagingItems()
}
How can I get a reference to the first tab? And furthermore how do I get its Stage?
class MainApp : App() {
override val primaryView = MainView::class
class MainView : View() {
override val root = VBox()
init {
with(root) {
tabpane {
tab("Report") {
hbox {
// TODO Want a reference to this tab here.
// Ideally something like tab.getStage()
this += Button("Hello 1")
}
}
tab("Data Entry") {
hbox {
this += Button("Hello 2")
}
}
}
}
}
}
}
Quickly: I've seen a lot of your posts here and they're pretty basic questions. These are things you could figure out on your own if you did your own digging. I'd recommend at least looking at the official guide to get a good grasp on most of what you need to know. Then, check out other posts on here to see if they've been answered already.
But to answer your question:
class MainView : View() {
override val root = vbox {
tabpane {
tab("Report") {
hbox {
val tab = this#tab //Here is your tab
button("Hello 1")
}
}
tab("Data Entry") {
hbox {
button("Hello 2")
}
}
}
}
}
Again, I would urge you to look at the guide, as you missed some helpful building tools (see how I built the buttons? see how I moved the root out of init?). I'd hate for you to code more than you need to then realize you could've done less work if you had known how.
Also: Tabs don't have references to stages. They just inherit Styleable and EventTarget, they're not like Views or Fragments.
I use kotlinx.serialization for my models.
I'd like the idea of them to not depend on JavaFX, so they do not expose properties.
Given a model, I want a tableview for a quick representation of a list of instances, and additionally a more detailed Fragment as an editor.
consider the following model:
#Serializable
data class Person(
var name: String,
var firstname: String,
var complex: Stuff)
the view containing the tableview contains
private val personlist = mutableListOf<Person>().observable()
with a tableview that opens an instance of PersonEditor for the selected row when Enter is pressed:
tableview(personlist) {
column("name", Person::name)
column("first name", Person::firstname)
setOnKeyPressed { ev ->
selectedItem?.apply {
when (ev.code) {
KeyCode.ENTER -> PersonEditor(this).openModal()
}
}
}
}
I followed this gitbook section (but do not want the modelview to be rebound on selection of another row within the tableview)
The editor looks about like this:
class PersonEditor(person: Person) : ItemFragment<Person>() {
val model: Model = Model()
override val root = form {
fieldset("Personal information") {
field("Name") {
textfield(model.name)
}
field("Vorname") {
textfield(model.firstname)
}
}
fieldset("complex stuff") {
//... more complex stuff here
}
fieldset {
button("Save") {
enableWhen(model.dirty)
action { model.commit() }
}
button("Reset") { action { model.rollback() } }
}
}
class Model : ItemViewModel<Person>() {
val name = bind(Person::name)
val firstname = bind(Person::firstname)
//... complex stuff
}
init {
itemProperty.value = mieter
model.bindTo(this)
}
}
When I save the edited values in the detail view, the tableview is not updated.
Whats the best practize to solve this?
Also I'm unsure, if what I'm doing can be considered good practize, so i'd be happy for some advice on that too.
The best practice in a JavaFX application is to use observable properties. Not doing so is an uphill battle. You can keep your lean domain objects, but add a JavaFX/TornadoFX specific version with observable properties. This object can know how to copy data to/from your "lean" domain objects.
With this approach, especially in combination with ItemViewModel wrappers will make sure that your data is always updated.
The setOnKeyPressed code you posted can be changed to:
setOnUserSelect {
PersonEditor(it).openModal()
}
Notice though, that you are not supposed to instantiate Views and Fragments directly, as doing so skips certain steps in the TornadoFX life cycle. Instead you should pass the person as a parameter, or create a new scope and inject a PersonModel into that scope before opening the editor in that scope:
setOnUserSelect {
find<PersonEditor>(Scope(PersonEditor(it)))
}
I have the following code:
class ExampleView :View("My Example view") {
val model:ExampleModel by inject()
override val root= vbox {
textfield(model.data)
button("Commit") {
setOnAction {
model.commit()
closeModal()
}
}
button("Rollback") {
setOnAction {
model.rollback()
closeModal()
}
}
button("Just quit") {
setOnAction {
closeModal()
}
}
}
}
class Example() {
var data by property<String>()
fun dataProperty() = getProperty(Example::data)
}
class ExampleModel(example: Example) : ItemViewModel<Example>() {
init {
item = example
}
val data = bind { item?.dataProperty() }
}
class MainView : View() {
val example:Example
override val root = BorderPane()
init {
example = Example()
example.data = "Data for example"
val exampleModel = ExampleModel(example)
with(root){
top {
menubar {
menu("Test") {
menuitem("Example - 1") {
val scope = Scope()
setInScope(exampleModel, scope)
find<ExampleView>(scope).openWindow()
}
menuitem("Example - 2") {
val scope = Scope()
setInScope(exampleModel, scope)
find<ExampleView>(scope).openWindow()
}
}
}
}
}
}
}
I have two questions for this example:
1) If i change the value and close the window without a commit (User can do this with help [X] button) then only ViewModel will store changes (and it will be displayed in GUI even after the re-opening ), but model POJO object will keep the old data.
if I used instance of Example class (without DI) then this instance received all the changes at once.
For example i don't want commit/rollback functionality but i want DI and immediate updating. What i should do? (ofcource i can call "commit" for "textfield change value event")
2) ViewModel has constructor with parameter and if i try open ExampleView like this
find<ExampleView>(Scope()).openWindow()
then I got an an obvious RuntimeException. Can I avoid this for example, by a compiler warnings (or by something else)?
1) This is the correct default behavior of the ViewModel. If you bind a property of the view model to an input, the changes are immediately reflected in that bound property, but will only be flushed into the underlying model object once you commit it.
If you want to autocommit changes in the view model properties back into the underlying model object, you can create the binding with the autocommit property set to true:
val data = bind(true) { item?.dataProperty() }
You can also write bind(autocommit = true) if that looks clearer to you. This will cause any changes to be automatically flushed back into the underlying object.
I also want to make you aware that by requiring an item in the constructor of your view model, you're effectively preventing it from being used with injection unless you prime it like you do using setInScope. This might be fine for your use case, but worth noting.
2) The upcoming TornadoFX 1.5.10 will give you a better runtime error message if you forget to pass a parameter. It also introduces default values for parameters. See https://github.com/edvin/tornadofx/pull/227 for more info.