Kivy Native Modules
Interact with native modules in Kivy — practical Mobile App Development tutorial for step-by-step learners.
Focus: interact with native modules in kivy
You've built a beautiful Kivy UI, your buttons respond to taps, and everything works perfectly on your desktop. Then you ship it to a phone, and suddenly you need the device's camera, its GPS, or the vibration motor — and your app goes silent. That's the wall every mobile developer hits: your Python code runs in a virtual environment, but the hardware lives in the native world of Android and iOS. This lesson teaches you how to interact with native modules in Kivy, so you can bridge that gap and unlock the full power of the device. By the end, you'll be able to call native APIs from your Kivy app with confidence — and you'll know exactly what to learn next.
The Problem This Lesson Solves
Kivy is a cross-platform framework: you write UI once, and it renders on Windows, macOS, Linux, Android, and iOS. But the moment you need a feature that isn't pure Python — a push notification, a sensor reading, or a file picker that uses the system dialog — you hit a wall. Your Python code can't directly talk to the Android activity or the iOS view controller.
This is the native bridge problem. Every mobile framework faces it. For Kivy, the solution is a set of tools and libraries that let you "reach across" from Python into Java/Kotlin (Android) or Objective-C/Swift (iOS). Without this skill, your app is stuck in a sandbox: it can display content and handle touches, but it's blind and deaf to the phone itself.
Think of your app like a tourist in a foreign country. Your Kivy interface is the phrasebook — it lets you communicate with the user. But to actually use the local services — the bank, the transit system, the tour guide — you need an interpreter. That interpreter is the native module layer.
Core Concept / Mental Model
Let's define the pieces:
- Native module: A piece of code written in the platform's native language (Java on Android, Objective-C on iOS) that exposes functions your Python code can call. For example, Android's
Toastclass shows a short message; it's a native module. - Bridge: The mechanism that lets Python call into native code. Kivy's primary bridge is Pyjnius for Android (Java Native Interface) and Pyobjus for iOS (Objective-C runtime).
- Wrapper: A Python-friendly function that hides the raw native calls. Libraries like plyer wrap common device features (camera, GPS, accelerometer) so you don't write low-level JNI code yourself.
Here's a mental picture: your Kivy app is the main character in a play. The stage is the screen, the script is your Python code. When the play needs a prop — like a real-world location — a stagehand (the bridge) fetches it from the backstage area (the native module). The stagehand translates the director's (your) request into something the backstage crew understands, and brings back the result.
This separation exists because Android and iOS enforce strict security. Your Python code isn't allowed to touch the hardware directly; it must go through the native APIs, which manage permissions and safe access.
How It Works Step by Step
Interacting with native modules in Kivy follows a predictable pattern:
- Identify what you need — Which device feature? Camera, GPS, battery status, notifications?
- Choose your tool — Use a high-level wrapper (plyer) when possible; fall back to a lower-level bridge (Pyjnius) for custom needs.
- Request permissions — Android and iOS require runtime permissions for sensitive features (camera, location, etc.). You must declare them in your app's build config and request them at runtime.
- Call the native API — Invoke the function through the bridge, passing any parameters.
- Handle the result — The native module returns data; you process it in Python and update your UI.
The key point: the bridge is synchronous or asynchronous depending on the API. Some calls (like showing a toast) are fire-and-forget. Others (like reading GPS) are callbacks — you provide a function that gets called when data is ready.
Hands-On Walkthrough
Let's build a working example. We'll create a Kivy app that shows a toast message using Android's native Toast class. We'll use Pyjnius, which comes preinstalled with Kivy when you build for Android.
First, install the prerequisite library on your desktop for testing (though Pyjnius only works on Android, we'll structure the code so it won't crash on desktop).
pip install kivy pyjnius
Now, create main.py:
from kivy.app import App
from kivy.uix.button import Button
from kivy.utils import platform
class NativeToastApp(App):
def build(self):
return Button(text="Show Toast", on_press=self.show_toast)
def show_toast(self, instance):
if platform == 'android':
from jnius import autoclass
Toast = autoclass('android.widget.Toast')
context = autoclass('org.kivy.android.PythonActivity').mActivity
Toast.makeText(context, "Hello from Python!", Toast.LENGTH_SHORT).show()
else:
print("Toast is not available on this platform")
if __name__ == '__main__':
NativeToastApp().run()
Expected output: On Android, a small toast appears at the bottom of the screen. On desktop, you'll see a print message in the console.
Now let's exercise a more practical feature: reading the battery level. We'll use plyer, which wraps native APIs in a simple Python class.
pip install plyer
from kivy.app import App
from kivy.uix.label import Label
from plyer import battery
class BatteryApp(App):
def build(self):
# Start a thread to avoid blocking UI
import threading
threading.Thread(target=self.get_battery, daemon=True).start()
return Label(text="Reading battery...")
def get_battery(self):
try:
battery.status
# Access percentage and charging status
percent = battery.percentage
charging = battery.is_charging
self.root.text = f"Battery: {percent}% (Charging: {charging})"
except NotImplementedError:
self.root.text = "Battery not available on this platform"
if __name__ == '__main__':
BatteryApp().run()
Expected output: On Android, the label updates with the actual battery percentage. On desktop, it may raise NotImplementedError or return dummy data.
Pro tip: Always run native module calls in a separate thread (like we did above) if they could take time. Blocking the main thread freezes your UI and may trigger an "Application Not Responding" (ANR) error on Android.
Even deeper: you can look up and call arbitrary native methods. Here's how you'd invoke a custom Java method if you had one:
from jnius import autoclass
# Load a Java class
MyClass = autoclass('com.example.MyClass')
# Create an instance
obj = MyClass()
# Call a method
result = obj.someMethod(42, "hello")
Compare Options / When to Choose What
You have several ways to interact with native modules in Kivy. Here's a comparison to guide your choice:
| Tool | Use Case | Pros | Cons |
|---|---|---|---|
| plyer | Common features: battery, GPS, camera, notifications | Simple Python API, cross-platform, no need to know Java/Obj-C | Limited to built-in features; can't extend without forking |
| Pyjnius | Custom Android Java API calls | Full access to Android SDK, call any Java class | Only works on Android; requires basic Java knowledge; verbose |
| Pyobjus | Custom iOS Objective-C API calls | Full access to iOS SDK | Only works on iOS; requires Objective-C knowledge; less mature |
| Custom native module (buildozer spec) | Very high performance or complex logic | Maximum control, can compile C/C++ | Most complex; requires compiling per platform |
For most apps, start with plyer. It covers ~80% of typical needs (battery, vibration, accelerometer, notifications). Only drop down to Pyjnius/Pyobjus when you need a specific API that plyer doesn't wrap — like a proprietary SDK or a hardware sensor not yet supported.
Troubleshooting & Edge Cases
jniusnot found: Pyjnius is only available on Android, not on desktop. Wrap your imports insideif platform == 'android':blocks. Also, make sure you're building with Buildozer, which includes Pyjnius by default.- Permission errors: Android 6+ requires runtime permissions. Declare them in your Buildozer spec (e.g.,
android.permissions = CAMERA) and request permission usingandroid.permissionsmodule. Without this, your call will silently fail or throw aSecurityException. NotImplementedErrorfrom plyer: The platform you're on doesn't support that feature. Always catch it and provide a fallback UI message.- UI freezes: Native calls can take seconds (e.g., GPS fix). Never run them on the main thread. Use
threadingas we did in the battery example. - Callbacks never fire: Some native APIs require a valid
Activitycontext. On Android, useorg.kivy.android.PythonActivity.mActivityto get the current activity. If you create a fresh context, callbacks may not attach. - Referencing your Android project classes: If you wrote custom Java code, it must be compiled into your APK. You can't just call arbitrary classes that aren't part of the Android SDK or your app's build.
What You Learned & What's Next
Now you understand the core of interacting with native modules in Kivy. You learned:
- How to identify when you need a native bridge.
- The role of
pyjnius,pyobjus, andplyer. - How to call native Toast and battery APIs from Python.
- How to avoid common pitfalls like permission errors and UI freezing.
This is a foundational skill for any serious mobile app. Up next in the track, you'll likely explore handling GPS and location services, or pushing notifications — both built on the same bridging principles you just mastered. Keep this mental model and the troubleshooting tips handy; they'll save you hours when your app hits its first real device.
Practice recap
Write a small Kivy app that uses plyer.vibrator to buzz the device when a button is pressed. Make sure to catch NotImplementedError and print a debug message on desktop. Then, change the call to use pyjnius directly and call the Android Vibrator service — compare the verbosity of both approaches.
Common mistakes
- Importing
jniusat the top of your script without checking the platform — it crashes on desktop. Always guard withif platform == 'android':. - Forgetting to declare permissions in
buildozer.spec(e.g.,android.permissions = CAMERA) — the native call will throw aSecurityExceptionor silently fail. - Running long native calls (like GPS) on the main thread, which freezes the UI and can cause an ANR error.
- Assuming
plyerworks the same on all platforms — it raisesNotImplementedErroron desktop, so you must catch it and provide a fallback.
Variations
- Use
pyobjusfor iOS native interaction instead ofpyjnius; the syntax is similar but targets Objective-C. - Leverage Kivy's
androidmodule (ororg.kivy.android) for Android-specific features like the activity context and permissions. - Write your own compiled native module (e.g., using Cython or C) for performance-critical tasks, integrated via Buildozer.
Real-world use cases
- A fitness app that checks battery level to warn users before starting a long GPS tracking session.
- A camera app that uses the native camera API to capture photos with full resolution and save to gallery.
- A business social app that shows a native toast notification when a message is received, using the Android Toast class.
Key takeaways
- Native modules let Kivy apps access device hardware and system APIs that Python alone can't reach.
- Use high-level wrappers like
plyerfor common features, and drop topyjnius/pyobjusfor custom calls. - Always guard platform-specific imports and feature calls with
platform == 'android'checks. - Request and declare permissions explicitly in your build configuration.
- Keep native calls off the main thread to avoid UI freezes.
- Handle
NotImplementedErrorgracefully to support desktop development.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.