I recently heard about chaquopy - A "Python SDK for Android" and was really excited. There's already a demo app on the Play store for python 2 and python 3. As the author of enaml-native, I had to take a look to see how others are trying to tackle the python on android challenge. So here's a short review and discussion of my initial thoughts on chaquopy.
Initial thoughts. It starts reasonably fast (about 3-4 seconds on my phone). There's a noticeable delay when starting the python activity on my phone (much smoother on the emulator), but other than that the widgets look great and interaction is smooth!
The apk is 11.54 MB and the app is only 18 MB installed (21MB if you include the cache), which is very good for bundling python!
Also, the build process was a piece of cake! I cloned the repo, opened in android studio, pressed play, and it worked! This is because the build system includes precompiled python and libs for you.
The Android UI demo part is written in python. The rest of the app is Java. Here's the code ui_demo.py
The code, while python, looks very similar to Java. You can see there's no async stuff going on here (like enaml-native), everything is being used directly, which is nice. I also like the annotations much more than the pyjnius like syntax. It looks very similar than jnius and I assume that's what it's using for the python side of the JNI (or a similar implementation).
Also it requires a layout file for the view in xml here https://github.com/chaquo/chaquopy/blob/master/app/src/main/res/layout/activity_menu.xml.
I must admit this seems a bit complicated for what it does. If you don't know the android api's this will really leave you lost. As a comparison, I wrote the same view using enaml-native. Here's the code.
And here's the enaml-native version in action.
The enaml-native version doesn't require any xml layout files. I'd also argue that it's easier to get an idea of what is actually going on by looking at the code, however this enaml-native example doesn't implement the sound, vibrate, and notification API's (as these aren't yet implemented in enaml-native).
The build system is easy because it bundles precompiled versions of crystax python for each arch (see https://chaquo.com/maven/com/chaquo/python/target/). It also makes fantastic use of the gradle build system for installing pip dependencies and python.
There are also a few precompiled wheels available for a few python extensions here (such as numpy and ujson). However any other extension modules will need cross compiled and I see no documentation on how they suggest to do it.
It looks like the python stdlib, requirements, and app code are all zipped and added to the assets in the apk.

What's interesting however is that they are NOT extracted (that I can see anywhere) so they must be read directly from the zip. In my own testing this leads to VERY poor performance, but this easily explains how the app size is 10MB less than a typical enaml-native and kivy app. Python's zipimport uses C so I don't think any Java implementation will be faster.
Also a few of the python extension modules are not included as separate modules (such as pyexpat and unicodedata) so I'm interested to see if a library like tornado will work with Chaquopy. Edit: These are included under assets/chaquopy/stdlib-native so it should work fine.
If the goal is to simply reuse some python libraries in a java app with a native look and feel then this is the perfect fit. It requires an in depth knowledge of android to use but is effective if this is your use case.
But if the goal is to be able to build an app entirely with python than I'm a bit skeptical of how well this will perform when it comes to complex layouts (but please prove me wrong!). When I first created enaml-native, I tried a similar implementation that used PyJnius to create native widgets and implement interfaces required to interact with them. It worked very well for simple layouts and a small number of nodes (<100), however the view tree grew I started to see very bad performance. The JNI was simply too much of a bottleneck (and so is python).
It looks like a lot of work went into this and I'm excited to see how it turns out. I wish the best for the project and will be actively be checking for progress updates! The gradle integration is by far the best I've ever seen (even amongst react native projects) and is way beyond what I could do, great work!
Also, if the developer(s) happens to read this, I'd very much love to chat!
Happy coding!
Demo
I encourage you to try out the demo yourself to get a feel for it, but here's a short demo of what it does.Initial thoughts. It starts reasonably fast (about 3-4 seconds on my phone). There's a noticeable delay when starting the python activity on my phone (much smoother on the emulator), but other than that the widgets look great and interaction is smooth!
The apk is 11.54 MB and the app is only 18 MB installed (21MB if you include the cache), which is very good for bundling python!
Also, the build process was a piece of cake! I cloned the repo, opened in android studio, pressed play, and it worked! This is because the build system includes precompiled python and libs for you.
Code
The Android UI demo part is written in python. The rest of the app is Java. Here's the code ui_demo.py
from __future__ import absolute_import, division, print_function
from java import dynamic_proxy, jboolean, jvoid, Override, static_proxy
from android.app import AlertDialog
from android.content import Context, DialogInterface
from android.graphics.drawable import ColorDrawable
from android.os import Bundle
from android.support.v4.app import DialogFragment
from android.support.v7.app import AppCompatActivity
from android.support.v7.preference import Preference, PreferenceFragmentCompat
from android.view import Menu, MenuItem, View
from java.lang import String
from com.chaquo.python.demo import R
from .utils import view_source
class UIDemoActivity(static_proxy(AppCompatActivity)):
@Override(jvoid, [Bundle])
def onCreate(self, state):
AppCompatActivity.onCreate(self, state)
if state is None:
state = Bundle()
self.setContentView(R.layout.activity_menu)
self.findViewById(R.id.tvCaption).setText(R.string.demo_caption)
self.title_drawable = ColorDrawable()
self.getSupportActionBar().setBackgroundDrawable(self.title_drawable)
self.title_drawable.setColor(
state.getInt("title_color", self.getResources().getColor(R.color.blue)))
self.wvSource = self.findViewById(R.id.wvSource)
view_source(self, self.wvSource, "ui_demo.py")
self.wvSource.setVisibility(state.getInt("source_visibility", View.GONE))
self.getSupportFragmentManager().beginTransaction()\
.replace(R.id.flMenu, MenuFragment()).commit()
@Override(jvoid, [Bundle])
def onSaveInstanceState(self, state):
state.putInt("source_visibility", self.wvSource.getVisibility())
state.putInt("title_color", self.title_drawable.getColor())
@Override(jboolean, [Menu])
def onCreateOptionsMenu(self, menu):
self.getMenuInflater().inflate(R.menu.view_source, menu)
return True
@Override(jboolean, [MenuItem])
def onOptionsItemSelected(self, item):
id = item.getItemId()
if id == R.id.menu_source:
vis = self.wvSource.getVisibility()
new_vis = View.VISIBLE if (vis == View.GONE) else View.GONE
self.wvSource.setVisibility(new_vis)
return True else:
return False
class MenuFragment(static_proxy(PreferenceFragmentCompat)):
@Override(jvoid, [Bundle, String])
def onCreatePreferences(self, state, rootKey):
self.addPreferencesFromResource(R.xml.activity_ui_demo)
from android.media import AudioManager, SoundPool
self.sound_pool = SoundPool(1, AudioManager.STREAM_MUSIC, 0)
self.sound_id = self.sound_pool.load(self.getActivity(), R.raw.sound, 1)
@Override(jboolean, [Preference])
def onPreferenceTreeClick(self, pref):
method = getattr(self, pref.getKey())
if method:
method(self.getActivity())
return True else:
return False
def demo_dialog(self, activity):
ColorDialog().show(self.getFragmentManager(), "color")
def demo_notify(self, activity):
from android.app import Notification
builder = Notification.Builder(activity)
builder.setSmallIcon(R.drawable.ic_launcher)
builder.setContentTitle(
activity.getString(R.string.demo_notify_title))
builder.setContentText(
activity.getString(R.string.demo_notify_text))
activity.getSystemService(Context.NOTIFICATION_SERVICE)\
.notify(0, builder.getNotification())
def demo_toast(self, activity):
from android.widget import Toast
Toast.makeText(activity, R.string.demo_toast_text, Toast.LENGTH_SHORT).show()
def demo_sound(self, activity):
self.sound_pool.play(self.sound_id, 1, 1, 0, 0, 1)
def demo_vibrate(self, activity):
activity.getSystemService(Context.VIBRATOR_SERVICE)\
.vibrate(200)
class ColorDialog(static_proxy(DialogFragment)):
@Override(AlertDialog, [Bundle])
def onCreateDialog(self, state):
activity = self.getActivity()
builder = AlertDialog.Builder(activity)
builder.setTitle(R.string.demo_dialog_title)
builder.setMessage(R.string.demo_dialog_text)
class Listener(dynamic_proxy(DialogInterface.OnClickListener)):
def __init__(self, color_res):
super(Listener, self).__init__()
self.color = activity.getResources().getColor(color_res)
def onClick(self, dialog, which):
activity.title_drawable.setColor(self.color)
builder.setNegativeButton(R.string.red, Listener(R.color.red))
builder.setNeutralButton(R.string.green, Listener(R.color.green))
builder.setPositiveButton(R.string.blue, Listener(R.color.blue))
return builder.create()
The code, while python, looks very similar to Java. You can see there's no async stuff going on here (like enaml-native), everything is being used directly, which is nice. I also like the annotations much more than the pyjnius like syntax. It looks very similar than jnius and I assume that's what it's using for the python side of the JNI (or a similar implementation).
Also it requires a layout file for the view in xml here https://github.com/chaquo/chaquopy/blob/master/app/src/main/res/layout/activity_menu.xml.
I must admit this seems a bit complicated for what it does. If you don't know the android api's this will really leave you lost. As a comparison, I wrote the same view using enaml-native. Here's the code.
from enamlnative.core.api import Looper, Conditionalfrom enamlnative.widgets.api import (
Flexbox, Toolbar, Button, Dialog, TextView, View, ViewPager, PagerFragment, WebView, ScrollView
)
from enamlnative.android.app import AndroidApplication
app = AndroidApplication.instance()
COLORS = {
"blue": "#3F51B5",
"red": "#b53f3f",
"green": "#3f8e3d"}
enamldef Spacer(View):
height = 1
background_color = "#ccc"
enamldef ListButton(Button):
flat = True
all_caps = False
text_alignment = "left"
padding = (20, 20, 10, 20)
enamldef HomeScreen(PagerFragment): page:
Flexbox:
flex_direction = "column"
background_color = "#FFF"
Toolbar:
height = "wrap_content"
min_height = 60
background_color << COLORS['blue']
title = "Enaml demo"
title_color = "#FFF"
ListButton:
text = "Android UI demo"
clicked :: page.parent.current_index = 1
enamldef AndroidDemoScreen(PagerFragment): page:
Flexbox: view:
attr color = "blue"
attr show_source = False
flex_direction = "column"
align_items = "stretch"
#align_content = "stretch"
background_color = "#FFF"
transition = "default"
Toolbar:
height = "wrap_content"
min_height = 60
background_color << COLORS[view.color]
content_padding = (0,0,0,0)
Flexbox:
width = "match_parent"
justify_content = "space_between"
align_items = "center"
TextView:
text = "Android UI demo"
padding = (10, 10, 10, 10)
font_style = "bold"
text_size = 18
text_color = "#FFF"
Button:
flat = True
text = "View source"
text_size = 14
text_color = "#FFF"
clicked :: view.show_source = not view.show_source
TextView:
height = "wrap_content"
width = "match_parent"
min_height = 80
text = "This activity was written entirely in Python using enaml-native. " \
"To view it's source code, press the button above."
padding = (10, 10, 10, 10)
ScrollView:
height = "wrap_content"
width = "match_parent"
Flexbox:
flex_direction = "column"
height = 200
Spacer:
pass
ListButton:
text = "Dialog box"
clicked :: dialog.show = True
Spacer:
pass
ListButton:
text = "Notification"
clicked :: dialog.show = True
Spacer:
pass
ListButton:
text = "Toast"
clicked :: app.show_toast("Toast from Python")
Spacer:
pass
ListButton:
text = "Sound"
clicked :: app.show_toast("Toast from Python")
Spacer:
pass
ListButton:
text = "Vibration"
clicked :: app.show_toast("Toast from Python")
Spacer:
pass
Conditional:
condition << view.show_source
WebView:
width = "match_parent"
height = 300
activated ::
with open(__file__) as f:
self.source = """<?xml version=\"1.0\" encoding=\"UTF-8\" ?>
<html>
<head>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/default.min.css"> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js"></script> </head> <body><pre><code class="python">{}</code></pre> <script>hljs.initHighlightingOnLoad();</script> </body> </html>""".format(f.read())
source = ""
javascript_enabled = True
Dialog: dialog:
Flexbox:
flex_direction = "column"
TextView:
text = "Dialog from Python" font_style = "bold" text_size = 18 padding = (20, 20, 20, 0)
TextView:
text = "Select title color" padding = (20, 0, 0, 0)
Flexbox:
width = "match_parent"
justify_content = "space_between"
padding = (0, 20, 0, 0)
Button:
flat = True
text = "green"
text_color = COLORS[self.text]
clicked ::
view.color = self.text
dialog.show = False
Button:
flat = True
text = "red"
text_color = COLORS[self.text]
clicked ::
view.color = self.text
dialog.show = False
Button:
flat = True
text = "blue"
text_color = COLORS[self.text]
clicked ::
view.color = self.text
dialog.show = False
enamldef ContentView(ViewPager): view:
transition = 'draw_from_back'
background_color = "#000"
paging_enabled = False
activated::
app.observe('back_pressed',on_back_pressed)
func on_back_pressed(change):
change['value']['handled'] = True
view.current_index = max(0, view.current_index-1) HomeScreen: pass AndroidDemoScreen: pass
And here's the enaml-native version in action.
The enaml-native version doesn't require any xml layout files. I'd also argue that it's easier to get an idea of what is actually going on by looking at the code, however this enaml-native example doesn't implement the sound, vibrate, and notification API's (as these aren't yet implemented in enaml-native).
Internals
From inspecting the chaquopy code I can say it looks a lot like pybridge except that it uses a custom Python/Java api instead of json. As mentioned before this bridge looks a lot like PyJnius except it has extra code on the Java side to do things like retrieving modules and objects from python.The build system is easy because it bundles precompiled versions of crystax python for each arch (see https://chaquo.com/maven/com/chaquo/python/target/). It also makes fantastic use of the gradle build system for installing pip dependencies and python.
There are also a few precompiled wheels available for a few python extensions here (such as numpy and ujson). However any other extension modules will need cross compiled and I see no documentation on how they suggest to do it.
It looks like the python stdlib, requirements, and app code are all zipped and added to the assets in the apk.
What's interesting however is that they are NOT extracted (that I can see anywhere) so they must be read directly from the zip. In my own testing this leads to VERY poor performance, but this easily explains how the app size is 10MB less than a typical enaml-native and kivy app. Python's zipimport uses C so I don't think any Java implementation will be faster.
Final comments
I'm glad to see more interest and work being done to try and make python available for Android so seeing this project is awesome.If the goal is to simply reuse some python libraries in a java app with a native look and feel then this is the perfect fit. It requires an in depth knowledge of android to use but is effective if this is your use case.
But if the goal is to be able to build an app entirely with python than I'm a bit skeptical of how well this will perform when it comes to complex layouts (but please prove me wrong!). When I first created enaml-native, I tried a similar implementation that used PyJnius to create native widgets and implement interfaces required to interact with them. It worked very well for simple layouts and a small number of nodes (<100), however the view tree grew I started to see very bad performance. The JNI was simply too much of a bottleneck (and so is python).
It looks like a lot of work went into this and I'm excited to see how it turns out. I wish the best for the project and will be actively be checking for progress updates! The gradle integration is by far the best I've ever seen (even amongst react native projects) and is way beyond what I could do, great work!
Also, if the developer(s) happens to read this, I'd very much love to chat!
Happy coding!
im glad, you implemented a similar version in enaml-native to let users know they both offer, i must say it looks more like java than python,
ReplyDeleteHi, I'm the creator of Chaquopy. I'm really happy to see this review: I think you've given a fair summary of our strengths and weaknesses.
ReplyDeleteThe only correction I'd like to make is that pyexpat, unicodedata and the other standard native modules are actually included. They're in the APK under assets/chaquopy/stdlib-native, and you should be able to import and test them in the demo app console.
I think there's room for many different approaches to Python on Android. Chaquopy focuses on giving complete access to all the features of the standard Android API and build tools. (For example, the XML layout file you mentioned was generated using Android Studio's WYSIWYG editor: it didn't have to be written by hand.) But if you want something more Pythonic, or portable to other platforms, then enaml-native or Kivy is the way to go.
Thanks again for the great article, and if you want to chat, please drop me an email at the address on the Chaquopy website.
Appreciate the feedback, updated the post. Will send you an email. Thanks!
DeleteIt's amazing that you two guys have created such great works.
ReplyDeleteI use react-native to build an app. It's convenient to do what i want to do. But the only thing make me give up RN is the speed. Although facebook says RN is very fast like native apps, but it't not. Since the communication between JavaScript and Java is too slow, especially when I use their Navigator and FlatList/SectionList.
I readed many enaml-native source codes, and EN use jni/thread to do the communication too. So, I doubt if the same things(e.g. performance) will happen in EN.
Thanks for the feedback. Yes, EN uses the same approach as RN just with msgpack instead of json. Unfortunately there's no way around this, there's always a trade off. For sectons of an app that need to be very performant (ex a complex ListView) i'd recommend just writing the view in java and then exposing the widget to enaml-native as a native component. This way you get 100% native performance for that widget but still can use the high level api's provided by enaml-native for the other parts of the app.
DeleteThans for the replying.
DeleteIt's great if EN can use(inject/import) native component directly.
I'll try that.
I'll also try to find if there's a same way in RN.
Thanks!