Welcome to the final part of this series on creating a working Jabber client in Kivy. While there are numerous things we could do with our code to take it to Orkiv version 2, I think it’s currently in a lovely state that we can call “finished” for 1.0.
In this article, we’re going to talk about releasing and distributing Kivy to an Android mobile device. We’ll use a tool called buildozer that the Kivy developers have written. These commands will, in the future, extend to other operating systems.
We aren’t going to be writing a lot of code in this part (some adaptation to make it run on android is expected). If you’re more interested in deploying than coding, you can check out my state of the orkiv repository as follows:
git clone https://github.com/buchuki/orkiv.git git checkout end_part_eight
Remember to create and activate a virtualenv populated with the appropriate dependencies, as discussed in part 1.
Table Of Contents
Here are links to all the articles in this tutorial series:
- Part 1: Introduction to Kivy
- Part 2: A basic KV Language interface
- Part 3: Handling events
- Part 4: Code and interface improvements
- Part 5: Rendering a buddy list
- Part 6: ListView Interaction
- Part 7: Receiving messages and interface fixes
- Part 8: Different views for different screens
- Part 9: Deploying your kivy application
Restructuring the directory
Back in Part 1, we chose to put all our code in
__main__.py so that we could run it from a zip file or directly from the directory. In some ways, this is pretty awesome for making a package that can easily be distributed to other systems (assuming those systems have the dependencies installed). However, Kivy’s build systems presume that the file is called
I’m not overly happy about this. Kivy sometimes neglects Python best practices and reinvents tools that are widely used by the Python community. That said, it is possible for us to have the best of both worlds. We can move our code into
orkiv/main.py and still have a
__main__.py that imports it.
First, move the entire
__main__.py file into
main.py, using git:
git mv orkiv/__main__.py orkiv/main.py
Next, edit the new
main.py so that it can be imported from other modules without automatically running the app. There is a standard idiomatic way to do this in python. Replace the code that calls
Orkiv().run() with the following:
def main(): Orkiv().run() if __name__ == "__main__": main()
There are two things going on here. First, we moved the code that instantiates the application and starts it running in a function called
main(). There is nothing exciting about this function. It does the same thing that used to happen as soon as the module was imported. However, nothing will happen now unless that function is called.
One way to regain the previous functionality of immediately running the app when we type
python main.py would be to just call
main() at the bottom of the module. However, that is not what we want. Instead, our goal is to make the application automatically run if the user runs
python orkiv/main.py, but if they run
import main from a different module or the interpreter, nothing is explicitly run.
That’s what the
if __name__ conditional does. Every module that is ever imported has a magic
__name__ attribute. This is almost always the name of the module file without the
.py extension (eg:
main.py has a
main). However, if the module is not an imported module, but is the script that was run from the command line, that
__name__ attribute is set to
__main__ instead of the name of the module. Thus, we can easily tell if we are an invoked script or an imported script by testing if
__main__. Indeed, we are taking advantage of the same type of magic when we name a module
__main__.py. When we run
python zipfile_or_directory it tries to load the
__main__ module in that directory.
However, we do not currently have a
__main__ module, since we just moved it. Let’s rectify that:
from main import main main()
Save this as a new
orkiv/__main__.py and test that running
python orkiv from the parent directory still works.
You’ll find that the window runs, however, if you try to connect to a jabber server, you get an exception when it tries to display the buddy list. This is because our
orkiv.kv file was explicitly importing from
__main__. Fix the import at the top of the file so that it imports from
#:import la kivy.adapters.listadapter #:import ok main OrkivRoot:
While we’re editing code files, let’s also add a
__version__ string to the top of our
main.py that the Kivy build tools can introspect:
__version__ = 1.0
Deploying to Android with buildozer
Deploying to Android used to be a fairly painful process. Back in December, 2012, I described how to do it using a bulky Ubuntu virtual machine the Kivy devs had put together. Since then, they’ve designed a tool called buildozer that is supposed to do all the heavy lifting. Once again, I have some issues with this plan; it would be much better if Kivy integrated properly with setuptools, the de facto standard python packaging system than to design their own build system. However, buildozer is available and it works, so we’ll use it. The tool is still in alpha, so your mileage may vary. However, I’ve discovered that Kivy products in alpha format are a lot better than final releases from a lot of other projects, so you should be ok.
Let’s take it out for a spin. First activate your virtualenv and install the buildozer app into it:
. venv/bin/activate pip install buildozer
Also make sure you have a java compiler, apache ant, and the android platform tools installed. This is OS specific; these are the commands I used in Arch Linux:
sudo pacman -S jdk7-openjdk sudo pacman -S apache-ant yaourt android-sdk-platform-tools
Then run the init command to create a boilerplate
Now edit the
buildozer.spec to suit your needs. Most of the configuration is either straightforward (such as changing the application name to
Orkiv) or you can keep the default values. One you might overlook is that
source.dir should be set to
orkiv the directory that contains
requirements should include not only
sleekxmpp but also
dnspython, a library that sleekxmpp depends on. You can use the
pip freeze command to get a list of packages currently installed in your virtualenv. Then make a conscious decision as to whether you need to include each one, remembering that many of those (cython, buildozer, etc) are required for development, but not Android deployment.
For testing, I didn’t feel like creating images for presplash and the application icon, so I just pointed those at
icons/available.png. It’s probably prettier than anything I could devise myself, anyway!
If you want to see exactly what changes I made, have a look at the diff.
The next step is to actually build and deploy the app. This takes a while, as buildozer automatically downloads and installs a wide variety of tools on your behalf. I don’t recommend doing it over 3G!
First, Enable USB debugging on the phone and plug it into your development machine with a USB cable. Then tell buildozer to do all the things it needs to do to run a debug build on the phone:
buildozer android debug deploy run
I had a couple false starts before this worked. Mostly I got pertinent error messages that instructed me to install the dependencies I mentioned earlier. Then, to my surprise, the phone showed a Kivy “loading…” screen. Woohoo! It worked!
And then the app promptly crashed without any obvious error messages or debugging information.
Luckily, such information does exist. With the phone plugged into usb, run
adb logcat from your development machine. Review the Python tracebacks and you’ll discover that it is having trouble finding the sound file
I/python (25368): File "/home/dusty/code/orkiv/.buildozer/android/platform/python-for-android/build/python-install/lib/python2.7/site-packages/kivy/core/audio/__init__.py", line 135, in on_source I/python (25368): File "/home/dusty/code/orkiv/.buildozer/android/platform/python-for-android/build/python-install/lib/python2.7/site-packages/kivy/core/audio/audio_pygame.py", line 84, in load I/python (25368): File "/home/dusty/code/orkiv/.buildozer/android/platform/python-for-android/build/python-install/lib/python2.7/site-packages/android/mixer.py", line 202, in __init__ I/python (25368): IOError: [Errno 2] No such file or directory: '/data/data/ca.archlinux.orkiv/files/orkiv/sounds/in.wav' I/python (25368): Python for android ended.
The problem here is that the code specifies a relative path to the sound file as
orkiv/sounds/in.wav. This worked when we were running
python orkiv/ from the parent directory, but python for android is running the code as if it was inside the
orkiv directory. We can reconcile this later, but for now, let’s focus on getting android working and just hard code the file location:
self.in_sound = SoundLoader.load("sounds/in.wav")
While we’re at it, we probably also need to remove “orkiv” from the location of the icon files in
source: "icons/" + root.online_status + ".png"
adb logcat again indicates the error message hasn’t changed. This is because we aren’t explicitly including the
.wav file with our distribution. Edit
buildozer.spec to change that:
# (list) Source files to include (let empty to include all the files) source.include_exts = py,png,jpg,kv,atlas,wav
buildozer command once again and the app fire up and start running! Seeing your Python app running on an Android phone is a bit of a rush, isn’t it? I was able to log into a jabber account, but selecting a chat window goes into “narrow” mode because the phone’s screen has a much higher pixel density than my laptop. We’ll have to convert our size handler to display pixels somehow.
I was able to send a chat message, which was exciting, but when the phone received a response, it crashed. Hard.
adb logcat showed a segmentation fault or something equally horrifying. I initially guessed that some concurrency issue was happening in
sleekxmpp, but it turned out that the problem was in Kivy. I debugged this by putting print statements between each line in the
handle_xmpp_message method and seeing which ones executed before it crashed. It turned out that Kivy is crashing in its attempt to play the
.wav file on an incoming message. Maybe it can’t handle the file format of that particular audio file or maybe there’s something wrong with the media service. Hopefully the media service will be improved in future versions of Kivy. For now, let’s use the most tried and true method of bugfixing: pretend we never needed that feature! Comment out the line:
buildozer android debug deploy run.
Now the chat application interacts more or less as expected, though there are definitely some Android related quirks. Let’s fix the display pixel issue in orkiv.kv:
<OrkivRoot>: mode: "narrow" if self.width < dp(600) else "wide" AccountDetailsForm:
All I did was wrap the
600 in a call to
dp, which converts the 600 display pixels into real pixels so the comparison is accurate. Now when we run the app, it goes into “narrow” mode because the screen is correctly reporting as “not wide enough for wide mode”. However, if you start the app in landscape mode, it does allow side-by-side display. Perfect!
And that’s the app running on Android. Unfortunately, in all honesty, it’s essentially useless. As soon as the phone display goes to sleep, the jabber app closes which means the user is logged out. It might be possible to partially fix this using judicious use of Pause mode. However, since the phone has to shut down internet connectivity to preserve any kind of battery life, there’s probably a lot more involved than that.
On my phone, there also seems to be a weird interaction that the
BuddyList buttons all show up in a green color instead of the colors specified in the Kivy language file and the args_converter.
Third, the app crashes whenever we switch orientations. This is probably a bug fixable in our code rather than in Kivy itself, but I don’t know what it is.
Also, touch events are erratic on the phone. Sometimes the scroll view won’t allow me to scroll when I first touch it, but immediately interprets a touch event on the ListItem. Sometimes touch events appear to be forgotten or ignored and I have to tap several times for a button to work. Sometimes trying to select a text box utterly fails. I think there must be some kind of interaction between Kivy’s machinery and my hardware here, but I’m not certain how to fix it.
Occasionally, when I first log in, the BuddyList refuses to act like a
ScrollView and the first touch event is interpreted as opening a chat instead of scrolling the window. This is not a problem for subsequent touch events on the buddy list.
Finally, there are some issues with the onscreen keyboard. When I touch a text area, my keyboard (I’m using SwiftKey) pops up, and it works well enough. However, the swipe to delete behavior, which deletes an entire word in standard android apps, only deletes one letter here. More alarmingly, when I type into the password field, while the characters I typed are astrisk’d out in the field, they are still showing up in the SwiftKey autocomplete area. There seems to be some missing hinting between the OS and the app for password fields.
Before closing, I’d like to fix the problem where the app is no longer displaying icons on the laptop when I run
python orkiv/. My solution for this is not neat, but it’s simple and it works. However, it doesn’t solve the problem if we put the files in a
.zip. I’m not going to worry about this too much, since
buildozer is expected, in the future, to be able to make packages for desktop operating systems. It’s probably better to stick with Kivy’s tool. So in the end, this
__main__.py feature is probably not very useful and could be removed. (Removing features and useless code is one of the most important parts of programming.) However, for the sake of learning, let’s make it work for now! First we need to add a
root_dir field to the
Orkiv app. This variable can be accessed as
app.root_dir in kv files and as
Orkiv.get_running_app().root_dir in python files.
class Orkiv(App): def __init__(self, root_dir): super(Orkiv, self).__init__() self.root_dir = root_dir self.xmpp = None
Of course, we also have to change the
main() function that invokes the app to pass a value in. However, we can have it default to the current behavior by making the
root_dira keyword argument with a default value:
def main(root_dir=""): Orkiv(root_dir).run()
Next, we can change the
__main__.py to set this
root_dir variable to
from main import main main("orkiv/")
The idea is that whenever a file is accessed, it will be specified relative to
Orkiv.root_dir. So if we ran
python main.py from inside the
orkiv/ directory (this is essentially what happens on android), the root_dir is empty, so the relative path is relative to the directory holding
main.py. But when we run
python orkiv/ from the parent directory, the
root_dir is set to
orkiv, so the icon files are now relative to the directory holding
Finally, we have to change the icons line in the kivy file to reference
app.root_dir instead of hardcoding the path (a similar fix would be required for the sound file if we hadn’t commented it out):
Image: source: app.root_dir + "icons/" + root.online_status + ".png"
And now, the app works if we run
python orkiv/ or
python main.py and also works correctly on Android.
There are a million things that could be done with this Jabber client, from persistent logs to managed credentials to IOS deployment. I encourage you to explore all these options and more. However, this is as far as I can guide you on this journey. Thus part 9 is the last part of this tutorial. I hope you’ve enjoyed the ride and have learned much along the way. Most importantly, I hope you’ve been inspired to start developing your own applications and user interfaces using Kivy.
Writing this tutorial has required more effort and consumed more time than I expected. I’ve put on average five hours per tutorial (with an intended timeline of one tutorial per week) into this work, for a total of around 50 hours for the whole project.
The writing itself and reader feedback has certainly been compensation enough. However, I’m a bit used up after this marathon series and I’m planning to take a couple months off before writing anything like this tutorial again in my free time.
That said, I have a couple of ideas for further tutorials in Kivy that would build on this one. I may even consider collecting them into a book. It would be ideal to see this funded on gittip, but I’d want to be making $20 / hour (far less than half my normal wage, so I’d still be “donating” much of my time) for four hours per week before I could take half a day off from my day job to work on such pursuits — without using up my free time.
A recent tweet by Gittip founder Chad Whitacre suggests that people hoping to be funded on gittip need to market themselves. I don’t want to do that. I worked as a freelancer for several years, and I have no interest in returning to that paradigm. If I have to spend time “selling” myself, it is time I’m not spending doing what I love. In my mind, if I have to advertise myself, then my work is not exceptional enough to market itself. So I leave it up to the community to choose whether to market my work. If it is, someone will start marketing me and I’ll see $80/week in my gittip account. Then I’ll give those 4 hours per week to tutorials like this or other open source contributions. If it’s not, then I’ll feel more freedom to spend my free time however I like. Either way, I’ll be doing something that I have a lot of motivation to do.
Regardless of any financial value, I really hope this tutorial has been worth the time you spent reading it and implementing the examples! Thank you for being an attentive audience, and I hope to see you back here next time, whenever that is!
Creating Apps In Kivy: The Book
My request for crowd funding above didn’t take off. I’m making just under $12 per week on Gittip, all of which I’m currently regifting. Instead of my open contribution dream being fulfilled, I opted to follow the traditional publishing paradigm and wrote a book Creating Apps In Kivy, which was published with O’Reilly. I’m really excited about this book; I think it’s the highest quality piece of work I have done. If you enjoyed this tutorial, I encourage you to purchase the book, both to support me and because I think you’ll love it!