Skip to content

Instantly share code, notes, and snippets.

@freakboy3742
Last active October 23, 2019 16:30
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save freakboy3742/7beb22c587e57240610777a44af645d8 to your computer and use it in GitHub Desktop.
Save freakboy3742/7beb22c587e57240610777a44af645d8 to your computer and use it in GitHub Desktop.
One app, three platforms...
import android
from android.app import AlertDialog
from android.graphics import Color
from android.graphics.drawable import ColorDrawable
from android.os import AsyncTask
from android.os import Looper
from android.os import Handler
from android.util import TypedValue
from android.view import View
from android.view import MenuItem
from android.widget import LinearLayout
from android.widget import ArrayAdapter
from android.widget import ImageView
from android.widget import TextView
from android.widget import EditText
from android.support.v4.widget import SwipeRefreshLayout
from android.util import Log
from java.io import BufferedInputStream, BufferedReader, InputStreamReader, OutputStreamWriter
from java.net import URL
from org.json import JSONArray, JSONObject
from com.baoyz.swipemenulistview import SwipeMenuListView, SwipeMenuCreator, SwipeMenuItem
class RefreshTask(implements=java.lang.Runnable):
def __init__(self, adapter):
self.adapter = adapter
def run(self) -> void:
Log.i("TESTAPP", "BACKGROUND REFRESH DONE TASK")
self.adapter.notifyDataSetChanged()
Log.i("TESTAPP", "BACKGROUND REFRESH DONE TASK DONE")
class UpdateDataTask(implements=java.lang.Runnable):
def __init__(self, adapter):
self.adapter = adapter
def run(self) -> void:
url = URL("http://freakboy3742.pythonanywhere.com/api/todo/?format=json")
connection = url.openConnection()
Log.i("TESTAPP", "GET RESPONSE: %s" % connection.getResponseCode())
reader = BufferedReader(InputStreamReader(connection.getInputStream()))
content = ""
line = reader.readLine()
while line:
content = content + '\n' + line
line = reader.readLine()
json = JSONArray(content)
self.adapter.data = []
for i in range(0, json.length()):
obj = json.get(i)
self.adapter.data.append(obj)
handler = Handler(Looper.getMainLooper())
handler.post(RefreshTask(self.adapter))
connection.disconnect()
class DeleteItemTask(implements=java.lang.Runnable):
def __init__(self, adapter, index):
self.adapter = adapter
self.index = index
def run(self) -> void:
Log.i("TESTAPP", "DELETE ITEM %s" % self.index)
url = URL("http://freakboy3742.pythonanywhere.com/api/todo/%s/" % self.adapter.data[self.index].get('id'))
Log.i("TESTAPP", "DELETE URL %s" % url)
connection = url.openConnection()
connection.setRequestMethod("DELETE")
Log.i("TESTAPP", "DELETE RESPONSE: %s" % connection.getResponseCode())
del self.adapter.data[self.index]
handler = Handler(Looper.getMainLooper())
handler.post(RefreshTask(self.adapter))
class AddItemTask(implements=java.lang.Runnable):
def __init__(self, adapter, description):
self.adapter = adapter
self.description = description
def run(self) -> void:
url = URL("http://freakboy3742.pythonanywhere.com/api/todo/")
connection = url.openConnection()
connection.setRequestMethod("POST")
connection.setDoOutput(True)
connection.setRequestProperty("Content-Type", "application/json")
writer = OutputStreamWriter(connection.getOutputStream())
writer.write('{"description": "%s", "completed": false}' % self.description)
writer.flush()
# writer.close()
Log.i("TESTAPP", "PUT RESPONSE: %s %s" % (connection.getResponseCode(), connection.getResponseMessage()))
reader = BufferedReader(InputStreamReader(connection.getInputStream()))
content = ""
line = reader.readLine()
while line:
content = content + '\n' + line
line = reader.readLine()
self.adapter.data.append(JSONObject(content))
handler = Handler(Looper.getMainLooper())
handler.post(RefreshTask(self.adapter))
connection.disconnect()
class DataAdapter(extends=com.baoyz.swipemenulistview.BaseSwipeListAdapter):
def __init__(self, context):
Log.i("TESTAPP", "INIT DATA ADAPTER " + str(data))
self.context = context
self.updateData()
def updateData(self):
task = UpdateDataTask(self)
AsyncTask.execute(task)
def deleteItem(self, item):
task = DeleteItemTask(self, item)
AsyncTask.execute(task)
def addItem(self, description):
task = AddItemTask(self, description)
AsyncTask.execute(task)
def getCount(self) -> int:
# Log.i("TESTAPP", "GET COUNT")
# Log.i("TESTAPP", "DATA SIZE of " + str(self.data))
# Log.i("TESTAPP", "DATA SIZE " + str(len(self.data)))
return len(self.data)
def getItem(self, position: int) -> java.lang.String:
# Log.i("TESTAPP", "GET ITEM " + str(position))
return self.data[position]
def getItemId(self, position: int) -> long:
# Log.i("TESTAPP", "GET ITEM ID " + str(position))
return position
def getView(self, position: int, convertView: android.view.View, parent: android.view.ViewGroup) -> android.view.View:
if convertView is None:
convertView = View.inflate(self.context, android.R.layout.simple_list_item_1, None)
item = self.getItem(position)
convertView.setText(item.get('description'))
return convertView
class RefreshListener(implements=android.support.v4.widget.SwipeRefreshLayout[OnRefreshListener]):
def __init__(self, layout: android.support.v4.widget.SwipeRefreshLayout, adapter):
self.layout = layout
self.adapter = adapter
def onRefresh(self) -> void:
Log.i("TESTAPP", "REFRESH!!")
self.adapter.updateData()
self.layout.setRefreshing(False)
class ListSwipeMenuCreator(implements=com.baoyz.swipemenulistview.SwipeMenuCreator):
def __init__(self, context):
self.context = context
def create(self, menu: com.baoyz.swipemenulistview.SwipeMenu) -> void:
# # Create "Open" item
# openItem = SwipeMenuItem(self.context)
# openItem.setBackground(ColorDrawable(Color.rgb(0xC9, 0xC9, 0xCE)))
# openItem.setWidth(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 90, self.context.getResources().getDisplayMetrics()))
# openItem.setTitle("Open")
# openItem.setTitleSize(18)
# openItem.setTitleColor(Color.WHITE)
# menu.addMenuItem(openItem)
# create "delete" item
deleteItem = SwipeMenuItem(self.context)
deleteItem.setBackground(ColorDrawable(Color.rgb(0xF9, 0x3F, 0x25)))
deleteItem.setWidth(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 90, self.context.getResources().getDisplayMetrics()))
deleteItem.setIcon(android.R.drawable.ic_delete)
menu.addMenuItem(deleteItem)
class ListMenuItemClickListener(implements=com.baoyz.swipemenulistview.SwipeMenuListView[OnMenuItemClickListener]):
def __init__(self, listview, adapter):
self.listview = listview
self.adapter = adapter
def onMenuItemClick(self, position: int, menu: com.baoyz.swipemenulistview.SwipeMenu, index: int) -> bool:
Log.i("TESTAPP", "CLICK MENU ITEM %s, action %s" % (position, index))
# if index == 0:
# Log.i("TESTAPP", "OPEN item %s" % position)
# self.adapter.addItem('New item')
# elif index == 1:
Log.i("TESTAPP", "DELETE item %s" % position)
self.adapter.deleteItem(position)
return True
class DialogOKClickListener(implements=android.content.DialogInterface[OnClickListener]):
def __init__(self, input_field, adapter):
self.input_field = input_field
self.adapter = adapter
def onClick(self, dialog: android.content.DialogInterface, id: int) -> void:
Log.i("TESTAPP", "User input: %s" % self.input_field.getText())
self.adapter.addItem(self.input_field.getText())
class DialogCancelClickListener(implements=android.content.DialogInterface[OnClickListener]):
def onClick(self, dialog: android.content.DialogInterface, id: int) -> void:
Log.i("TESTAPP", "Cancel dialog")
dialog.cancel()
class MainActivity(extends=android.support.v4.app.FragmentActivity):
# /** Called when the activity is first created. */
def onCreate(self, savedInstanceState: android.os.Bundle) -> void:
super().onCreate(savedInstanceState)
Log.i("TESTAPP", "CREATE APP")
adapter = DataAdapter(self)
adapter.data = []
adapter.context = self
adapter.updateData()
self.adapter = adapter
listview = SwipeMenuListView(self)
listview.setSwipeDirection(SwipeMenuListView.DIRECTION_LEFT)
layout = SwipeRefreshLayout(self)
layout.setLayoutParams(
LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
)
listener = RefreshListener(layout, adapter)
layout.setOnRefreshListener(listener)
listview.setAdapter(adapter)
listview.setMenuCreator(ListSwipeMenuCreator(self))
listview.setOnMenuItemClickListener(ListMenuItemClickListener(listview, adapter))
layout.addView(listview)
layout.setId(1234)
self.layout_id = layout.getId()
self.setContentView(layout)
def onCreateOptionsMenu(self, menu: android.view.Menu) -> bool:
Log.i("TESTAPP", "CREATE OPTIONS MENU %s" % menu)
item = menu.add(0, 42, 0, 'Add Item')
# item.setIcon(android.R.drawable.ic_input_add)
item.setIcon(android.R.drawable.ic_menu_add)
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
return True
def onOptionsItemSelected(self, item: android.view.MenuItem) -> bool:
item_id = item.getItemId()
if item_id == 42:
alertDialogBuilder = AlertDialog.Builder(self)
layout = LinearLayout(self)
layout.setLayoutParams(
LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT
)
)
layout.setPadding(
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16, self.getResources().getDisplayMetrics()),
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16, self.getResources().getDisplayMetrics()),
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16, self.getResources().getDisplayMetrics()),
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16, self.getResources().getDisplayMetrics())
)
layout.setOrientation(LinearLayout.VERTICAL)
input_field = EditText(self)
input_field.setLayoutParams(
LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
)
input_field.setHint("What to do...")
layout.addView(input_field)
alertDialogBuilder.setView(layout)
# set dialog message
alertDialogBuilder.setCancelable(False)
alertDialogBuilder.setPositiveButton("OK", DialogOKClickListener(input_field, self.adapter))
alertDialogBuilder.setNegativeButton("Cancel", DialogCancelClickListener())
# create alert dialog
alertDialog = alertDialogBuilder.create()
# show it
alertDialog.show()
return True
import toga
class Example(toga.App):
def startup(self):
self.list = toga.List(
widget_id='todo',
source='todo-list',
detail='todo-detail',
item_class=toga.SimpleListElement,
on_item_press=self.remove_entry
)
self.input = toga.TextInput(
widget_id='data',
placeholder="new todo",
flex=1, margin=5
)
container = toga.Container(
self.list,
toga.Container(
self.input,
toga.Button('Add', on_press=self.add_entry),
flex_direction='row'
),
flex_direction='column'
)
self.main_window.title = "Toga demo"
self.main_window.content = container
def add_entry(self, widget):
toga.post(self.list.create_url, {
'description': self.input.value(),
'completed': False,
})
self.list.add(self.input.value())
self.input.clear()
def remove_entry(self, widget):
if widget.delete_url:
toga.delete(widget.delete_url)
widget.remove()
else:
dom.window.alert("Can't delete new item")
from http.client import HTTPConnection
import json
import random
from rubicon.objc import objc_method
from toga_iOS.libs import *
class AddItemController(UIViewController):
@objc_method
def loadView(self) -> None:
self.title = 'Add item'
self.__dict__['cancelButton'] = UIBarButtonItem.alloc().initWithBarButtonSystemItem_target_action_(
UIBarButtonSystemItemCancel,
self,
get_selector('cancelClicked')
)
self.navigationController.navigationBar.topItem.leftBarButtonItem = self.__dict__['cancelButton']
self.__dict__['doneButton'] = UIBarButtonItem.alloc().initWithBarButtonSystemItem_target_action_(
UIBarButtonSystemItemDone,
self,
get_selector('doneClicked')
)
self.navigationController.navigationBar.topItem.rightBarButtonItem = self.__dict__['doneButton']
self.view = UIView.alloc().initWithFrame_(UIScreen.mainScreen().bounds)
self.view.setBackgroundColor_(UIColor.whiteColor())
self.__dict__['input'] = UITextField.alloc().init()
inputsize = self.__dict__['input'].systemLayoutSizeFittingSize_(CGSize(0, 0))
self.__dict__['input'].setFrame_(NSRect(NSPoint(10, 100), NSSize(300, inputsize.height + 14)))
self.__dict__['input'].setBorderStyle_(UITextBorderStyleRoundedRect)
self.__dict__['input'].setTranslatesAutoresizingMaskIntoConstraints_(False)
self.__dict__['input'].setAutoresizesSubviews_(False)
self.__dict__['input'].setPlaceholder_("what to do...")
self.__dict__['input'].becomeFirstResponder()
self.view.addSubview_(self.__dict__['input'])
@objc_method
def cancelClicked(self):
self.dismissModalViewControllerAnimated_(True)
@objc_method
def doneClicked(self):
self.dismissModalViewControllerAnimated_(True)
new_item = self.__dict__['input'].text
if new_item:
conn = HTTPConnection('freakboy3742.pythonanywhere.com')
conn.request(
'POST', '/api/todo/',
body=json.dumps({
'description': new_item,
'completed': False
}).encode('utf-8'),
headers={
'Content-type': 'application/json',
'Content-encoding': 'utf-8',
}
)
r = conn.getresponse()
data = json.loads(r.read().decode('utf8'))
self.__dict__['tablecontroller'].__dict__['data'].append(data)
self.__dict__['tablecontroller'].tableView.reloadData()
class TableViewController(UITableViewController):
@objc_method
def numberOfSectionsInTableView_(self) -> int:
return 1
@objc_method
def tableView_numberOfRowsInSection_(self, tableView, section: int) -> int:
return len(self.__dict__['data'])
@objc_method
def tableView_cellForRowAtIndexPath_(self, tableView, indexPath):
cell = tableView.dequeueReusableCellWithIdentifier_("row")
if cell is None:
cell = UITableViewCell.alloc().initWithStyle_reuseIdentifier_(UITableViewCellStyleDefault, "row")
cell.textLabel.text = self.__dict__['data'][indexPath.item]['description']
return cell
@objc_method
def tableView_commitEditingStyle_forRowAtIndexPath_(self, tableView, editingStyle: int, indexPath):
if editingStyle == UITableViewCellEditingStyleDelete:
item = self.__dict__['data'][indexPath.row]
conn = HTTPConnection('freakboy3742.pythonanywhere.com')
conn.request('DELETE', '/api/todo/%s/' % item['id'])
del self.__dict__['data'][indexPath.row]
paths = NSArray.alloc().initWithObjects_(indexPath, None)
tableView.deleteRowsAtIndexPaths_withRowAnimation_(paths, UITableViewRowAnimationFade)
@objc_method
def refreshTable(self):
conn = HTTPConnection('freakboy3742.pythonanywhere.com')
conn.request('GET', '/api/todo/')
r = conn.getresponse()
data = json.loads(r.read().decode('utf8'))
self.__dict__['data'] = data
self.refreshControl.endRefreshing()
self.tableView.reloadData()
@objc_method
def addClicked(self):
self.__dict__['additemcontroller'] = AddItemController.alloc().init()
self.__dict__['additemcontroller'].__dict__['tablecontroller'] = self
navigationController = UINavigationController.alloc().initWithRootViewController_(self.__dict__['additemcontroller'])
self.presentModalViewController_animated_(navigationController, True)
class PythonAppDelegate(UIResponder):
# @objc_method
# def applicationDidBecomeActive(self) -> None:
# print("BECAME ACTIVE")
@objc_method
def application_didFinishLaunchingWithOptions_(self, application, launchOptions) -> bool:
self.__dict__['tablecontroller'] = TableViewController.alloc().init()
self.__dict__['tablecontroller'].refreshControl = UIRefreshControl.alloc().init()
self.__dict__['tablecontroller'].refreshControl.addTarget_action_forControlEvents_(
self.__dict__['tablecontroller'],
get_selector('refreshTable'),
UIControlEventValueChanged
)
conn = HTTPConnection('freakboy3742.pythonanywhere.com')
conn.request('GET', '/api/todo/')
r = conn.getresponse()
data = json.loads(r.read().decode('utf8'))
self.__dict__['tablecontroller'].__dict__['data'] = data
self.__dict__['navcontroller'] = UINavigationController.alloc().initWithRootViewController_(self.__dict__['tablecontroller'])
# self.__dict__['navcontroller'].navigationBar.topItem.title = "Hello World"
self.__dict__['tablecontroller'].title = "TodoList"
self.__dict__['addButton'] = UIBarButtonItem.alloc().initWithBarButtonSystemItem_target_action_(
UIBarButtonSystemItemAdd,
self.__dict__['tablecontroller'],
get_selector('addClicked')
)
self.__dict__['navcontroller'].navigationBar.topItem.rightBarButtonItem = self.__dict__['addButton']
self.__dict__['window'] = UIWindow.alloc().initWithFrame_(UIScreen.mainScreen().bounds)
self.__dict__['window'].rootViewController = self.__dict__['navcontroller']
self.__dict__['window'].makeKeyAndVisible()
return True
import toga
class TodoApp(toga.App):
def startup(self):
self.list = toga.List(
widget_id='todo',
data=[
{'description': 'item 1'},
{'description': 'item 2'},
{'description': 'item 3'},
],
# item_class=toga.SimpleListElement,
on_delete=self.remove_entry,
on_refresh=self.refresh
)
self.input = toga.TextInput(placeholder="thing to do...")
self.add_item_dialog = toga.Dialog(
title="Add item",
content=toga.Container(
self.input
),
on_accept=self.add_entry
)
container = toga.NavigationView(
title="Todo List",
content=self.list,
on_action=self.show_add_dialog
)
self.main_window.content = container
def show_add_dialog(self, widget):
self.input.clear()
self.show_dialog(self.add_item_dialog)
def add_entry(self, widget):
if self.input.value:
self.list.add({'description': self.input.value})
def remove_entry(self, widget):
print("REMOVE ENTRY", widget)
def refresh(self, list_widget):
print("REFRESH LIST", list_widget)
if __name__ == '__main__':
app = TodoApp('TodoList', 'org.pybee.todolist')
@freakboy3742
Copy link
Author

