DataBinding with LiveData- after return to fragment from popbackstack, UI update not made - data-binding

I have a viewmodel with following methods:
private fun getCart(): LiveData<MenuCart?> {
return Transformations.switchMap(venueId) { venueId ->
venueId?.let {
repository.getMenuCart(it)
} ?: MutableLiveData<MenuCart?>(null)
}
}
fun getCartQty(): LiveData<Int> {
return Transformations.map(cartVal) {
it?.items?.count() ?: 0
}
}
Also have these fields defined inside view model:
val cartVal = getCart()
val cartQtyVal = getCartQty()
Then inside xml have this inside TextView:
android:text="#{viewModel.cartQtyVal.toString()}"
And data for xml defined as:
<data>
<variable
name="viewModel"
type="mypackage.viewmodels.VenueMealsViewModel" />
</data>
Inside fragment, have this:
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val _binding = LayoutVenueMealsMenuBinding.inflate(inflater, container, false)
_binding!!.lifecycleOwner = this
_binding!!.viewModel = this.viewModel
return _binding!!.root
}
I use a similar approach in a few places, and it works. But in this case, I have seen a bug where after navigating to another view, and then returning to this fragment, the UI does not update with the latest value of cartQtyVal. Any ideas why? Since the data binding approach is not working, I am temporarily not use data binding, and instead am observing the live data inside the fragment, which works robustly.

I think the main problem is that when the fragment is re-created after pop backstack, the switchmaps in my view model are not re-triggered. I had to move the setter for the input variable that drives the switchmap to inside the onViewCreated method.

Related

Spinner keeps last selected item

I have a spinner with an array adapter. The spinner is populated inside a fragment onCreateView().
spinner.setSelection(0)
spinner.onItemSelectedListener =
object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View?, pos: Int, id: Long) {
}
override fun onNothingSelected(var1: AdapterView<*>?) {
}
}
Whenever I get back to the fragment and the spinner is created, the last selected item is selected when onItemSelected() is called automatically and ignoring the spinner.setSelection(0) call.
I have put many logs to see what is going, but I cannot understand why the lately selected item is the one being selected by default and not the one at position 0.
I solved the issue by setting a click listener on the drop down view and basically do the same stuff I was doing with the OnItemSelectedListener.
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
val binding = SpinnerItemChartDropdownBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
val item = getItem(position)
val root = binding.root
bindDropdown(root, item)
binding.setClickListener {
listener.onChartRangeSelected(item)
}
return root
}
One important stuff. You need to do something like this, to dismiss the drop down view after an item has been selected:
fun hideSpinnerDropDown(spinner: Spinner) {
try {
val method: Method = Spinner::class.java.getDeclaredMethod("onDetachedFromWindow")
method.isAccessible = true
method.invoke(spinner)
} catch (e: java.lang.Exception) {
e.printStackTrace()
}
}

javafx binding from list property to arbitrary object property

I am trying to get a class to have a property bound to another class's list property, where the 1st property is derived from a summarizing calculation over the objects in the list. The code below is a simplified version of my production code. (The production code is doing a summary over DateTime objects -- the essential part of the code below is the binding between a list and an object property (here, it is a String for simplicity).)
I have tried various things. One approach was using addListener on the list in the Summary class below but I was running into weird bugs with the listener callback making updates on the Summary object. After doing a bunch of reading I think that a binding between the summary string and the list is more appropriate but I don't know exactly how to hook up the binding to the property?
package com.example.demo.view
import javafx.beans.Observable
import javafx.beans.binding.StringBinding
import javafx.beans.property.SimpleIntegerProperty
import javafx.beans.property.SimpleListProperty
import javafx.beans.property.SimpleStringProperty
import javafx.collections.FXCollections
import tornadofx.View
import tornadofx.button
import tornadofx.label
import tornadofx.vbox
class Thing(x: Int) {
val xProperty = SimpleIntegerProperty(x)
val yProperty = SimpleStringProperty("xyz")
}
class Collection {
private var things = FXCollections.observableList(mutableListOf<Thing>()) {
arrayOf<Observable>(it.xProperty)
}
val thingsProperty = SimpleListProperty<Thing>(things)
fun addThing(thing: Thing) {
things.add(thing)
}
}
class Summary(var collection: Collection) {
val summaryBinding = object : StringBinding() {
// The real code is more practical but
// this is just a minimal example.
override fun computeValue(): String {
val sum = collection.thingsProperty.value
.map { it.xProperty.value }
.fold(0, { total, next -> total + next })
return "There are $sum things."
}
}
// How to make this property update when collection changes?
val summaryProperty = SimpleStringProperty("There are ? things.")
}
class MainView : View() {
val summary = Summary(Collection())
override val root = vbox {
label(summary.summaryProperty)
button("Add Thing") {
summary.collection.addThing(Thing(5))
}
}
}
Keep in mind that I made this answer based on your minimal example:
class Thing(x: Int) {
val xProperty = SimpleIntegerProperty(x)
var x by xProperty
val yProperty = SimpleStringProperty("xyz")
var y by yProperty
}
class MainView : View() {
val things = FXCollections.observableList(mutableListOf<Thing>()) {
arrayOf<Observable>(it.xProperty)
}
val thingsProperty = SimpleListProperty<Thing>(things)
val totalBinding = integerBinding(listProperty) {
value.map { it.x }.fold(0, { total, next -> total + next })
}
val phraseBinding = stringBinding(totalBinding) { "There are $value things." }
override val root = vbox {
label(phraseBinding)
button("Add Thing") {
action {
list.add(Thing(5))
}
}
}
}
I removed your other classes because I didn't see a reason for them based on the example. If the collection class has more functionality than holding a list property in your real project, then add just add it back in. If not, then there's no reason to give a list its own class. The summary class is really just two bindings (or one if you have no need to separate the total from the phrase). I don't see the need to give them their own class either unless you plan on using them in multiple views.
I think your biggest problem is that you didn't wrap your button's action in action {}. So your code just added a Thing(5) on init and had no action set.
P.S. The var x by xProperty stuff will only work if you import tornadofx.* for that file.

