kivykivy-language

Kivy App failing to load after adding screenmanager to handle the details


Writing a recipe app to play with, I got to the point of when you click on the recipe to see instructions/ingredients as a different screen it fails out completely. It originally was a popup but the formatting was an issue.

.kv file:

   ScreenManager:
    id: screen_manager
    RecipeTableScreen:
        name: 'recipe_table'
    RecipeDetailScreen:
        name: 'recipe_detail'

<RecipeTableScreen>:
    name: 'recipe_table'
    ScrollView:
        size_hint: (1, None)
        size: (root.width, root.height)
        GridLayout:
            id: recipe_layout
            cols: 3
            padding: 10
            spacing: 10
            size_hint_y: None
            height: self.minimum_height

            # Add headers
            Label:
                text: 'Name'
                bold: True
                size_hint_y: None
                height: 40
            Label:
                text: 'Category'
                bold: True
                size_hint_y: None
                height: 40
            Label:
                text: 'Rating'
                bold: True
                size_hint_y: None
                height: 40

<RecipeDetailScreen>:
    name: 'recipe_detail'
    BoxLayout:
        orientation: 'vertical'
        padding: 10
        spacing: 10
        Label:
            id: recipe_name
            text: ''
            bold: True
            font_size: 24
        Label:
            text: 'Ingredients:'
            bold: True
        BoxLayout:
            orientation: 'vertical'
            id: ingredients_box
            padding: 8
            spacing: 8
        Label:
            text: 'Instructions:'
            bold: True
        ScrollView:
            size_hint: (1, None)
            size: (root.width * 0.7, root.height * 0.4)
            Label:
                id: instructions_label
                text_size: (self.width, None)
                size_hint_y: None
                height: self.texture_size[1]
        Button:
            text: 'Back'
            size_hint_y: None
            height: 40
            on_release:
                app.root.current = 'recipe_table'

.py file:

    import json
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
from kivy.uix.scrollview import ScrollView
from kivy.uix.button import Button
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.widget import Widget
from kivy.properties import ObjectProperty, StringProperty

class RecipeTableScreen(Screen):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.load_recipes()

    def load_recipes(self):
        # Load recipes from JSON file
        with open('recipes.json', 'r') as file:
            recipes = json.load(file)
        
        # Load recipe details from JSON file
        with open('recipe_details.json', 'r') as file:
            self.recipe_details = json.load(file)
        
        layout = self.ids.recipe_layout

        # Add rows of recipes
        for recipe in recipes:
            button = Button(text=recipe['name'], size_hint_y=None, height=40)
            button.bind(on_release=lambda btn: self.show_recipe_details(btn.text))
            layout.add_widget(button)
            layout.add_widget(Label(text=recipe['category'], size_hint_y=None, height=40))
            layout.add_widget(Label(text=recipe['rating'], size_hint_y=None, height=40))

    def show_recipe_details(self, recipe_name):
        details = self.recipe_details.get(recipe_name, {})
        ingredients = details.get('ingredients', [])
        instructions = details.get('instructions', 'No instructions available.')

        recipe_detail_screen = self.manager.get_screen('recipe_detail')
        recipe_detail_screen.ids.recipe_name.text = recipe_name
        ingredients_box = recipe_detail_screen.ids.ingredients_box
        ingredients_box.clear_widgets()
        for ingredient in ingredients:
            ingredients_box.add_widget(Label(text=ingredient))
        recipe_detail_screen.ids.instructions_label.text = instructions

        self.manager.current = 'recipe_detail'

class RecipeDetailScreen(Screen):
    pass

class RecipeTableApp(App):
    def build(self):
        sm = ScreenManager()
        sm.add_widget(RecipeTableScreen(name='recipe_table'))
        sm.add_widget(RecipeDetailScreen(name='recipe_detail'))
        return sm

if __name__ == '__main__':
    RecipeTableApp().run()

Here is the error:

Traceback (most recent call last):
   File "kivy\\properties.pyx", line 961, in kivy.properties.ObservableDict.__getattr__
 KeyError: 'recipe_layout'

 During handling of the above exception, another exception occurred:

 Traceback (most recent call last):
   File "C:\Users\X\OneDrive\Recipe_App\RecipeAppv2.py", line 62, in <module>
     RecipeTableApp().run()
   File "C:\Users\X\OneDrive\Recipe_App\kivy_venv\Lib\site-packages\kivy\app.py", line 955, in run
     self._run_prepare()
   File "C:\Users\X\OneDrive\Recipe_App\kivy_venv\Lib\site-packages\kivy\app.py", line 924, in _run_prepare      
     self.load_kv(filename=self.kv_file)
   File "C:\Users\X\OneDrive\Recipe_App\kivy_venv\Lib\site-packages\kivy\app.py", line 697, in load_kv
     root = Builder.load_file(rfilename)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   File "C:\Users\X\OneDrive\Recipe_App\kivy_venv\Lib\site-packages\kivy\lang\builder.py", line 310, in load_file     return self.load_string(data, **kwargs)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   File "C:\Users\X\OneDrive\Recipe_App\kivy_venv\Lib\site-packages\kivy\lang\builder.py", line 412, in load_string
     self._apply_rule(
   File "C:\Users\X\OneDrive\Recipe_App\kivy_venv\Lib\site-packages\kivy\lang\builder.py", line 663, in _apply_rule
     child = cls(__no_builder=True)
             ^^^^^^^^^^^^^^^^^^^^^^
   File "C:\Users\X\OneDrive\Recipe_App\RecipeAppv2.py", line 15, in __init__
     self.load_recipes()
   File "C:\Users\X\OneDrive\Recipe_App\RecipeAppv2.py", line 26, in load_recipes
     layout = self.ids.recipe_layout
              ^^^^^^^^^^^^^^^^^^^^^^
   File "kivy\\properties.pyx", line 964, in kivy.properties.ObservableDict.__getattr__
 AttributeError: 'super' object has no attribute '__getattr__'. Did you mean: '__setattr__'?

If I add an attribute to super it failes out with 'super() takes no keywork arguments' I have absolutely no idea why this is failing out now, what am I missing here?


Solution

  • The problem is that you are trying to access the ids of the RecipeTableScreen before they are assigned. From the documentation:

    Note that the outermost widget applies the kv rules to all its inner widgets before any other rules are applied. This means if an inner widget contains ids, these ids may not be available during the inner widget’s init function.

    A simple fix is to delay the access until after the ids are assigned. The Clock.schedule_once() method is handy here. Try changing the __init__() method to:

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

    And a minor change to the load_recipes() method to handle the dt parameter that Clock.schedule_once() passes:

    def load_recipes(self, _dt):