freakboy3742 commented May 1, 2016

Three platforms (Web, iOS and Android), one app, native on each of them.

The Web app is delivered using Toga-web, a prototype wrapper API that that builds on top of Django's APIs. In the next few weeks, I'm going to provide a similar wrapped API for the Android and iOS versions. For the moment, Android and iOS are implemented using native platform APIs.

The iOS implementation uses Rubicon to bridge between Python and the Objective C runtime. It's running on a simulator because I needed my iPhone to shoot the video - but the app will run on the device as well.

The Android implementation uses VOC to compile Python to a set of Java classfiles. It also uses @baoyongzhang's SwipeListView implementation to get the "Swipe to Delete" behavior. It should run on any Android 3 device or higher.

Video of the apps running is on YouTube

@freakboy3742
Copy link
Author

toga-mobile.py shows the slightly different API required for mobile apps. This is to be expected; there are UI/UX metaphors that work on web apps that don't work on mobile.

@takluyver
Copy link

This is pretty amazing!

The methods remove_entry and refresh in the toga-mobile version aren't doing anything with the list. Are the delete and refresh functionality baked in to toga's List class, or is there a bit more code that's not shown in toga-mobile here? I'm guessing there must be a bit of extra code to handle syncing the data in the different apps, as shown in the video?

@freakboy3742
Copy link
Author

@takluyver You've caught me - that's a bit of detail that I glossed over in the interests of getting a demo working :-)

The issue was that when returning values from the call to the API POST, Batavia (the underlying Python->Javascript layer) was breaking. Longer term, this should be addressed, and the call to the API should be backed by receiving a response from the API and handling it.

@ata2001
Copy link

ata2001 commented Mar 27, 2017

Hi!

Could anyone please provide a briefcase setup.py example for android.py?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment