Search code examples
pythonuser-interfaceanimationkivykivymd

Exception KeyError when using MDHero


I am writing a python application using the kyvimd framework and encountered an incomprehensible error

Below is a model of the situation in which my problem occurs. This code works flawlessly until I clear and repopulate the GridLayout on screen A (these actions can be called using the appropriate buttons). After that, all the animation associated with the movement of the Heroes breaks. In addition, if we add a new Hero and click on it, screen B will not display the picture, and when going back, a KeyError exception will occur.

If you look at the children of screen A while the application is running, you can find MDHeroFrom with the corresponding tag. I couldn't fix this error

My Python file:

from kivymd.uix.hero import MDHeroFrom
from kivymd.uix.screen import MDScreen
from kivymd.uix.screenmanager import MDScreenManager
from kivymd.app import MDApp

from kivy.animation import Animation
from kivy.clock import Clock
from kivy.properties import ObjectProperty, DictProperty

hero_data = [
    {'name': 'obj1', 'address': 'aa'},
    {'name': 'obj2', 'address': 'bb'},
    {'name': 'obj3', 'address': 'cc'},
]
i = 3

class Card(MDHeroFrom):

    # argumnts
    info = DictProperty() 
    scr_manager = ObjectProperty()

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.tag = self.info['name']
        self.tile.ids.image.ripple_duration_in_fast = 0.05

    def on_transform_in(self, instance_hero_widget, duration):
        Animation(
            radius=[0, 0, 0, 0],
            box_radius=[0, 0, 0, 0],
            duration=duration,
        ).start(instance_hero_widget)

    def on_transform_out(self, instance_hero_widget, duration):
        Animation(
            radius=[24, 24, 24, 24],
            box_radius=[0, 0, 24, 24],
            duration=duration,
        ).start(instance_hero_widget)

    def on_release(self):
        """Switch screen and post data"""
        scr_b = self.scr_manager.scr_b
        scr_b.set_atr_obj(self.tag, self)

        def switch_screen(*args):
            self.scr_manager.current_heroes = [self.tag]
            scr_b.hero_to.tag = self.tag
            self.scr_manager.current = "scr_b"

        Clock.schedule_once(switch_screen, 0.2)


class ScreenB(MDScreen):
    name = "scr_b"

    def set_atr_obj(self, id_obj, hero):
        """Set attribute with id opening obj and remeber hero.
        :param:`id_obj` shared object's id
        :param:`hero` object of instance `MDHero`"""

        self.id_obj = id_obj
        self.hero_from = hero

    def go_back(self):
        """Returns to the page with the list of houses"""
        self.manager.current_heroes = [self.hero_to.tag]
        self.manager.current = "scr_a"


class ScreenA(MDScreen):
    box = ObjectProperty()
    name = 'scr_a'


class Container(MDScreenManager):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.scr_a = ScreenA()
        self.scr_b = ScreenB()
        self.add_widget(self.scr_a)
        self.add_widget(self.scr_b)


class HeroApp(MDApp):

    def __init__(self, **kwargs):

        self.data = {
            "Add new": [
                'plus',
                'on_release', self.add
            ],
            "Clear": [
                'delete-outline',
                'on_release', self.clear
            ],
            "Show": [
                'eye-outline',
                'on_release', self.show
            ]
        }

        super().__init__(**kwargs)

    def build(self):
        self.theme_cls.theme_style = "Dark"
        self.container = Container()
        return self.container

    def on_start(self):
        self.show()

    def add(self, btn=None):
        hero_data.append({'name': 'New obj', 'address': 'Someone'})
        self.clear()
        self.show()

    def clear(self, btn=None):
        self.container.scr_a.box.clear_widgets()

    def show(self, btn=None):
        box = self.container.scr_a.box
        for hero in hero_data:
            box.add_widget(Card(info= hero, scr_manager= self.container))


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

My KV file:

<Card>
    padding: 4
    size_hint: 0.5, None
    size_y: dp(200)
    radius: 24
    label_item: label_item
    tile: tile

    MDSmartTile:
        id: tile
        radius: 24
        box_radius: 0, 0, 24, 24
        box_color: 0, 0, 0, .5
        source: "testdata/house.jpg"
        size_hint: None, None
        size: root.size
        mipmap: True
        lines: 2
        on_release: root.on_release()

        TwoLineListItem:
            id: label_item
            text: f"[b]{root.info['name']}[/b]"
            secondary_text: root.info['address']
            pos_hint: {"center_y": .5}
            _no_ripple_effect: True


<ScreenB>

    heroes_to: [hero_to]
    hero_to: hero_to

    MDBoxLayout:
        orientation: "vertical"

        MDHeroTo:
            id: hero_to
            size_hint: 1, None
            height: dp(220)
            pos_hint: {"top": 1}

        MDLabel:
            text: 'Someone widgets'

    MDIconButton:
        icon: "arrow-left"
        pos_hint: {"top": 1, "right": .12}
        on_release: root.go_back()

<ScreenA>
    box: box

    MDGridLayout:
        id: box
        cols: 2

    MDFloatingActionButtonSpeedDial:
        data: app.data


Solution

  • I found what the problem is and fixed this error. The thing is, when you add MDHero to your application, KivyMD automatically adds some data about the Hero to memory when the application starts. And after when you clear the container and add Heroes there again, this special data will not be updated. That is why it is necessary to do this operation.

    class Container(MDScreenManager):
       ...
       def update_hero_data(self, widget):
           self._heroes_data = []
           self._create_heroes_data(widget)
    
    class HeroApp(MDApp):
       ...
       def clear(self):
           self.container.scr_a.box.clear_widgets()
           self.container.update_hero_data(self.container.scr_a)