0

When the checkbox for an item is clicked/unclicked in a recycleview grid, the click/unclick also automatically repeats for other data items in the grid. Why is this happening? The code below is a minimum working example. Thanks.

from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.recycleview import RecycleView
from kivy.properties import StringProperty, ListProperty
from kivy.clock import Clock

from kivymd.app import MDApp
from kivymd.uix.imagelist import SmartTile
from kivymd.uix.selectioncontrol import MDCheckbox

Builder.load_string("""
<Check>:

<GridTile>:
    SmartTile:
        source: root.tile
        size_hint_y: None
        height: '150dp'
        Check:

<GridScreen>:
    name: 'grid_screen'
    RV:
        id: rv
        viewclass: 'GridTile'
        RecycleGridLayout:
            cols: 2
            size_hint_y: None
            default_size: 1, dp(150)
            default_size_hint: 1, None
            height: self.minimum_height
""")

class GridTile(Screen):
    tile = StringProperty('')

class GridScreen(Screen):
    pass

class RV(RecycleView):
    data = ListProperty('[]')

    def __init__(self, **kwargs):
        super(RV, self).__init__(**kwargs)
        self.cell_data()

    def cell_data(self):
        self.data = [{"tile": 'The Beatles'} for i in range(41)]

class Check(SmartTile):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        Clock.schedule_once(self.add_checkbox)

    def add_checkbox(self, interval):
        app = MDApp.get_running_app()
        self.check = MDCheckbox(size_hint=(None, None), size=(48, 48))
        self.check.bind(active=app.on_checkbox_active)
        self._box_overlay.add_widget(self.check)

class ThisApp(MDApp):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)

    def build(self):
        self.sm = ScreenManager()
        self.sm.add_widget(GridScreen(name='grid_screen'))
        return self.sm

    def on_checkbox_active(self, checkbox, value):
        if value:
            print('The checkbox', checkbox, 'is active', 'and', checkbox.state, 'state')
        else:
            print('The checkbox', checkbox, 'is inactive', 'and', checkbox.state, 'state')

if __name__ == "__main__":
    ThisApp().run()
Vadim Kotov
  • 8,084
  • 8
  • 48
  • 62
Henry Thornton
  • 4,381
  • 9
  • 36
  • 43

2 Answers2

0

The RecycleView works by recycling a minimal number of instances of the viewclass, which is GridTile in your case. The RecycleView assigns properties to those instances of GridTile based on the entries in the data. If you change any properties of GridTile, or its children, that are not handled in the data, then the RecycleView is unaware of those changes and those changes remain in the recycled instances of GridTile. So, if you want the MDCheckBox state to be handled correctly, you must include it in your data as another property of GridTile. The fact that your MDCheckBox is not in your kv, makes this much more difficult to accomplish. This answers the why question.

John Anderson
  • 35,991
  • 4
  • 13
  • 36
  • The reason MDCheckBox is placed outside of the kv is because the folks on the kivymd board said to do it - something do with the _box_overlay id in SmartTile. It didn't make sense but here we are. Originally, the MDCheckBox was in the kv and I just tested it again with it inside the kv and the checkbox action repeats as before. What is the method to solve this? Btw, thank-you for your generously clear answer. – Henry Thornton Jul 05 '21 at 20:32
  • The MDCheckBox code in the kv: : id: _box_overlay size_hint: None, None size: dp(48), dp(48) on_active: app.on_checkbox_active(*args) : SmartTile: source: root.tile size_hint_y: None height: '150dp' Check: – Henry Thornton Jul 05 '21 at 20:35
0

Here is a modified version of your original posted code. This version works, but there is some interaction between GridTile instances (when you click on one check box, another GridTile appears to refresh itself). I have only seen this interaction with KivyMd. Writing a similar app without KivyMD does not display that odd interaction.

from functools import partial

from kivy.lang import Builder
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.recycleview import RecycleView
from kivy.properties import StringProperty, ListProperty, NumericProperty, ObjectProperty
from kivy.clock import Clock

from kivymd.app import MDApp
from kivymd.uix.imagelist import SmartTile
from kivymd.uix.selectioncontrol import MDCheckbox

Builder.load_string("""
<GridTile>:
    SmartTile:
        source: root.tile
        size_hint_y: None
        height: '150dp'
        Check:
            id: ck
            root_ref: root  # creat reference to containing GridTile

<GridScreen>:
    name: 'grid_screen'
    RV:
        id: rv
        viewclass: 'GridTile'
        RecycleGridLayout:
            cols: 2
            size_hint_y: None
            default_size: 1, dp(150)
            default_size_hint: 1, None
            height: self.minimum_height
""")

class GridTile(Screen):
    # properties to be set in the rv.data
    tile = StringProperty('')
    index = NumericProperty(-1)
    cb_state = StringProperty('normal')

    def __init__(self, **kwargs):
        super(GridTile, self).__init__(**kwargs)
        self.bind(cb_state=self.set_cb_state)  # bind the cb_state property to set the state of the MDCheckBox

    def set_cb_state(self, gridtile, cb_state_value):
        self.ids.ck.check.state = cb_state_value  # actually set the state of the MDCheckBox

class GridScreen(Screen):
    pass

class RV(RecycleView):
    data = ListProperty('[]')

    def __init__(self, **kwargs):
        super(RV, self).__init__(**kwargs)
        self.cell_data()

    def cell_data(self):
        self.data = [{"tile": 'The Beatles', "index": i, "cb_state": 'normal'} for i in range(41)]

class Check(SmartTile):
    root_ref = ObjectProperty(None)  # reference to the containing GridTile (set by kv)

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        Clock.schedule_once(self.add_checkbox)

    def add_checkbox(self, interval):
        app = MDApp.get_running_app()
        self.check = MDCheckbox(size_hint=(None, None), size=(48, 48))
        self.check.bind(on_press=partial(app.on_checkbox_press, self))  # bind to on_press to avoid possible looping when active is changed
        self._box_overlay.add_widget(self.check)

class ThisApp(MDApp):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)

    def build(self):
        self.sm = ScreenManager()
        self.sm.add_widget(GridScreen(name='grid_screen'))
        return self.sm

    def on_checkbox_press(self, check, checkbox):
        new_state = checkbox.state

        # set checkbox state back to the default
        checkbox.state = 'normal'  # avoids setting checkbox state without data

        rv = self.root.get_screen('grid_screen').ids.rv
        rv.data[check.root_ref.index]['cb_state'] = new_state
        rv.refresh_from_data()  # set the state from data

if __name__ == "__main__":
    ThisApp().run()

Th gist of the modifications is the adding of the index and cb_state properties to the GridTile class and to the data. The index property is just used as the index into the data when adjusting the data. And the cb_state is the state of the MDCheckbox. Since the MDCheckbox does not appear in the kv, there is no automatic binding if the cb_state property to the actual state of the MDChckbox, so that binding is explicitly created in the GridTile class. Also, the binding of the MDCheckbox to update the data is changed to bind to on_press, rather than on_active, since the active property will be changed by the RecycleView based on the data and could result in a looping effecet.

John Anderson
  • 35,991
  • 4
  • 13
  • 36