Tech Daily

Setting up Hot Reload with KivyMD

Setting up Hot Reload with KivyMD

The KivyMD hot reload feature allows you to make changes to the Python code and kvlang files in your Kivy application and see the results in the running application without having to entirely stop and restart the application each time you make a change. Many other application development platforms support this type of feature, so it is nice to see it is available for developing Kivy/KivyMD applications as well.

But that said, I could not get it to work when I first tried using it. At the time, I was already in the middle of developing an application and didn't want to get side-tracked, so I put off enabling the hot reload feature for another time.

On my second try, I was determined to get it working with my now existing application. Several tutorials and videos are available for getting hot reloading set up with Kivy, but even with that I still struggled to make it functional. After reading documentation, digging into the code for the hot reload feature itself, watching a bunch of videos, reading several blog posts on the subject, and doing a lot of trial-and-error, I finally got it working. In the end, it really wasn't that difficult to set up, but there were some seemingly minor details that kept me from getting it set up correctly on the first try, and it took me longer to get working than it should have.

All that said, I'll document what I finally figured out here to hopefully help anyone who might fall into the same traps that I did. I'm going to assume that anyone reading this is already familiar with the basics of Kivy/KivyMD, so I won't go into much detail on general project setup. Instead, I will focus on just the specific changes that need to be made to enable the hot reload feature.

The Good (Key points for getting it to work)

The hot reload tool now built-in to KivyMD originated as a fork of Kaki. Many of the tutorials on using hot reload with Kivy that you may find are based on that library, but most of the content in them applies to the KivyMD tool as well.

Unlike the kaki Python library that automatically installs the dependencies it requires, with KivyMD you will need to pip install the watchdog library yourself if you use the KivyMD hot reload tool.

MDApp import

The first step in setting up your project for hot reloading, is to change where you import the MDApp Kivy application instance from. Typically, you would import MDApp using:

from kivymd.app import MDApp
Enter fullscreen mode Exit fullscreen mode

but to use the hot reload feature, you need to instead import MDApp like this:

from kivymd.tools.hotreload.app import MDApp
Enter fullscreen mode Exit fullscreen mode

MDApp.build_app() vs MDApp.build()

This is one of the significant points that I had originally missed. While Kivy and KivyMD normally use the build() method for initializing applications, the KivyMD hot reload tool (and Kaki) both instead use build_app() to perform that function. All of the tutorials I saw did indeed use build_app(), but that was a detail I had missed initially.

But even after I realized this, I still had questions. Did it replace the build() method, or was it in addition to the build() method? Through trial-and-error and digging into the source code, I determined that it essentially replaced the build() method that you would normally use. In fact, trying to override the App.build() method like you typically would, generally resulted in just an empty screen being displayed.

One side note about the build_app() method, the signature of that method is slightly different, and it needs to accept an argument in its definition that the normal build() method does not need:

def build_app(self, first=False):
    ...
Enter fullscreen mode Exit fullscreen mode

Lastly, the build_app() method must also use the ScreenManager object as its return value.

DEBUG

DEBUG is a class property that determines if the running instance should use the hot reload feature. Almost every tutorial I saw insisted that you must start the application from the command line after setting this as an environment variable with DEBUG=1. Since I'm using PyCharm, I instead set it up as part of the run configuration for the project. What I didn't expect was that creating another run configuration with DEBUG=0 would still enable the hot reloading feature. Instead, I had to leave the DEBUG environment variable out of the non-debug run configuration altogether in order to actually disable hot reloading.

You also have the option of hard coding the DEBUG flag in your instance of MDApp, or reading it from a dotenv file and then dynamically setting it as well.

KV_FILES, KV_DIRS & CLASSES

Depending on how your project is set up and what you want the hot reload tool to watch, you need to populate these lists and/or dictionary as properties in your subclass implementation of MDApp:

  • KV_FILES: list[str]
    A list of relative or fully qualified file names for each kv file that you want to watch:

    KV_FILES = ["./View/LoginScreen/login_screen.kv"]
    
  • KV_DIRS: list[str]
    A list of relative or fully qualified folder names for each directory containing kv files that you want to watch:

    KV_DIRS = ["./View/LoginScreen"]
    
  • CLASSES: dict[str:str]
    A dictionary of Python classes that you want to watch for changes. The dict key is the name of the class as a string, and the dict value is the module name as a string. For example, if you would normally import a particular class into a module, it might look like this:

    from View.LoginScreen.login_screen import Login
    

    To define an analogous CLASSES dictionary, you would create it like this:

    CLASSES = {"Login": "View.LoginScreen.login_screen"}
    

Note that you do not need to populate all three properties. Just utilize the ones that make sense for how your project is organized, and for what you want to watch for changes that trigger the hot reload.

Summary

So, the tl;dr for enabling the KivyMD hot reload tool ends up being this:

  • Make sure the Python watchdog library is installed
  • Use the right MDApp class
  • Override the right build method
  • Set the DEBUG property
  • Populate the variables that tell hot reload what to watch for changes

The Bad (Dealing with maintenance hassles)

Now that hot loading is working, I still didn't care for having to manually add classes and kv file names in the code every time I changed or added something. It's a task that I'm sure I would repeatedly forget to do all the time. So, I added a few helper functions that would do that for me automatically at run time, eliminating the manual code maintenance.

I already had a simple helper function that I was using for loading kv files where I would pass in the name of a module, and it would load a kv file by the same name:

def load_kv(module_name):  
    Builder.load_file(f"{os.path.join(*module_name.split('.'))}.kv")
Enter fullscreen mode Exit fullscreen mode

At the top of each view/screen module I created, I called this function, passing in the name of the current Python module:

load_kv(__name__)
Enter fullscreen mode Exit fullscreen mode

Because the hot loader tool manages loading and unloading kv files itself, I no longer needed to load them myself. In fact, if I did, Kivy would give me warnings about the kv file being loaded multiple times. I happen to know about this because I have personally seen them myself. So I changed the helper function to add the kv file to the KV_FILES list instead of directly calling Builder.load_file():

def load_kv(module_name):
    app = MDApp.get_running_app()
    app.KV_FILES.append(f"{os.path.join(*module_name.split('.'))}.kv")
Enter fullscreen mode Exit fullscreen mode

For the classes, I added a helper method to the sub-classed ScreenManager:

class SM(ScreenManager):  
    def get_classes(self):  
        return {screen.__class__.__name__: screen.__class__.__module__ for screen in self.screens}
Enter fullscreen mode Exit fullscreen mode

It loops through the list of screens that have been added to the ScreenManager object using a dictionary comprehension, adding each {class: module} to the dictionary. Then in the build_app() method, I call the get_classes() method after instantiating the ScreenManager:

screen_manager = SM()  
CLASSES = screen_manager.get_classes()
Enter fullscreen mode Exit fullscreen mode

With these two functions, I no longer have to do any manual editing in order to make sure any changes to the application are being watched by the hot reload tool.

The Ugly (Is it really worth it?)

State management on a hot reload can be tricky. When a screen reloads, it may lose its state. This is particularly true if you are relying on the UI to hold state, like text in a TextField widget. And then, non-UI state also needs to be considered. For example, what screen is currently being displayed, or whether the application is logged-in to a back-end REST server would need to be maintained. This may require you to rethink how state is updated and stored if you want to use hot reloading when developing your application.

One possible solution, is to manually reload state after a hot reload. There is a generic state object tied to the MDApp class as a property and an apply_state(state) method that gets called on every hot reload of the application. This method can be overridden to reapply any state that was lost due to the reload. However, as of right now, there is no hook implemented to give you a chance to save the application state before the hot reload starts.

To use these state management features, you basically need to serialize the state of your application to the MDApp.state object at the beginning of the rebuild process before the ScreenManager gets restarted, and then deserialize the state back to your application in the apply_state() method. For example:

class MainApp(MDApp):  
    DEBUG = True  
    sm = None  

    def build_app(self, first=False):  
        if self.sm is None:  
            self.state = {'current': 'one',  
                          'one': 'data one',  
                          'two': 'data two'}  
        else:  
            self.state = {'current': self.sm.current,  
                          'one': self.sm.get_screen('one').ids.data.text,  
                          'two': self.sm.get_screen('two').ids.data.text}  

        KV_FILES = []  
        self.sm = SM()  
        CLASSES = self.sm.get_classes()  

        return self.sm  

    def apply_state(self, state):  
        self.sm.current = state['current']  
        self.sm.get_screen('one').ids.data.text = state['one']  
        self.sm.get_screen('two').ids.data.text = state['two']
Enter fullscreen mode Exit fullscreen mode

I got around the current lack of a pre-reload hook for state by keeping my own reference to the ScreenManager object as self.sm. I pull the current state from that just before instantiating a new ScreenManager and save it to the MDApp.state object to be retrieved later in the apply_state() method.

Conclusion

While the hot reloading feature is very convenient, I found that I had to modify my code in some ways that I wouldn’t otherwise have to, just to make it work well with hot reloading. In some cases, it made the code more complicated and subjectively less readable, in my opinion. Refactoring your code so that state is organized and well managed is obviously a good thing. However, there may be situations where it could also over-complicate what may otherwise be a simple application. So that said, while it’s a cool feature, hot reloading may not always be worth the hassle or extra tricks you have to do with the code to get it to work reliably and consistently.

Links to working KivyMD projects demonstrating the hot reload configuration are included below.

Resources