Hello all,
I am currently working on a small demonstrator using RecycleView. I’m having a bit of a hard time understanding how the data manipulation really works. I think I know that you should only use rv.data when you want to manipulate the content of widgets inside a “view”. If a “view” contains more complex widgets, it can be a bit difficult to use only this data list, but that’s another story.
The point is that it seems that sometimes it is unavoidable to use the refresh_from_data method, especially when widget contents change, views are deleted, views are moved, etc…. As soon as I do that, however, a somewhat strange behavior shows up. Below is the extended standard example for selectable rows in a recycleview that shows this behavior.
Two widgets per view were used. Both text properties are reflected in the data list. However, as soon as I call refresh_from_data, the widget content is mirrored from the spinner button to the other end of the visible view. Funnily enough, though, the label content stays where it belongs.
Maybe someone can help me to understand and maybe there is a simple workaround for this.
the kivy version is 2.1.0, python is 3.11.2
I think a similar question is discussed in:
I also tried several things without success, including writing with a spinners callback directly to rv.data to force a sync between widget and data content. Probably I did it wrong, so I leave it to you to guide me on the right track.
thanks in advance
Below find an example of a simple recycleview, where I attempt to explain some of the behavior.
The reason your spinner is not working properly is because you are relying on the widget to maintain state, this won’t work. It helps to understand how RecycleView works: Every time a widget is visible in the view, the visible widget will apply the list of attributes from the items in the data list, to that widget. Of course, the binding applies, so keeping a selected state in the widget doesn't work. For the spinner to work properly you need to save the state of the spinner outside of the widget. You want the (recycled) widget to be set/reset when the widget is used for another data item so you have to save that selected state outside of the widget. One possible solution is to edit the items in data(the RecycleView data attribute), but that could trigger new dispatches and so reset which widgets displays which items, and cause trouble. The preferred solution is to save the widget state to a different list property, and just make the widget lookup that property when the widget’s key is updated.
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.recycleview import RecycleView
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import StringProperty, ListProperty
kv = '''
<TwoButtons>:
# This class is used as the viewclass in the RecycleView
# The means this widget will be instanced to view one element of data from the data list.
# The RecycleView data list is a list of dictionaries. The keys in the dictionary specify the
# attributes of the widget.
Button:
text: root.left_text
on_release: print(f'Button {self.text} pressed')
Button:
text: root.right_text
on_release: print(f'Button {self.text} pressed')
BoxLayout:
orientation: 'vertical'
Button:
size_hint_y: None
height: 48
text: 'Add widget to RV list'
on_release: rv.add()
RV: # A Reycleview
id: rv
viewclass: 'TwoButtons' # The view class is TwoButtons, defined above.
data: self.rv_data_list # the data is a list of dicts defined below in the RV class.
scroll_type: ['bars', 'content']
bar_width: 10
RecycleBoxLayout:
# This layout is used to hold the Recycle widgets
default_size: None, dp(48) # This sets the height of the BoxLayout that holds a TwoButtons instance.
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height # To scroll you need to set the layout height.
orientation: 'vertical'
'''
class TwoButtons(BoxLayout): # The viewclass definitions, and property definitions.
left_text = StringProperty()
right_text = StringProperty()
class RV(RecycleView):
rv_data_list = ListProperty() # A list property is used to hold the data for the recycleview, see the kv code
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.rv_data_list = [{'left_text': f'Left {i}', 'right_text': f'Right {i}'} for i in range(2)]
# This list comprehension is used to create the data list for this simple example.
# The data created looks like:
# [{'left_text': 'Left 0', 'right_text': 'Right 0'}, {'left_text': 'Left 1', 'right_text': 'Right 1'},
# {'left_text': 'Left 2', 'right_text': 'Right 2'}, {'left_text': 'Left 3'},...
# notice the keys in the dictionary correspond to the kivy properties in the TwoButtons class.
# The data needs to be in this kind of list of dictionary formats. The RecycleView instances the
# widgets, and populates them with data from this list.
def add(self):
l = len(self.rv_data_list)
self.rv_data_list.extend(
[{'left_text': f'Added Left {i}', 'right_text': f'Added Right {i}'} for i in range(l, l + 1)])
class RVTwoApp(App):
def build(self):
return Builder.load_string(kv)
RVTwoApp().run()
--
You received this message because you are subscribed to the Google Groups "Kivy users support" group.
To unsubscribe from this group and stop receiving emails from it, send an email to kivy-users+...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/kivy-users/bcdefb0a-8c4d-4fe2-94f9-4cd5f945bdddn%40googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/kivy-users/74c4e61f-fb7b-4824-9125-a780f64af304n%40googlegroups.com.
One step further again. I have now adjusted the code so that a “refresh_form_data” no longer causes collateral damages .. call it a solution. Probably there are easier ways to achieve this. If you have any thoughts on this, I would be very grateful.
Just briefly to document this one possible solution of the problem:
as ElliotG already described very well, you should not want to change widgets directly, because a “refresh_form_data” messes up the list “data” and the children order. This is also necessary if the number of rows is larger than the visible area. Nonetheless, in this special case I need to manipulate the widget contents manually — see below.
the “text-change” event of the spinner button was redirected to a new function. In this function, the corresponding record in “data” is set accordingly. However, you have to take care that the record is only set directly after the selection with the spinner button. Immediately after altering the record the other widgets must be updated manually with the contents of “data” (“refresh_from_data” apparently did not consider such a case). To prevent this from happening unnecessarily too often, “mask-event” is used. Also it helps to prevent recursions, because “text-change” events can occur again during this manual update.
“refresh_from_data” can now be used without problems.
For a simple list this sounds a way too complicated, doesn’t it? But I think it is at least a solution …
.. here the code: