2025, Dec 22 13:00

Rotating Images in Kivy Every Second: Fixing the White Screen by Updating the Correct Widget Instance

Learn why Kivy shows a white screen when rotating images with Clock: you're updating the wrong widget instance. See the fix using self.ids and timers in build

Rotating images in Kivy every second sounds straightforward: pick a random number, choose one of several files, update Image.source. Yet the UI stays blank. The root cause is subtle and very common for newcomers to Kivy — working with the wrong widget instance and updating an Image that isn’t actually on screen.

Problem setup

The goal is to cycle through three images using Clock, updating once per second. Below is a minimal example that looks reasonable at first glance but produces a white screen instead of animated faces.

# main.py
import random
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.image import Image
from kivy.clock import Clock
def pick_num():
    val = random.randint(1, 3)
    return val
class FacePanel(BoxLayout):
    def start_loop(self):
        Clock.schedule_interval(self.apply_sprite, 1)
    def pick_sprite(dummy_arg):
        n = pick_num()
        if n == 1:
            sprite_name = 'smile1.png'
        elif n == 2:
            sprite_name = 'smile2.png'
        elif n == 3:
            sprite_name = 'smile3.png'
        return sprite_name
    def apply_sprite(self, dt):
        self.img = FacePanel().ids.sprite
        self.img.source = self.pick_sprite()
        print(self.img.source)
        self.img.reload()
class ServoDemo(App):
    def __init__(self, **kwargs):
        super(ServoDemo, self).__init__(**kwargs)
        self.panel_ref = FacePanel()
        self.panel_ref.start_loop()
    def build(self):
        panel = FacePanel()
        return panel
if __name__ == "__main__":
    ServoDemo().run()
# servot.kv
#:kivy 2.3.1
<FacePanel>:
    orientation: 'vertical'
    canvas:
        Color:
            rgba: 1, 0.93, 0.75, 1
        Rectangle:
            pos: self.pos
            size: self.size
    Image:
        id: sprite
        size: self.size

What actually goes wrong

The logic creates and updates the wrong instances. Every time FacePanel() is called, a new, separate widget is constructed. The App.__init__ method instantiates a FacePanel and starts the timer there, but the widget returned by build is a different FacePanel. The scheduled timer keeps updating the Image of the off-screen instance, not the one attached to the window.

The same mistake repeats in apply_sprite: FacePanel().ids.sprite creates yet another fresh FacePanel to access ids, so the code changes the source of an Image that is never displayed.

There’s a smaller correctness issue too. The pick_sprite function uses a parameter it doesn’t need. It should rely on self only, then return the file name based on a random choice. Also, when changing Image.source, an explicit reload is not necessary in this scenario.

Fix: update the displayed instance, not a new one

The fix is to work with the actual widget instance that is on screen. Schedule the interval after creating that instance in build. Inside the widget, use self.ids to grab the Image from kv. Keep pick_sprite as a proper instance method with self only. With these changes, the source updates correctly and the UI shows the rotating images.

# main.py
import random
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.image import Image
from kivy.clock import Clock
def pick_num():
    val = random.randint(1, 3)
    return val
class FacePanel(BoxLayout):
    def start_loop(self):
        Clock.schedule_interval(self.apply_sprite, 1)
    def pick_sprite(self):
        n = pick_num()
        if n == 1:
            sprite_name = 'smile1.png'
        elif n == 2:
            sprite_name = 'smile2.png'
        elif n == 3:
            sprite_name = 'smile3.png'
        return sprite_name
    def apply_sprite(self, dt):
        img = self.ids.sprite
        img.source = self.pick_sprite()
        print(img.source)
class ServoDemo(App):
    def build(self):
        panel = FacePanel()
        panel.start_loop()
        return panel
if __name__ == "__main__":
    ServoDemo().run()
# servot.kv
#:kivy 2.3.1
<FacePanel>:
    orientation: 'vertical'
    canvas:
        Color:
            rgba: 1, 0.93, 0.75, 1
        Rectangle:
            pos: self.pos
            size: self.size
    Image:
        id: sprite
        size: self.size

Why this matters

Kivy’s widget tree is instance-based. The object you return from build is the one connected to the window. If timers, callbacks, or state changes target a separate instance, you’ll see no visible effect even though prints confirm changes are happening. Using self.ids within widget methods and starting timers on the actual displayed instance keeps the UI and logic in sync. It also keeps code predictable and easier to reason about.

Takeaways

Always schedule intervals and run UI updates on the same widget that is attached to the app root. Access kv elements via self.ids from within the widget instead of constructing new widgets. Keep method signatures aligned with how they are called, and avoid unnecessary reload calls when simply changing Image.source. This small set of habits prevents a whole class of “it prints updates but the screen is blank” bugs.

Thank you this was so helpful and it did work!

If you stick to updating the right instance and keep the ownership of timers clear, Kivy’s reactive updates behave as expected and your UI will animate without surprises.