Search code examples
pythonkivykivymdkivy-recycleview

CheckBox Action Repeats in a KivyMD RecycleView Grid


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()

Solution

  • 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.