Why won't my SwiftUI Text view update when viewModel changes using Combine?

I'm creating a new watchOS app using SwiftUI and Combine trying to use a MVVM architecture, but when my viewModel changes, I can't seem to get a Text view to update in my View.
I'm using watchOS 6, SwiftUI and Combine. I am using #ObservedObject and #Published when I believe they should be used, but changes aren't reflected like I would expect.
// Simple ContentView that will push the next view on the navigation stack
struct ContentView: View {
var body: some View {
NavigationLink(destination: NewView()) {
Text("Click Here")
}
}
}
struct NewView: View {
#ObservedObject var viewModel: ViewModel
init() {
viewModel = ViewModel()
}
var body: some View {
// This value never updates
Text(viewModel.str)
}
}
class ViewModel: NSObject, ObservableObject {
#Published var str = ""
var count = 0
override init() {
super.init()
// Just something that will cause a property to update in the viewModel
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.count += 1
self?.str = "\(String(describing: self?.count))"
print("Updated count: \(String(describing: self?.count))")
}
}
}
Text(viewModel.str) never updates, even though the viewModel is incrementing a new value ever 1.0s. I have tried objectWillChange.send() when the property updates, but nothing works.
Am I doing something completely wrong?
For the time being, there is a solution that I luckily found out just by experimenting. I'm yet to find out what the actual reason behind this. Until then, you just don't inherit from NSObject and everything should work fine.
class ViewModel: ObservableObject {
#Published var str = ""
var count = 0
init() {
// Just something that will cause a property to update in the viewModel
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.count += 1
self?.str = "\(String(describing: self?.count))"
print("Updated count: \(String(describing: self?.count))")
}
}
}
I've tested this and it works.
A similar question also addresses this issue of the publisher object being a subclass of NSObject. So you may need to rethink if you really need an NSObject subclass or not. If you can't get away from NSObject, I recommend you try one of the solutions from the linked question.

Tornadofx - How to pass parameter to Fragment on every instance

I am a newbie to javafx, kotlin and obviously tornadofx.
Issue:
How to pass parameters to Fragment on every instance?
Lets say I have a table view layout as my fragment. Now this fragment is used at multiple places but with different datasets.
eg.
Adding a fragment in:
class SomeView : View() {
...
root += SomeViewFragment::class
}
class SomeAnotherView : View() {
...
root += SomeViewFragment::class
}
Declaring Fragment:
class SomeViewFragment : Fragment() {
...
tableview(someDataSetFromRestApiCall) {
...
}
}
How can I pass different someDataSetFromRestApiCall from SomeView and SomeAnotherView ?
Let's start with the most explicit way to pass data to Fragments. For this TableView example you could expose an observable list inside the Fragment and tie your TableView to this list. Then you can update that list from outside the Fragment and have your changes reflected in the fragment. For the example I created a simple data object with an observable property called SomeItem:
class SomeItem(name: String) {
val nameProperty = SimpleStringProperty(name)
var name by nameProperty
}
Now we can define the SomeViewFragment with an item property bound to the TableView:
class SomeViewFragment : Fragment() {
val items = FXCollections.observableArrayList<SomeItem>()
override val root = tableview(items) {
column("Name", SomeItem::nameProperty)
}
}
If you later update the items content, the changes will be reflected in the table:
class SomeView : View() {
override val root = stackpane {
this += find<SomeViewFragment>().apply {
items.setAll(SomeItem("Item A"), SomeItem("Item B"))
}
}
}
You can then do the same for SomeOtherView but with other data:
class SomeOtherView : View() {
override val root = stackpane {
this += find<SomeViewFragment>().apply {
items.setAll(SomeItem("Item B"), SomeItem("Item C"))
}
}
}
While this is easy to understand and very explicit, it creates a pretty strong coupling between your components. You might want to consider using scopes for this instead. We now have two options:
Use injection inside the scope
Let the scope contain the data
Use injection inside the scope
We will go with option 1 first, by injecting the data model. We first create a data model that can hold our items list:
class ItemsModel(val items: ObservableList<SomeItem>) : ViewModel()
Now we inject this ItemsModel into our Fragment and extract the items from that model:
class SomeViewFragment : Fragment() {
val model: ItemsModel by inject()
override val root = tableview(model.items) {
column("Name", SomeItem::nameProperty)
}
}
Lastly, we need to define a separate scope for the fragments in each view and prepare the data for that scope:
class SomeView : View() {
override val root = stackpane {
// Create the model and fill it with data
val model= ItemsModel(listOf(SomeItem("Item A"), SomeItem("Item B")).observable())
// Define a new scope and put the model into the scope
val fragmentScope = Scope()
setInScope(model, fragmentScope)
// Add the fragment for our created scope
this += find<SomeViewFragment>(fragmentScope)
}
}
Please not that the setInScope function used above will be available in TornadoFX 1.5.9. In the mean time you can use:
FX.getComponents(fragmentScope).put(ItemsModel::class, model)
Let the scope contain the data
Another option is to put data directly into the scope. Let's create an ItemsScope instead:
class ItemsScope(val items: ObservableList<SomeItem>) : Scope()
Now our fragment will expect to get an instance of SomeItemScope so we cast it and extract the data:
class SomeViewFragment : Fragment() {
override val scope = super.scope as ItemsScope
override val root = tableview(scope.items) {
column("Name", SomeItem::nameProperty)
}
}
The View needs to do less work now since we don't need the model:
class SomeView : View() {
override val root = stackpane {
// Create the scope and fill it with data
val itemsScope= ItemsScope(listOf(SomeItem("Item A"), SomeItem("Item B")).observable())
// Add the fragment for our created scope
this += find<SomeViewFragment>(itemsScope)
}
}
Passing parameters
EDIT: As a result of this question, we decided to include support for passing parameters with find and inject. From TornadoFX 1.5.9 you can therefore send the items list as a parameter like this:
class SomeView : View() {
override val root = stackpane {
val params = "items" to listOf(SomeItem("Item A"), SomeItem("Item B")).observable()
this += find<SomeViewFragment>(params)
}
}
The SomeViewFragment can now pick up these parameters and use them directly:
class SomeViewFragment : Fragment() {
val items: ObservableList<SomeItem> by param()
override val root = tableview(items) {
column("Name", SomeItem::nameProperty)
}
}
Please not that this involves an unchecked cast inside the Fragment.
Other options
You could also pass parameters and data over the EventBus, which will also be in the soon to be released TornadoFX 1.5.9. The EventBus also supports scopes which makes it easy to target your events.
Further reading
You can read more about Scopes, EventBus and ViewModel in the guide:
Scopes
EventBus
ViewModel and Validation
I've been trying to figure this out recently, and that's what I got:
You need create button which will switch your components
button {
text = "open fragment"
action {
val params = Pair("text", MySting("myText"))
replaceWith(find<MyFragment>(params))
}
}
On second components
class MyFragment : Fragment("Test") {
var data = SimpleStringProperty()
override val root = hbox {
setMinSize(600.0, 200.0)
label(data) {
addClass(Styles.heading)
}
}
override fun onDock() {
data.value = params["text"] as String
}
}
As a result, we get the parameters in the second component

Share data in paged based interface in Watchkit

I need to share a string with all the pages from the first page.
I was trying to do this with didDeactive() but its not working.
Is it possible to even do this?
It's a bit difficult to tell what you are really trying to do, so I'm going to infer a bit. I "think" you are trying to set some shared value from the first page and be able to use that value in other pages. If that's the case, then you could do something like the following:
class SharedData {
var value1 = "some initial value"
var value2 = "some other initial value"
class var sharedInstance: SharedData {
struct Singleton { static let instance = SharedData() }
return Singleton.instance
}
private init() {
// No-op
}
}
class Page1InterfaceController: WKInterfaceController {
func buttonTapped() {
SharedData.sharedInstance.value1 = "Something new that the others care about"
}
}
class Page2InterfaceController: WKInterfaceController {
#IBOutlet var label: WKInterfaceLabel!
override func willActivate() {
super.willActivate()
self.label.setText(SharedData.sharedInstance.value1)
}
}
The SharedData object is a singleton class that is a global object. Anyone can access it from anywhere. In the small example I provided, the Page1InterfaceController is handling a buttonTapped event and changing the value property of the SharedData instance to a new value. Then, when swiping to Page2InterfaceController, the new value is being displayed in label.
This is a super simple example of sharing data between objects. Hopefully that helps get you moving in the right direction.

Resources