2013-08-26

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:

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 main.py.

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:

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:

(commit)

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 __name__ of 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 __name__ is __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:

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 main instead:

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:

(commit)

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:

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:

Then run the init command to create a boilerplate buildozer.spec:

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 main.py.

The requirements should include not only kivy and 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:

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 in.wav:

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:

(commit)

While we’re at it, we probably also need to remove “orkiv” from the location of the icon files in orkiv.kv:

Finally, running buildozer and 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:

Run the 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:

(commit)

and rerun 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:

(commit)

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.

(commit)

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:

Next, we can change the __main__.py to set this root_dir variable to 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 orkiv/.

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):

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.

Financial Feedback

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!

Show more