So, I have a case in which I need to have N rows in form of: Label TextView/Checkbox. Maybe I will have to have more than those two views, so I want to be able to support anything that is TornadoFx View.
I've created an interface that has one method that returns TornadoFx View and it looks like this:
interface ValueContainer {
fun getView() : View
}
One of the implementations looks like this:
class BooleanValueContainer(val checked: Boolean) : ValueContainer {
val valueProperty = SimpleBooleanProperty(checked)
override fun getView(): View {
return (object : View() {
override val root = checkbox {
bind(valueProperty)
}
})
}
}
Now, when I try to use it inside init block, it doesn't show in the layout. root is GridPane and parameters is a list of objects that have name and reference to ValueContainer implementation (BooleanValueContainer or other one which I haven't shown):
init {
with(root) {
parameters.map {
row(it.name) {
it.parameterContainer.getView()
}
}
}
}
I'm stuck here for quite a while and I've tried anything I could find but nothing really worked except putting textview or checkbox block instead of getView() call, but then I would have to have logic on what view should I show inside this class which represents a view and I don't want that.
The reason this is not working for you is that you simply call parameterContainer.getView() but you don't add the View to the row. I think what's confusing you is that for builders you can just say label() for example, and it's added to the current Node in the builder tree. In your case, you just say Label() (just create an instance of Label, not call the label builder), which would create a new Label, but not add it to the children list of the current Node. To solve your problem, do:
this += it.parameterContainer.getView()
This will add the View to the row.
Apart from this, I don't quite see the point of the ValueContainer. What does it solve to put a View inside this container object? I suspect this as well might be due to a misunderstanding and I'd like to understand why you feel that you need this construct.
Related
I have an activity which displays multiple fragments depending on which one is selected.
I also have a button in this activity and I want to obtain a value from a specific fragment when this button is clicked.
How can I obtain this value?
I tried to get the view I wanted from the fragment from the activity as the code shows below but I can understand that it doesn't work since the fragment is still to be created.
onOffButton.setOnClickListener {
if (onOffButton.text.contains("ON")) {
onOffButton.text = "TURN OFF"
var hoursPicker = findViewById<NumberPicker>(R.id.hoursPicker)
}
}
The short version is you shouldn't do this, there are all kinds of complications (especially when you're trying to access the Fragment's Views).
It's even more complicated if the Fragment might not even be added to the UI at all! If it's not there, what value are you supposed to use? If you want to somehow create the Fragment just so it exists, and so you can read the value from its text box, then that's a sign the value really needs to be stored somewhere else, so you don't need the Fragment if you want to access it.
The easiest, recommended, and modern way to share data like this is with a ViewModel:
class MyViewModel : ViewModel() {
// setting a default value here!
var currentHour: Int = 0
}
class MyActivity : AppCompatActivity() {
val model: MyViewModel by viewModels()
fun onCreate(...) {
...
onOffButton.setOnClickListener {
// access the data in the ViewModel
val currentHour = model.currentHour
}
}
}
class MyFragment : Fragment() {
// using activityViewModels so we get the parent Activity's copy of the VM,
// so we're all sharing the same object and seeing the same data
val model: MyViewModel by activityViewModels()
fun onViewCreated(...) {
...
hoursPicker.setOnValueChangeListener { _, _, newValue ->
// update the VM
model.currentHour = newValue
}
}
}
So basically, you have this ViewModel object owned by the Activity and visible to its Fragments. The VM outlives all of those components, so you don't lose data while an Activity is being destroyed on rotation, or when a Fragment isn't added to the UI, etc.
The VM is the source of data, everything else just reads from it, or updates it when something changes (like when the Fragment updates the variable when its number picker's value changes). This way, the Activity doesn't need to go "ask" the Fragment for info - it's stored in a central location, in the VM
This is the most basic way to use a ViewModel - you can start using LiveData and Flow objects to make different UI components observe data and react to changes too. For example, your button in your Activity could change some enabled state in the VM, and the Fragment (if it's added) will see that change and can do things like make the number picker visible or invisible.
It's way easier to coordinate this stuff with a ViewModel, so if you don't already know how to use them, I'd recommend learning it!
I'm fiddling around with this code where I have a base class Node which can be extended:
open class Node
class SubNode : Node()
Now, I have a Behavior class that can be attached to a node, and when this attachment happens, the behavior object is invoked:
open class Behavior {
fun attach(node: Node) {
println("Behavior was attached to a node")
}
}
open class Node {
var behavior: Behavior? = null
set(value) {
field = value
value.attach(this)
}
}
This works, but could this be generified in such way that the type of the attach method would always refer to the actual type of the attached Node? For instance, if the Behavior class was extended like this:
open class Behavior<NodeType: Node> {
open fun attach(node: NodeType) {
}
}
class SubBehavior : Behavior<SubNode>() {
override fun attach(node: SubNode) {
}
}
I've tried various ways of setting up the types in Node class, but can't figure any other way than passing the actual subclass type to the base class (which seems rather cumbersome):
open class Node<SubType: Node> {
var behavior: Behavior<SubType>? = null
}
class SubNode : Node<SubNode>()
Is there a way to do this in any other way?
I think what you need are self types, which don't exist in Kotlin (at least, not yet).
Using recursive generics like you did is the most common way around the problem.
That said, I have trouble understanding your use case here for intertwining these 2 classes together this way. Like how is behaviour used inside your node, etc.
I have stumbled upon a behaviour in TornadoFx that doesn't seem to be mentioned anywhere (I have searched a lot) and that I'm wondering about.
If I define a view like this with the TornadoFx builders for the labels:
class ExampleView: View() {
override
val root = vbox{ label("first label") }
val secondLabel = label("second label")
}
The result is:
That is, the mere definition of secondLabel automatically adds it to the rootof the scene.
However, if I place this definition BEFORE the definition of root...
class ExampleView: View() {
val secondLabel = Label("second label")
override
val root = vbox{ label("first label") }
}
... or if I use the JavaFx Labelclass instead of the TornadoFx builder ...
class ExampleView: View() {
override
val root = vbox{ label("first label") }
val secondLabel = Label("second label")
}
... then it works as I expect:
Of course, I can simply define all variables in the view before I define the rootelement but I'm still curious why this behaviour exists; perhaps I am missing some general design rule or setting.
The builders in TornadoFX automatically attach themselves to the current parent in the scope they are called in. Therefore, if you call a builder function on the View itself, the generated ui component is automatically added to the root of that View. That's what you're seeing.
If you really have a valid use case for creating a ui component outside of the hierarchy it should be housed in, you shouldn't call a builder function, but instead instantiate the element with it's constructor, like you did with Label(). However, the use cases for such behavior are slim to none.
Best practice is to store value properties in the view or a view model and bind the property to the ui element using the builders. You then manipulate the value property when needed, and the change will automatically update in the ui. Therefore, you very very seldom have a need to access a specific ui element at a later stage. Example:
val myProperty = SimpleStringProperty("Hello world")
override val root = hbox {
label(myProperty)
}
When you want to change the label value, you just update the property. (The property should be in an injected view model in a real world application).
If you really need to have a reference to the ui element, you should declare the ui property first, then assign to it when you actually build the ui element. Define the ui property using the singleAssign() delegate to make sure you only assign to it once.
var myLabel: Label by singleAssign()
override val root = hbox {
label("My label) {
myLabel = this
}
}
I want to stress again that this is very rarely needed, and if you feel you need it you should look to restructure your ui code to be more data driven.
Another technique to avoid storing references to ui elements is to leverage the EventBus to listen for events. There are plenty of examples of this out there.
I have created an ObservableObject in a View.
#ObservedObject var selectionModel = FilterSelectionModel()
I put a breakpoint inside the FilterSelectionModel's init function and it is called multiple times. Because this View is part of a NavigationLink, I understand that it gets created then and along with it, the selectionModel. When I navigate to the View, the selectionModel is created again.
In this same View I have a "sub View" where I pass the selectionModel as an EnvironmentObject so the sub-view can change it.
AddFilterScreen().environmentObject(self.selectionModel)
When the sub view is dismissed, the selectionModel is once more created and the changes made to it have disappeared.
Interesting Note: At the very top level is a NavigationView. IF I add
.navigationViewStyle(StackNavigationViewStyle())
to this NavigationView, my selectionModel's changes disappear. BUT if I do not add the navigationStyle, the selectionModel's changes made in the sub view remain!! (But I don't want a split nav view, I want a stacked nav view)
In both cases - with or without the navigationStyle, the selectionModel is created multiple times. I can't wrap my head around how any of this is supposed to work reliably.
Latest SwiftUI updates have brought solution to this problem. (iOS 14 onwards)
#StateObject is what we should use instead of #ObservedObject, but only where that object is created and not everywhere in the sub-views where we are passing the same object.
For eg-
class User: ObservableObject {
var name = "mohit"
}
struct ContentView: View {
#StateObject var user = User()
var body: some View {
VStack {
Text("name: \(user.name)")
NameCount(user: self.user)
}
}
}
struct NameCount: View {
#ObservedObject var user
var body: some View {
Text("count: \(user.name.count)")
}
}
In the above example, only the view responsible (ContentView) for creating that object is annotating the User object with #StateObject and all other views (NameCount) that share the object is using #ObservedObject.
By this approach whenever your parent view(ContentView) is re-created, the User object will not be re-created and it will persist its #State, while your child views just observing to the same User object doesn't have to care about its re-creation.
You can instantiate the observable object in the init method, in this way you will be able to hold its value or the value won't disappear.
Instantiate this way in the view file.
#ObservedObject var selectionModel : FilterSelectionModel
init() {
selectionModel = FilterSelectionModel(value : "value to be saved from disappearing")
}
Instantiate this way in the viewModel file.
class FilterSelectionModel : ObservableObject {
#Published var value : String
init(value : String) {
self.value = value
}
}
This is a workaround that I found, but still, the init method is called multiple times and I didn't get any success with this issue.
In order to stop multiple initializing of the ViewModels as the view is declared in the Navigation View and SwiftUI uses struct which is a value type, so eventually these are initialized before the view is presented, therefore you can convert that view into a LazyView, so that it will only be initialized once the view is about to be presented or shown.
// Use this to delay instantiation when using `NavigationLink`, etc...
struct LazyView<Content: View>: View {
var content: () -> Content
var body: some View {
self.content()
}
}
You can call it like this...
NavigationLink(destination: LazyView { ViewTobePresented() })
I want to have a window which shows info about certain ViewModel
Suppose you have a simple Person:
class Person(name: String) {
val nameProperty = SimpleStringProperty(name)
}
and have instance of Person save in property:
val personProperty = SimpleObjectProperty(Person("John"))
what's the correct solution to show Person's name in label?
Using this:
label(personProperty.value.nameProperty)
Will not update when I update the property's person:
personProperty.value = Person("Joe")
(That's obvious because only the reference changes, not the value itself)
So is there any good way to do this or do I have to manually add listeners for personProperty and update which Person does label point to?
EDIT:
I also found this question: JavaFX binding and property change, but it doesn't contain anything new and useful that I didn't know about, is there any TornadoFX-specific way of doing this?
This is exactly what the ItemViewModel does for you. If you want to make a binding for the name property that updates automatically, outside of an ItemViewModel, you can use the TornadoFX feature select:
val nameProperty = personProperty.select { it.nameProperty }
A listener can be attached to the property:
personProperty.onChange {
it?.nameProperty.let(nameLabel.textProperty().bind)
}
This can be wrapped in extension function to simplify the task.