I wanted to use the Kivy Accordion widget in my python app, but I couldn't get it to work right - the accordion items would expand or contract to exactly fill the space in the window. That's when I realized I had a bigger problem: the number of accordion items could increase indefinitely, but my accordion had no scroll bar. So, after some searching, I found the RecycleView stuff in Kivy. After looking at the online documentation, as well as the demo code in \Python311\share\kivy-examples\widgets\recycleview, I decided to grab basic_data.py and make some modifications. Here's my code: from random import sample, randint from string import ascii_lowercase from datetime import date from kivy.app import App from kivy.lang import Builder from kivy.properties import ObjectProperty, StringProperty from kivy.uix.boxlayout import BoxLayout from kivy.uix.label import Label from kivy.uix.recycleview.views import RecycleKVIDsDataViewBehavior kv = """ <Row>: heading: '' BoxLayout: orientation: 'horizontal' Button: background_normal: '' background_color: 0.3, 0.4, 0.3, 1 text: root.heading on_press: root.expand() <Test>: canvas: Color: rgba: 0.3, 0.3, 0.3, 1 Rectangle: size: self.size pos: self.pos rv: rv orientation: 'vertical' GridLayout: cols: 3 rows: 2 size_hint_y: None height: dp(108) padding: dp(8) spacing: dp(16) Button: text: 'Populate list' on_press: root.populate() Button: text: 'Sort list' on_press: root.sort() Button: text: 'Clear list' on_press: root.clear() BoxLayout: spacing: dp(8) Button: text: 'Insert new item' on_press: root.insert(new_item_input.text) TextInput: id: new_item_input size_hint_x: 0.6 hint_text: 'heading' padding: dp(10), dp(10), 0, 0 BoxLayout: spacing: dp(8) Button: text: 'Update first item' on_press: root.update(update_item_input.text) TextInput: id: update_item_input size_hint_x: 0.6 hint_text: 'new heading' padding: dp(10), dp(10), 0, 0 Button: text: 'Remove first item' on_press: root.remove() RecycleView: id: rv scroll_type: ['bars', 'content'] scroll_wheel_distance: dp(114) bar_width: dp(10) viewclass: 'Row' RecycleBoxLayout: default_size: None, dp(56) default_size_hint: 1, None size_hint_y: None height: self.minimum_height orientation: 'vertical' spacing: dp(2) """ Builder.load_string(kv) class Row(RecycleKVIDsDataViewBehavior, BoxLayout): is_expanded = ObjectProperty(None) heading = StringProperty(None) label = ObjectProperty(None) orientation = 'vertical' def expand(self): print(f"Row with heading '{self.heading}' has been expanded!") if self.is_expanded: self.is_expanded = False self.remove_widget(self.label) self.height -= self.label.height else: self.is_expanded = True self.label = Label(text='Expanded data ...', markup=True) self.add_widget(self.label) self.height += self.label.height class Test(BoxLayout): def populate(self): self.rv.data = [ {'heading': date.today().__str__() + ' ' + str(randint(0, 2000))} for x in range(50)] def sort(self): self.rv.data = sorted(self.rv.data, key=lambda x: x['heading']) def clear(self): self.rv.data = [] def insert(self, heading): self.rv.data.insert(0, { 'name.text': heading or 'default heading', 'heading': 'unknown'}) def update(self, heading): if self.rv.data: self.rv.data[0]['name.text'] = heading or 'default new heading' self.rv.refresh_from_data() def remove(self): if self.rv.data: self.rv.data.pop(0) class TestApp(App): def build(self): return Test() # def expand(self, row): # row.expand() if __name__ == '__main__': TestApp().run() When I ran the code, I was able to populate the RecycleView. I had changed the definition of Row to include a button for expanding and contracting an individual Accordion item. When expanded, a Label widget is added to the Row. When contracted, the Label is removed. So far so good... But when expanded, the Row item did not increase in height - at least at first. So the Button and Label simply became more compact: I wanted the Row to expand, so the Label would appear below the Button, each at normal size. As I continued to play with it, I realized that it was actually inconsistent in its behavior: One time the Label would not be shown at all, but the button would be twice as high. Another time it would appear as I had hoped! I have no idea how to guarantee consistent, hoped-for behavior. I tried making changes to the code... I made a small change at the App level to hang on to the main layout, so I could get access to the RecycleView later: class TestApp(App): testLayout = ObjectProperty(None) def build(self): testLayout = Test() return testLayout Then I changed the code for expanding the Row: def expand(self): if self.is_expanded: self.is_expanded = False self.remove_widget(self.label) self.height -= self.label.height print(f"Row with heading '{self.heading}' has been contracted!") else: self.is_expanded = True self.label = Label(text='Expanded data ...', markup=True, size_hint_y=None, height=dp(50)) self.add_widget(self.label) self.height += self.label.height print(f"Row with heading '{self.heading}' has been expanded!") print(f"Label height '{self.label.height}' Row height '{self.height}' ") # Use Clock to schedule refresh after the layout update Clock.schedule_once(self.refresh_view, 0) def refresh_view(self, *args): rv = self.get_recycleview_parent() if rv: rv.refresh_from_data() def get_recycleview_parent(self): parent = self.parent while parent and not isinstance(parent, RecycleView): parent = parent.parent return parent I tried adding the label height to the Row height when expanded, but subtracting the label height from the Row height when contracted. Then I got the RecycleView from the main layout and called refresh_from_data() to readjust everything to the new reality. That didn't work. I couldn't get the RecycleView that way. That's when I added the call to get_recycleview_parent() to get the RecycleView. Now, clicking on a row causes multiple, random rows to expand, inconsistently, and the program hangs. Can anyone offer suggestions on how to get the desired behavior (in contrast to the ineffective guesses from Copilot)? Continue reading...