Setting up Hot Reload with KivyMD
Tags: python, kivy, tutorial, mobile
Published: 2022-08-02 02:27:00

The [KivyMD](https://kivymd.readthedocs.io/en/latest/) hot reload feature allows you to make changes to the Python code and kvlang files in your [Kivy](https://kivy.org) 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](https://github.com/tito/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](https://pypi.org/project/kaki/) Python library that automatically installs the dependencies it requires, with KivyMD you will need to `pip install` the [watchdog](https://pypi.org/project/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:  
```python
from kivymd.app import MDApp
```  
but to use the hot reload feature, you need to instead import `MDApp` like this:  
```python
from kivymd.tools.hotreload.app import MDApp
```

### 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:  
```python
def build_app(self, first=False):
    ...
```
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:  
    ```python
    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:  
    ```python
    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:  
    ```python
    from View.LoginScreen.login_screen import Login
    ```  
    To define an analogous *CLASSES* dictionary, you would create it like this:  
    ```python
    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:
```python
def load_kv(module_name):  
    Builder.load_file(f"{os.path.join(*module_name.split('.'))}.kv")
```
At the top of each view/screen module I created, I called this function, passing in the name of the current Python module:
```python
load_kv(__name__)
```
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()`:
```python
def load_kv(module_name):
    app = MDApp.get_running_app()
    app.KV_FILES.append(f"{os.path.join(*module_name.split('.'))}.kv")
```
For the classes, I added a helper method to the sub-classed `ScreenManager`:
```python
class SM(ScreenManager):  
    def get_classes(self):  
        return {screen.__class__.__name__: screen.__class__.__module__ for screen in self.screens}
```
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`:
```python
screen_manager = SM()  
CLASSES = screen_manager.get_classes()
```
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:

```python
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']
```
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
- Kivy hot reload tool example code:
    https://github.com/JennaSys/kivy_hotreload/tree/hotreload  
  
- Code for a more complete(ish) Kivy Project with hot reload enabled:
    https://github.com/JennaSys/kivy_rest/tree/hot-reload  
  
- KivyMD Hot Reload Tool Documentation:
    https://kivymd.readthedocs.io/en/latest/api/kivymd/tools/hotreload/app