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

Sponsored

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 Toast class 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:

  1. Identify what you need — Which device feature? Camera, GPS, battery status, notifications?
  2. Choose your tool — Use a high-level wrapper (plyer) when possible; fall back to a lower-level bridge (Pyjnius) for custom needs.
  3. 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.
  4. Call the native API — Invoke the function through the bridge, passing any parameters.
  5. 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

  • jnius not found: Pyjnius is only available on Android, not on desktop. Wrap your imports inside if 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 using android.permissions module. Without this, your call will silently fail or throw a SecurityException.
  • NotImplementedError from 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 threading as we did in the battery example.
  • Callbacks never fire: Some native APIs require a valid Activity context. On Android, use org.kivy.android.PythonActivity.mActivity to 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, and plyer.
  • 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 jnius at the top of your script without checking the platform — it crashes on desktop. Always guard with if platform == 'android':.
  • Forgetting to declare permissions in buildozer.spec (e.g., android.permissions = CAMERA) — the native call will throw a SecurityException or silently fail.
  • Running long native calls (like GPS) on the main thread, which freezes the UI and can cause an ANR error.
  • Assuming plyer works the same on all platforms — it raises NotImplementedError on desktop, so you must catch it and provide a fallback.

Variations

  1. Use pyobjus for iOS native interaction instead of pyjnius; the syntax is similar but targets Objective-C.
  2. Leverage Kivy's android module (or org.kivy.android) for Android-specific features like the activity context and permissions.
  3. 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 plyer for common features, and drop to pyjnius/pyobjus for 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 NotImplementedError gracefully to support desktop development.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.