diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index cedf103..e0298c8 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -46,7 +46,6 @@ cd examples/hello-world && pn run android
- `templates/` – Android/iOS project templates and zips
- `examples/` – runnable example apps
- `hello-world/` – minimal demo app using the library
-- `experiments/` – platform experiments (Android/iOS/Briefcase)
- `README.md`, `pyproject.toml` – repo docs and packaging
## Coding guidelines
@@ -107,7 +106,6 @@ Recommended scopes (match the smallest accurate directory/module):
- Templates and examples:
- `templates` – `templates/` (Android/iOS templates, zips)
- `examples` – `examples/` (e.g., `hello-world/`)
- - `experiments` – `experiments/`
diff --git a/docs/api/pythonnative.md b/docs/api/pythonnative.md
index 744d7a4..0233735 100644
--- a/docs/api/pythonnative.md
+++ b/docs/api/pythonnative.md
@@ -2,8 +2,10 @@
API reference will be generated here via mkdocstrings.
-Key flags and helpers (0.2.0):
+Key flags and helpers:
- `pythonnative.utils.IS_ANDROID`: platform flag with robust detection for Chaquopy/Android.
- `pythonnative.utils.get_android_context()`: returns the current Android Activity/Context when running on Android.
- `pythonnative.utils.set_android_context(ctx)`: set by `pythonnative.Page` on Android; you generally don’t call this directly.
+- `pythonnative.utils.get_android_fragment_container()`: returns the current Fragment container `ViewGroup` used for page rendering.
+- `pythonnative.utils.set_android_fragment_container(viewGroup)`: set by the host `PageFragment`; you generally don’t call this directly.
diff --git a/docs/concepts/architecture.md b/docs/concepts/architecture.md
new file mode 100644
index 0000000..c7726dc
--- /dev/null
+++ b/docs/concepts/architecture.md
@@ -0,0 +1,63 @@
+# Architecture
+
+PythonNative maps Python directly to native platform APIs. Conceptually, it is closer to NativeScript's dynamic bindings than to React Native's bridge-and-module approach.
+
+## High-level model
+
+- Direct bindings: call native APIs synchronously from Python.
+ - iOS: rubicon-objc exposes Objective-C/Swift classes (e.g., UIViewController, UIButton, WKWebView) and lets you create dynamic Objective-C subclasses and selectors.
+ - Android: Chaquopy exposes Java classes (e.g., android.widget.Button, android.webkit.WebView) via the java bridge so you can construct and call methods directly.
+- Shared Python API: components like Page, StackView, Label, Button, and WebView have a small, consistent surface. Platform-specific behavior is chosen at import time using pythonnative.utils.IS_ANDROID.
+- Thin native bootstrap: the host app remains native (Android Activity or iOS UIViewController). It passes a live instance/pointer into Python, and Python drives the UI from there.
+
+## Comparison
+
+- Versus React Native: RN typically exposes capabilities via native modules/TurboModules and a bridge. PythonNative does not require authoring such modules for most APIs; you can access platform classes directly from Python.
+- Versus NativeScript: similar philosophy—dynamic, synchronous access to Obj-C/Java from the scripting runtime.
+
+## iOS flow (Rubicon-ObjC)
+
+- The iOS template (Swift + PythonKit) boots Python and instantiates your `MainPage` with the current `UIViewController` pointer.
+- In Python, Rubicon wraps the pointer; you then interact with UIKit classes directly.
+
+```python
+from rubicon.objc import ObjCClass, ObjCInstance
+
+UIButton = ObjCClass("UIButton")
+vc = ObjCInstance(native_ptr) # passed from Swift template
+button = UIButton.alloc().init()
+# Configure target/action via a dynamic Objective-C subclass (see Button implementation)
+```
+
+## Android flow (Chaquopy)
+
+- The Android template (Kotlin + Chaquopy) initializes Python in MainActivity and provides the current Activity/Context to Python.
+- Components acquire the Context implicitly and construct real Android views.
+
+```python
+from java import jclass
+from pythonnative.utils import get_android_context
+
+WebViewClass = jclass("android.webkit.WebView")
+context = get_android_context()
+webview = WebViewClass(context)
+webview.loadUrl("https://example.com")
+```
+
+## Key implications
+
+- Synchronous native calls: no JS bridge; Python calls are direct.
+- Lifecycle rules remain native: Activities/ViewControllers are created by the OS. Python receives and controls them; it does not instantiate Android Activities directly.
+- Small, growing surface: the shared Python API favors clarity and consistency, expanding progressively.
+
+## Navigation model overview
+
+- See the Navigation guide for full details and comparisons with other frameworks.
+ - iOS: one host `UIViewController` class, many instances pushed on a `UINavigationController`.
+ - Android: single host `Activity` with a `NavHostFragment` and a stack of generic `PageFragment`s driven by a navigation graph.
+
+## Related docs
+
+- Guides / Android: guides/android.md
+- Guides / iOS: guides/ios.md
+- Concepts / Components: concepts/components.md
diff --git a/docs/concepts/components.md b/docs/concepts/components.md
index e1e33aa..e9d12ca 100644
--- a/docs/concepts/components.md
+++ b/docs/concepts/components.md
@@ -2,7 +2,7 @@
High-level overview of PythonNative components and how they map to native UI.
-## Constructor pattern (0.2.0)
+## Constructor pattern
- All core components share a consistent, contextless constructor on both platforms.
- On Android, a `Context` is acquired implicitly from the current `Activity` set by `pn.Page`.
@@ -30,7 +30,7 @@ Notes:
- `pn.Page` stores the Android `Activity` so components like `pn.Button()` and `pn.Label()` can construct their native counterparts.
- If you construct views before the `Page` is created on Android, a runtime error will be raised because no `Context` is available.
-## Core components (0.2.0)
+## Core components
Stabilized with contextless constructors on both platforms:
diff --git a/docs/getting-started.md b/docs/getting-started.md
index bd5fb67..f35ff23 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -18,7 +18,7 @@ This scaffolds:
- `requirements.txt`
- `.gitignore`
-A minimal `app/main_page.py` looks like:
+A minimal `app/main_page.py` looks like (no bootstrap needed):
```python
import pythonnative as pn
@@ -36,13 +36,6 @@ class MainPage(pn.Page):
button.set_on_click(lambda: print("Button clicked"))
stack.add_view(button)
self.set_root_view(stack)
-
-
-def bootstrap(native_instance):
- """Entry point called by the host app (Activity or ViewController)."""
- page = MainPage(native_instance)
- page.on_create()
- return page
```
## Run on a platform
diff --git a/docs/guides/navigation.md b/docs/guides/navigation.md
new file mode 100644
index 0000000..7ae0f4c
--- /dev/null
+++ b/docs/guides/navigation.md
@@ -0,0 +1,95 @@
+# Navigation
+
+This guide shows how to navigate between pages and handle lifecycle events.
+
+## Push / Pop
+
+Use `push` and `pop` on your `Page` to change screens. You can pass a dotted path string or a class reference.
+
+```python
+import pythonnative as pn
+
+class MainPage(pn.Page):
+ def on_create(self):
+ stack = pn.StackView()
+ btn = pn.Button("Go next")
+ btn.set_on_click(lambda: self.push("app.second_page.SecondPage", args={"message": "Hello"}))
+ stack.add_view(btn)
+ self.set_root_view(stack)
+```
+
+On the target page:
+
+```python
+class SecondPage(pn.Page):
+ def on_create(self):
+ args = self.get_args()
+ message = args.get("message", "Second")
+ stack = pn.StackView()
+ stack.add_view(pn.Label(message))
+ back = pn.Button("Back")
+ back.set_on_click(lambda: self.pop())
+ stack.add_view(back)
+ self.set_root_view(stack)
+```
+
+## Lifecycle
+
+PythonNative forwards lifecycle events from the host:
+
+- `on_create`
+- `on_start`
+- `on_resume`
+- `on_pause`
+- `on_stop`
+- `on_destroy`
+- `on_restart` (Android only)
+- `on_save_instance_state`
+- `on_restore_instance_state`
+
+Android uses a single `MainActivity` hosting a `NavHostFragment` and a generic `PageFragment` per page. iOS forwards `viewWillAppear`/`viewWillDisappear` via an internal registry.
+
+## Notes
+
+- On Android, `push` navigates via `NavController` to a `PageFragment` and passes `page_path` and optional JSON `args`.
+- On iOS, `push` uses the root `UINavigationController` to push a new `ViewController` and passes page info via KVC.
+
+## Platform specifics
+
+### iOS (UIViewController per page)
+- Each PythonNative page is hosted by a Swift `ViewController` instance.
+- Pages are pushed and popped on a root `UINavigationController`.
+- Lifecycle is forwarded from Swift to the registered Python page instance.
+- Root view wiring: `Page.set_root_view` sizes and inserts the Python-native view into the controller’s view.
+
+Why this matches iOS conventions
+- iOS apps commonly model screens as `UIViewController`s and use `UINavigationController` for hierarchical navigation.
+- The approach integrates cleanly with add-to-app and system behaviors (e.g., state restoration).
+
+### Android (single Activity, Fragment stack)
+- Single host `MainActivity` sets a `NavHostFragment` containing a navigation graph.
+- Each PythonNative page is represented by a generic `PageFragment` which instantiates the Python page and attaches its root view.
+- `push`/`pop` delegate to `NavController` (via a small `Navigator` helper).
+- Arguments (`page_path`, `args_json`) live in Fragment arguments and restore across configuration changes and process death.
+
+Why this matches Android conventions
+- Modern Android apps favor one Activity with many Fragments, using Jetpack Navigation for back stack, transitions, and deep links.
+- It simplifies lifecycle, back handling, and state compared to one-Activity-per-screen.
+
+## Comparison to other frameworks
+- React Native
+ - Android: single `Activity`, screens managed via `Fragment`s (e.g., `react-native-screens`).
+ - iOS: screens map to `UIViewController`s pushed on `UINavigationController`.
+- .NET MAUI / Xamarin.Forms
+ - Android: single `Activity`, pages via Fragments/Navigation.
+ - iOS: pages map to `UIViewController`s on a `UINavigationController`.
+- NativeScript
+ - Android: single `Activity`, pages as `Fragment`s.
+ - iOS: pages as `UIViewController`s on `UINavigationController`.
+- Flutter (special case)
+ - Android: single `Activity` (`FlutterActivity`/`FlutterFragmentActivity`).
+ - iOS: `FlutterViewController` hosts Flutter’s internal navigator; add-to-app can push multiple `FlutterViewController`s.
+
+Bottom line
+- iOS: one host VC class, many instances on a `UINavigationController`.
+- Android: one host `Activity`, many `Fragment`s with Jetpack Navigation.
diff --git a/examples/hello-world/app/main_page.py b/examples/hello-world/app/main_page.py
index 8a36892..032ef3b 100644
--- a/examples/hello-world/app/main_page.py
+++ b/examples/hello-world/app/main_page.py
@@ -22,8 +22,20 @@ def on_create(self):
except Exception:
pass
stack.add_view(pn.Label("Hello from PythonNative Demo!"))
- button = pn.Button("Tap me")
- button.set_on_click(lambda: print("Demo button clicked"))
+ button = pn.Button("Go to Second Page")
+
+ def on_next():
+ # Visual confirmation that tap worked (iOS only)
+ try:
+ if UIColor is not None:
+ button.native_instance.setBackgroundColor_(UIColor.systemGreenColor())
+ button.native_instance.setTitleColor_forState_(UIColor.whiteColor(), 0)
+ except Exception:
+ pass
+ # Demonstrate passing args
+ self.push("app.second_page.SecondPage", args={"message": "Greetings from MainPage"})
+
+ button.set_on_click(on_next)
# Make the button visually obvious
try:
if UIColor is not None:
@@ -57,9 +69,3 @@ def on_save_instance_state(self):
def on_restore_instance_state(self):
super().on_restore_instance_state()
-
-
-def bootstrap(native_instance):
- page = MainPage(native_instance)
- page.on_create()
- return page
diff --git a/examples/hello-world/app/second_page.py b/examples/hello-world/app/second_page.py
index 9be9495..99af521 100644
--- a/examples/hello-world/app/second_page.py
+++ b/examples/hello-world/app/second_page.py
@@ -1,5 +1,13 @@
import pythonnative as pn
+try:
+ # Optional: iOS styling support (safe if rubicon isn't available)
+ from rubicon.objc import ObjCClass
+
+ UIColor = ObjCClass("UIColor")
+except Exception: # pragma: no cover
+ UIColor = None
+
class SecondPage(pn.Page):
def __init__(self, native_instance):
@@ -8,8 +16,35 @@ def __init__(self, native_instance):
def on_create(self):
super().on_create()
stack_view = pn.StackView()
- label = pn.Label("Second page!")
- stack_view.add_view(label)
+ # Read args passed from MainPage
+ args = self.get_args()
+ message = args.get("message", "Second page!")
+ stack_view.add_view(pn.Label(message))
+ # Navigate to Third Page
+ to_third_btn = pn.Button("Go to Third Page")
+ # Style button on iOS similar to MainPage
+ try:
+ if UIColor is not None:
+ to_third_btn.native_instance.setBackgroundColor_(UIColor.systemBlueColor())
+ to_third_btn.native_instance.setTitleColor_forState_(UIColor.whiteColor(), 0)
+ except Exception:
+ pass
+
+ def on_next():
+ # Visual confirmation that tap worked (iOS only)
+ try:
+ if UIColor is not None:
+ to_third_btn.native_instance.setBackgroundColor_(UIColor.systemGreenColor())
+ to_third_btn.native_instance.setTitleColor_forState_(UIColor.whiteColor(), 0)
+ except Exception:
+ pass
+ self.push("app.third_page.ThirdPage", args={"from": "Second"})
+
+ to_third_btn.set_on_click(on_next)
+ stack_view.add_view(to_third_btn)
+ back_btn = pn.Button("Back")
+ back_btn.set_on_click(lambda: self.pop())
+ stack_view.add_view(back_btn)
self.set_root_view(stack_view)
def on_start(self):
diff --git a/examples/hello-world/app/third_page.py b/examples/hello-world/app/third_page.py
new file mode 100644
index 0000000..6c06594
--- /dev/null
+++ b/examples/hello-world/app/third_page.py
@@ -0,0 +1,30 @@
+import pythonnative as pn
+
+try:
+ # Optional: iOS styling support (safe if rubicon isn't available)
+ from rubicon.objc import ObjCClass
+
+ UIColor = ObjCClass("UIColor")
+except Exception: # pragma: no cover
+ UIColor = None
+
+
+class ThirdPage(pn.Page):
+ def __init__(self, native_instance):
+ super().__init__(native_instance)
+
+ def on_create(self):
+ super().on_create()
+ stack = pn.StackView()
+ stack.add_view(pn.Label("This is the Third Page"))
+ back_btn = pn.Button("Back")
+ # Style button on iOS similar to MainPage
+ try:
+ if UIColor is not None:
+ back_btn.native_instance.setBackgroundColor_(UIColor.systemBlueColor())
+ back_btn.native_instance.setTitleColor_forState_(UIColor.whiteColor(), 0)
+ except Exception:
+ pass
+ back_btn.set_on_click(lambda: self.pop())
+ stack.add_view(back_btn)
+ self.set_root_view(stack)
diff --git a/experiments/android_pythonnative_3/.gitignore b/experiments/android_pythonnative_3/.gitignore
deleted file mode 100644
index aa724b7..0000000
--- a/experiments/android_pythonnative_3/.gitignore
+++ /dev/null
@@ -1,15 +0,0 @@
-*.iml
-.gradle
-/local.properties
-/.idea/caches
-/.idea/libraries
-/.idea/modules.xml
-/.idea/workspace.xml
-/.idea/navEditor.xml
-/.idea/assetWizardSettings.xml
-.DS_Store
-/build
-/captures
-.externalNativeBuild
-.cxx
-local.properties
diff --git a/experiments/android_pythonnative_3/app/.gitignore b/experiments/android_pythonnative_3/app/.gitignore
deleted file mode 100644
index 42afabf..0000000
--- a/experiments/android_pythonnative_3/app/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/build
\ No newline at end of file
diff --git a/experiments/android_pythonnative_3/app/build.gradle b/experiments/android_pythonnative_3/app/build.gradle
deleted file mode 100644
index 50bbbfe..0000000
--- a/experiments/android_pythonnative_3/app/build.gradle
+++ /dev/null
@@ -1,59 +0,0 @@
-plugins {
- id 'com.android.application'
- id 'org.jetbrains.kotlin.android'
- id 'com.chaquo.python'
-}
-
-android {
- namespace 'com.pythonnative.pythonnative'
- compileSdk 33
-
- defaultConfig {
- applicationId "com.pythonnative.pythonnative"
- minSdk 24
- targetSdk 33
- versionCode 1
- versionName "1.0"
-
- testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
- ndk {
- abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
- }
- python {
- pip {
- // https://chaquo.com/chaquopy/doc/current/android.html#android-requirements
-// install "matplotlib"
-// install "pythonnative"
-
- // A directory containing a setup.py, relative to the project
- // directory (must contain at least one slash):
- install "/Users/owenthcarey/Documents/pythonnative-workspace/libs/pythonnative"
- }
- }
- }
-
- buildTypes {
- release {
- minifyEnabled false
- proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
- }
- }
- compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
- }
- kotlinOptions {
- jvmTarget = '1.8'
- }
-}
-
-dependencies {
-
- implementation 'androidx.core:core-ktx:1.8.0'
- implementation 'androidx.appcompat:appcompat:1.6.1'
- implementation 'com.google.android.material:material:1.5.0'
- implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
- testImplementation 'junit:junit:4.13.2'
- androidTestImplementation 'androidx.test.ext:junit:1.1.5'
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
-}
\ No newline at end of file
diff --git a/experiments/android_pythonnative_3/app/proguard-rules.pro b/experiments/android_pythonnative_3/app/proguard-rules.pro
deleted file mode 100644
index 481bb43..0000000
--- a/experiments/android_pythonnative_3/app/proguard-rules.pro
+++ /dev/null
@@ -1,21 +0,0 @@
-# Add project specific ProGuard rules here.
-# You can control the set of applied configuration files using the
-# proguardFiles setting in build.gradle.
-#
-# For more details, see
-# http://developer.android.com/guide/developing/tools/proguard.html
-
-# If your project uses WebView with JS, uncomment the following
-# and specify the fully qualified class name to the JavaScript interface
-# class:
-#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
-# public *;
-#}
-
-# Uncomment this to preserve the line number information for
-# debugging stack traces.
-#-keepattributes SourceFile,LineNumberTable
-
-# If you keep the line number information, uncomment this to
-# hide the original source file name.
-#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/experiments/android_pythonnative_3/app/src/androidTest/java/com/pythonnative/pythonnative/ExampleInstrumentedTest.kt b/experiments/android_pythonnative_3/app/src/androidTest/java/com/pythonnative/pythonnative/ExampleInstrumentedTest.kt
deleted file mode 100644
index e2269ba..0000000
--- a/experiments/android_pythonnative_3/app/src/androidTest/java/com/pythonnative/pythonnative/ExampleInstrumentedTest.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.pythonnative.pythonnative
-
-import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.ext.junit.runners.AndroidJUnit4
-
-import org.junit.Test
-import org.junit.runner.RunWith
-
-import org.junit.Assert.*
-
-/**
- * Instrumented test, which will execute on an Android device.
- *
- * See [testing documentation](http://d.android.com/tools/testing).
- */
-@RunWith(AndroidJUnit4::class)
-class ExampleInstrumentedTest {
- @Test
- fun useAppContext() {
- // Context of the app under test.
- val appContext = InstrumentationRegistry.getInstrumentation().targetContext
- assertEquals("com.pythonnative.pythonnative", appContext.packageName)
- }
-}
\ No newline at end of file
diff --git a/experiments/android_pythonnative_3/app/src/main/AndroidManifest.xml b/experiments/android_pythonnative_3/app/src/main/AndroidManifest.xml
deleted file mode 100644
index 411d3d1..0000000
--- a/experiments/android_pythonnative_3/app/src/main/AndroidManifest.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/experiments/android_pythonnative_3/app/src/main/java/com/pythonnative/pythonnative/MainActivity.kt b/experiments/android_pythonnative_3/app/src/main/java/com/pythonnative/pythonnative/MainActivity.kt
deleted file mode 100644
index b29e93e..0000000
--- a/experiments/android_pythonnative_3/app/src/main/java/com/pythonnative/pythonnative/MainActivity.kt
+++ /dev/null
@@ -1,144 +0,0 @@
-package com.pythonnative.pythonnative
-
-import android.graphics.BitmapFactory
-import androidx.appcompat.app.AppCompatActivity
-import android.os.Bundle
-import android.util.Log
-import android.widget.Button
-import android.widget.ImageView
-import android.widget.TextView
-import android.graphics.Color
-import android.view.View
-import android.widget.LinearLayout
-import androidx.constraintlayout.widget.ConstraintLayout
-import androidx.recyclerview.widget.RecyclerView
-import com.chaquo.python.PyException
-import com.chaquo.python.PyObject
-import com.chaquo.python.Python
-import com.chaquo.python.android.AndroidPlatform
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import org.json.JSONObject
-
-class MainActivity : AppCompatActivity() {
- private val TAG = javaClass.simpleName
- private lateinit var page: PyObject
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- Log.d(TAG, "onCreate() called")
-
-// setContentView(R.layout.activity_main)
-// val layoutMain = findViewById(R.id.layout_main)
-
- // Initialize Chaquopy
- if (!Python.isStarted()) {
- Python.start(AndroidPlatform(this))
- }
- val py = Python.getInstance()
-
- // Create an instance of the Page class
-// val pyModule = py.getModule("app/main_2")
-// page = pyModule.callAttr("Page", this)
-// val pyLayout = page.callAttr("on_create").toJava(View::class.java)
-// setContentView(pyLayout)
-
- // Create an instance of the Page class
- val pyModule = py.getModule("app/main_3")
- page = pyModule.callAttr("MainPage", this)
- page.callAttr("on_create")
-
-// val pyModule = py.getModule("app/main")
-// val pyLayout = pyModule.callAttr("on_create", this).toJava(View::class.java)
-// setContentView(pyLayout)
-
-// val createButtonModule = py.getModule("create_button")
-// val pyButton = createButtonModule.callAttr("create_button", this).toJava(Button::class.java)
-// layoutMain.addView(pyButton)
-
-// val createWidgetsModule = py.getModule("create_widgets")
-// val pyLayout = createWidgetsModule.callAttr("create_widgets", this).toJava(LinearLayout::class.java)
-// layoutMain.addView(pyLayout)
-
-// val createConstraintLayoutModule = py.getModule("create_constraint_layout")
-// val pyLayout = createConstraintLayoutModule.callAttr("create_constraint_layout", this).toJava(ConstraintLayout::class.java)
-// layoutMain.addView(pyLayout)
-
-// val createRecyclerViewModule = py.getModule("create_recycler_view")
-// val pyRecyclerView = createRecyclerViewModule.callAttr("create_recycler_view", this).toJava(RecyclerView::class.java)
-// layoutMain.addView(pyRecyclerView)
-
- // Existing code for displaying plot
-// val imageView = findViewById(R.id.image_home)
-// val plotModule = py.getModule("plot")
-// val xInput = "1 2 3 4 5"
-// val yInput = "1 4 9 16 25"
-// CoroutineScope(Dispatchers.Main).launch {
-// try {
-// val bytes = plotModule.callAttr(
-// "plot",
-// xInput,
-// yInput
-// ).toJava(ByteArray::class.java)
-// withContext(Dispatchers.IO) {
-// val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
-// withContext(Dispatchers.Main) {
-// imageView.setImageBitmap(bitmap)
-// }
-// }
-// } catch (e: PyException) {
-// Log.e("Python Error", "Error executing Python code", e)
-// }
-// }
- }
-
- override fun onStart() {
- super.onStart()
- Log.d(TAG, "onStart() called")
- page.callAttr("on_start")
- }
-
- override fun onResume() {
- super.onResume()
- Log.d(TAG, "onResume() called")
- page.callAttr("on_resume")
- }
-
- override fun onPause() {
- super.onPause()
- Log.d(TAG, "onPause() called")
- page.callAttr("on_pause")
- }
-
- override fun onStop() {
- super.onStop()
- Log.d(TAG, "onStop() called")
- page.callAttr("on_stop")
- }
-
- override fun onDestroy() {
- super.onDestroy()
- Log.d(TAG, "onDestroy() called")
- page.callAttr("on_destroy")
- }
-
- override fun onRestart() {
- super.onRestart()
- Log.d(TAG, "onRestart() called")
- page.callAttr("on_restart")
- }
-
- override fun onSaveInstanceState(outState: Bundle) {
- super.onSaveInstanceState(outState)
- Log.d(TAG, "onSaveInstanceState() called")
- page.callAttr("on_save_instance_state")
- }
-
- override fun onRestoreInstanceState(savedInstanceState: Bundle) {
- super.onRestoreInstanceState(savedInstanceState)
- Log.d(TAG, "onRestoreInstanceState() called")
- page.callAttr("on_restore_instance_state")
- }
-}
diff --git a/experiments/android_pythonnative_3/app/src/main/java/com/pythonnative/pythonnative/SecondActivity.kt b/experiments/android_pythonnative_3/app/src/main/java/com/pythonnative/pythonnative/SecondActivity.kt
deleted file mode 100644
index e377e5e..0000000
--- a/experiments/android_pythonnative_3/app/src/main/java/com/pythonnative/pythonnative/SecondActivity.kt
+++ /dev/null
@@ -1,73 +0,0 @@
-package com.pythonnative.pythonnative
-
-import androidx.appcompat.app.AppCompatActivity
-import android.os.Bundle
-import android.util.Log
-import com.chaquo.python.PyObject
-import com.chaquo.python.Python
-import com.chaquo.python.android.AndroidPlatform
-
-class SecondActivity : AppCompatActivity() {
- private val TAG = javaClass.simpleName
- private lateinit var page: PyObject
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- Log.d(TAG, "onCreate() called")
- if (!Python.isStarted()) {
- Python.start(AndroidPlatform(this))
- }
- val py = Python.getInstance()
- val pyModule = py.getModule("app/second_page")
- page = pyModule.callAttr("SecondPage", this)
- page.callAttr("on_create")
- }
-
- override fun onStart() {
- super.onStart()
- Log.d(TAG, "onStart() called")
- page.callAttr("on_start")
- }
-
- override fun onResume() {
- super.onResume()
- Log.d(TAG, "onResume() called")
- page.callAttr("on_resume")
- }
-
- override fun onPause() {
- super.onPause()
- Log.d(TAG, "onPause() called")
- page.callAttr("on_pause")
- }
-
- override fun onStop() {
- super.onStop()
- Log.d(TAG, "onStop() called")
- page.callAttr("on_stop")
- }
-
- override fun onDestroy() {
- super.onDestroy()
- Log.d(TAG, "onDestroy() called")
- page.callAttr("on_destroy")
- }
-
- override fun onRestart() {
- super.onRestart()
- Log.d(TAG, "onRestart() called")
- page.callAttr("on_restart")
- }
-
- override fun onSaveInstanceState(outState: Bundle) {
- super.onSaveInstanceState(outState)
- Log.d(TAG, "onSaveInstanceState() called")
- page.callAttr("on_save_instance_state")
- }
-
- override fun onRestoreInstanceState(savedInstanceState: Bundle) {
- super.onRestoreInstanceState(savedInstanceState)
- Log.d(TAG, "onRestoreInstanceState() called")
- page.callAttr("on_restore_instance_state")
- }
-}
\ No newline at end of file
diff --git a/experiments/android_pythonnative_3/app/src/main/python/app/__init__.py b/experiments/android_pythonnative_3/app/src/main/python/app/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/experiments/android_pythonnative_3/app/src/main/python/app/main.py b/experiments/android_pythonnative_3/app/src/main/python/app/main.py
deleted file mode 100644
index 2eab767..0000000
--- a/experiments/android_pythonnative_3/app/src/main/python/app/main.py
+++ /dev/null
@@ -1,109 +0,0 @@
-import pythonnative as pn
-
-
-def on_create(context):
- stack_view = pn.StackView(context)
-
- # label = pn.Label(context, "This is a PythonNative label")
- # stack_view.add_view(label)
- #
- # switch = pn.Switch(context)
- # stack_view.add_view(switch)
- #
- # text_field = pn.TextField(context)
- # stack_view.add_view(text_field)
- #
- # text_view = pn.TextView(context)
- # stack_view.add_view(text_view)
-
- activity_indicator_view = pn.ActivityIndicatorView(context)
- activity_indicator_view.start_animating()
- stack_view.add_view(activity_indicator_view)
-
- material_activity_indicator_view = pn.MaterialActivityIndicatorView(context)
- material_activity_indicator_view.start_animating()
- stack_view.add_view(material_activity_indicator_view)
-
- progress_view = pn.ProgressView(context)
- progress_view.set_progress(0.5)
- stack_view.add_view(progress_view)
-
- material_progress_view = pn.MaterialProgressView(context)
- material_progress_view.set_progress(0.5)
- stack_view.add_view(material_progress_view)
-
- material_button = pn.MaterialButton(context, "MaterialButton")
- stack_view.add_view(material_button)
-
- search_bar = pn.SearchBar(context)
- stack_view.add_view(search_bar)
-
- image_view = pn.ImageView(context)
- stack_view.add_view(image_view)
-
- picker_view = pn.PickerView(context)
- stack_view.add_view(picker_view)
-
- # date_picker = pn.DatePicker(context)
- # stack_view.add_view(date_picker)
-
- # time_picker = pn.TimePicker(context)
- # stack_view.add_view(time_picker)
-
- # TODO: fix
- # material_time_picker = pn.MaterialTimePicker(context)
- # stack_view.add_view(material_time_picker)
-
- # TODO: fix
- # material_date_picker = pn.MaterialDatePicker(context)
- # stack_view.add_view(material_date_picker)
-
- # TODO: fix
- # material_switch = pn.MaterialSwitch(context)
- # stack_view.add_view(material_switch)
-
- # TODO: fix
- # material_search_bar = pn.MaterialSearchBar(context)
- # stack_view.add_view(material_search_bar)
-
- # web_view = pn.WebView(context)
- # web_view.load_url("https://www.djangoproject.com/")
- # stack_view.add_view(web_view)
- #
- # for i in range(100):
- # button = pn.Button(context, "Click me")
- # stack_view.add_view(button)
-
- return stack_view.native_instance
-
-
-def on_start():
- print("on_start() called")
-
-
-def on_resume():
- print("on_resume() called")
-
-
-def on_pause():
- print("on_pause() called")
-
-
-def on_stop():
- print("on_stop() called")
-
-
-def on_destroy():
- print("on_destroy() called")
-
-
-def on_restart():
- print("on_restart() called")
-
-
-def on_save_instance_state():
- print("on_save_instance_state() called")
-
-
-def on_restore_instance_state():
- print("on_restore_instance_state() called")
diff --git a/experiments/android_pythonnative_3/app/src/main/python/app/main_2.py b/experiments/android_pythonnative_3/app/src/main/python/app/main_2.py
deleted file mode 100644
index 1dba4ee..0000000
--- a/experiments/android_pythonnative_3/app/src/main/python/app/main_2.py
+++ /dev/null
@@ -1,38 +0,0 @@
-import pythonnative as pn
-
-
-class Page:
- def __init__(self, context):
- self.context = context
-
- def on_create(self):
- print("on_create() called")
- stack_view = pn.StackView(self.context)
- material_button = pn.MaterialButton(self.context, "MaterialButton")
- stack_view.add_view(material_button)
- # Create and add other views to the stack_view here
- return stack_view.native_instance
-
- def on_start(self):
- print("on_start() called")
-
- def on_resume(self):
- print("on_resume() called")
-
- def on_pause(self):
- print("on_pause() called")
-
- def on_stop(self):
- print("on_stop() called")
-
- def on_destroy(self):
- print("on_destroy() called")
-
- def on_restart(self):
- print("on_restart() called")
-
- def on_save_instance_state(self):
- print("on_save_instance_state() called")
-
- def on_restore_instance_state(self):
- print("on_restore_instance_state() called")
diff --git a/experiments/android_pythonnative_3/app/src/main/python/app/main_3.py b/experiments/android_pythonnative_3/app/src/main/python/app/main_3.py
deleted file mode 100644
index 6c24d43..0000000
--- a/experiments/android_pythonnative_3/app/src/main/python/app/main_3.py
+++ /dev/null
@@ -1,42 +0,0 @@
-import pythonnative as pn
-
-
-class MainPage(pn.Page):
- def __init__(self, native_instance):
- super().__init__(native_instance)
-
- def on_create(self):
- super().on_create()
- stack_view = pn.StackView(self.native_instance)
- # list_data = ["item_{}".format(i) for i in range(100)]
- # list_view = pn.ListView(self.native_instance, list_data)
- # stack_view.add_view(list_view)
- button = pn.Button(self.native_instance, "Button")
- button.set_on_click(lambda: self.navigate_to(""))
- # button.set_on_click(lambda: print("Button was clicked!"))
- stack_view.add_view(button)
- self.set_root_view(stack_view)
-
- def on_start(self):
- super().on_start()
-
- def on_resume(self):
- super().on_resume()
-
- def on_pause(self):
- super().on_pause()
-
- def on_stop(self):
- super().on_stop()
-
- def on_destroy(self):
- super().on_destroy()
-
- def on_restart(self):
- super().on_restart()
-
- def on_save_instance_state(self):
- super().on_save_instance_state()
-
- def on_restore_instance_state(self):
- super().on_restore_instance_state()
diff --git a/experiments/android_pythonnative_3/app/src/main/python/app/second_page.py b/experiments/android_pythonnative_3/app/src/main/python/app/second_page.py
deleted file mode 100644
index 442fb53..0000000
--- a/experiments/android_pythonnative_3/app/src/main/python/app/second_page.py
+++ /dev/null
@@ -1,37 +0,0 @@
-import pythonnative as pn
-
-
-class SecondPage(pn.Page):
- def __init__(self, native_instance):
- super().__init__(native_instance)
-
- def on_create(self):
- super().on_create()
- stack_view = pn.StackView(self.native_instance)
- label = pn.Label(self.native_instance, "Second page!")
- stack_view.add_view(label)
- self.set_root_view(stack_view)
-
- def on_start(self):
- super().on_start()
-
- def on_resume(self):
- super().on_resume()
-
- def on_pause(self):
- super().on_pause()
-
- def on_stop(self):
- super().on_stop()
-
- def on_destroy(self):
- super().on_destroy()
-
- def on_restart(self):
- super().on_restart()
-
- def on_save_instance_state(self):
- super().on_save_instance_state()
-
- def on_restore_instance_state(self):
- super().on_restore_instance_state()
diff --git a/experiments/android_pythonnative_3/app/src/main/python/create_button.py b/experiments/android_pythonnative_3/app/src/main/python/create_button.py
deleted file mode 100644
index 1924e4a..0000000
--- a/experiments/android_pythonnative_3/app/src/main/python/create_button.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from java import cast, chaquopy, dynamic_proxy, jarray, jclass
-
-
-def create_button(context):
- Button = jclass("android.widget.Button")
- button = Button(context)
- button.setText("Button created in Python")
- return button
diff --git a/experiments/android_pythonnative_3/app/src/main/python/create_constraint_layout.py b/experiments/android_pythonnative_3/app/src/main/python/create_constraint_layout.py
deleted file mode 100644
index 9d295bd..0000000
--- a/experiments/android_pythonnative_3/app/src/main/python/create_constraint_layout.py
+++ /dev/null
@@ -1,46 +0,0 @@
-from java import jclass
-
-
-BottomNavigationView = jclass(
- "com.google.android.material.bottomnavigation.BottomNavigationView"
-)
-ConstraintLayout = jclass("androidx.constraintlayout.widget.ConstraintLayout")
-View = jclass("android.view.View")
-ViewGroup = jclass("android.view.ViewGroup")
-
-
-def create_constraint_layout(context):
- # Create ConstraintLayout
- layout = ConstraintLayout(context)
- layout_params = ViewGroup.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT
- )
- layout.setLayoutParams(layout_params)
-
- # Create BottomNavigationView
- bottom_nav = BottomNavigationView(context)
- bottom_nav.setId(
- View.generateViewId()
- ) # Add this line to generate unique id for the view
-
- # Create Menu for BottomNavigationView
- menu = bottom_nav.getMenu()
-
- # Add items to the menu
- menu.add(0, 0, 0, "Home")
- menu.add(0, 1, 0, "Search")
- menu.add(0, 2, 0, "Notifications")
- menu.add(0, 3, 0, "Messages")
- menu.add(0, 4, 0, "Profile")
-
- # Add BottomNavigationView to ConstraintLayout
- nav_layout_params = ConstraintLayout.LayoutParams(
- ConstraintLayout.LayoutParams.MATCH_PARENT,
- ConstraintLayout.LayoutParams.WRAP_CONTENT,
- )
- # Set the constraints here
- nav_layout_params.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID
- bottom_nav.setLayoutParams(nav_layout_params)
- layout.addView(bottom_nav)
-
- return layout
diff --git a/experiments/android_pythonnative_3/app/src/main/python/create_pn_layout.py b/experiments/android_pythonnative_3/app/src/main/python/create_pn_layout.py
deleted file mode 100644
index b4a7fde..0000000
--- a/experiments/android_pythonnative_3/app/src/main/python/create_pn_layout.py
+++ /dev/null
@@ -1,13 +0,0 @@
-import pythonnative as pn
-
-
-def create_pn_layout(context):
- layout = pn.StackView(context)
-
- label = pn.Label(context, "This is a PythonNative label")
- layout.add_view(label)
-
- button = pn.Button(context, "Click me")
- layout.add_view(button)
-
- return layout.native_instance
diff --git a/experiments/android_pythonnative_3/app/src/main/python/create_recycler_view.py b/experiments/android_pythonnative_3/app/src/main/python/create_recycler_view.py
deleted file mode 100644
index 973c75a..0000000
--- a/experiments/android_pythonnative_3/app/src/main/python/create_recycler_view.py
+++ /dev/null
@@ -1,42 +0,0 @@
-from java import jclass, static_proxy, Override
-
-LinearLayoutManager = jclass("androidx.recyclerview.widget.LinearLayoutManager")
-RecyclerView = jclass("androidx.recyclerview.widget.RecyclerView")
-TextView = jclass("android.widget.TextView")
-
-
-# RecyclerView ViewHolder
-class MyViewHolder(static_proxy(RecyclerView.ViewHolder)):
- def __init__(self, item_view):
- super(MyViewHolder, self).__init__(item_view)
- self.my_text_view = TextView(item_view.getContext())
-
-
-# RecyclerView Adapter
-class MyAdapter(static_proxy(RecyclerView.Adapter)):
- def __init__(self, my_dataset):
- self.my_dataset = my_dataset
-
- @Override(RecyclerView.Adapter)
- def onCreateViewHolder(self, parent, viewType):
- text_view = TextView(parent.getContext())
- return MyViewHolder(text_view)
-
- @Override(RecyclerView.Adapter)
- def onBindViewHolder(self, holder, position):
- holder.my_text_view.setText(self.my_dataset[position])
-
- @Override(RecyclerView.Adapter)
- def getItemCount(self):
- return len(self.my_dataset)
-
-
-# Create the RecyclerView
-def create_recycler_view(context):
- my_recycler_view = RecyclerView(context)
- my_layout_manager = LinearLayoutManager(context)
- my_recycler_view.setLayoutManager(my_layout_manager)
- my_dataset = ["Data 1", "Data 2", "Data 3"]
- my_adapter = MyAdapter(my_dataset)
- my_recycler_view.setAdapter(my_adapter)
- return my_recycler_view
diff --git a/experiments/android_pythonnative_3/app/src/main/python/create_widgets.py b/experiments/android_pythonnative_3/app/src/main/python/create_widgets.py
deleted file mode 100644
index 22668b3..0000000
--- a/experiments/android_pythonnative_3/app/src/main/python/create_widgets.py
+++ /dev/null
@@ -1,162 +0,0 @@
-from java import dynamic_proxy, jclass, static_proxy
-import random
-
-# Import View class which contains OnClickListener
-View = jclass('android.view.View')
-Color = jclass('android.graphics.Color')
-
-
-class ButtonClickListener(dynamic_proxy(View.OnClickListener)):
- def __init__(self, button):
- super().__init__()
- self.button = button
-
- def onClick(self, view):
- # Generate a random hex color.
- color = "#" + "".join(
- [random.choice("0123456789ABCDEF") for _ in range(6)])
-
- # Set the button's background color.
- self.button.setBackgroundColor(Color.parseColor(color))
-
- # Print something to the console.
- print("Button clicked! New color is " + color)
-
-
-def create_widgets(context):
- # Java Classes
- RelativeLayout = jclass("android.widget.RelativeLayout")
- FrameLayout = jclass("android.widget.FrameLayout")
- GridLayout = jclass("android.widget.GridLayout")
- LinearLayout = jclass("android.widget.LinearLayout")
- Button = jclass("android.widget.Button")
- TextView = jclass("android.widget.TextView")
- EditText = jclass("android.widget.EditText")
- CheckBox = jclass("android.widget.CheckBox")
- RadioButton = jclass("android.widget.RadioButton")
- ImageView = jclass("android.widget.ImageView")
- ProgressBar = jclass("android.widget.ProgressBar")
- Switch = jclass("android.widget.Switch")
- ToggleButton = jclass("android.widget.ToggleButton")
- SeekBar = jclass("android.widget.SeekBar")
- CardView = jclass("androidx.cardview.widget.CardView")
- ViewPager = jclass("androidx.viewpager.widget.ViewPager")
- DatePicker = jclass("android.widget.DatePicker")
- TimePicker = jclass("android.widget.TimePicker")
- Spinner = jclass("android.widget.Spinner")
- AutoCompleteTextView = jclass("android.widget.AutoCompleteTextView")
- RatingBar = jclass("android.widget.RatingBar")
- AbsoluteLayout = jclass("android.widget.AbsoluteLayout")
- ScrollView = jclass("android.widget.ScrollView")
- HorizontalScrollView = jclass("android.widget.HorizontalScrollView")
- TableLayout = jclass("android.widget.TableLayout")
- TableRow = jclass("android.widget.TableRow")
- ViewFlipper = jclass("android.widget.ViewFlipper")
- ViewSwitcher = jclass("android.widget.ViewSwitcher")
- WebView = jclass("android.webkit.WebView")
- RecyclerView = jclass("androidx.recyclerview.widget.RecyclerView")
- DrawerLayout = jclass("androidx.drawerlayout.widget.DrawerLayout")
- CoordinatorLayout = jclass("androidx.coordinatorlayout.widget.CoordinatorLayout")
- BottomNavigationView = jclass(
- "com.google.android.material.bottomnavigation.BottomNavigationView"
- )
- Chip = jclass("com.google.android.material.chip.Chip")
- FloatingActionButton = jclass(
- "com.google.android.material.floatingactionbutton.FloatingActionButton"
- )
- Snackbar = jclass("com.google.android.material.snackbar.Snackbar")
- NavigationView = jclass("com.google.android.material.navigation.NavigationView")
- ConstraintLayout = jclass("androidx.constraintlayout.widget.ConstraintLayout")
- TextInputLayout = jclass("com.google.android.material.textfield.TextInputLayout")
- MaterialCardView = jclass("com.google.android.material.card.MaterialCardView")
- BottomSheetDialogFragment = jclass(
- "com.google.android.material.bottomsheet.BottomSheetDialogFragment"
- )
-
- # Create LinearLayout
- layout = LinearLayout(context)
- layout_params = LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT
- )
- layout.setLayoutParams(layout_params)
- layout.setOrientation(LinearLayout.VERTICAL)
-
- # Create Button
- button = Button(context)
- button.setText("Button created in Python")
- button.setOnClickListener(ButtonClickListener(button))
- layout.addView(button)
-
- # Create TextView
- text_view = TextView(context)
- text_view.setText("TextView created in Python")
- layout.addView(text_view)
-
- # Create EditText
- edit_text = EditText(context)
- edit_text.setHint("EditText created in Python")
- layout.addView(edit_text)
-
- # Create CheckBox
- check_box = CheckBox(context)
- check_box.setText("CheckBox created in Python")
- layout.addView(check_box)
-
- # Create RadioButton
- radio_button = RadioButton(context)
- radio_button.setText("RadioButton created in Python")
- layout.addView(radio_button)
-
- # Create ImageView (X)
- image_view = ImageView(context)
- layout.addView(image_view)
-
- # Create ProgressBar
- progress_bar = ProgressBar(context)
- layout.addView(progress_bar)
-
- # Create Switch
- switch = Switch(context)
- switch.setText("Switch created in Python")
- layout.addView(switch)
-
- # Create ToggleButton
- toggle_button = ToggleButton(context)
- toggle_button.setTextOn("On")
- toggle_button.setTextOff("Off")
- layout.addView(toggle_button)
-
- # Create SeekBar (X)
- seek_bar = SeekBar(context)
- layout.addView(seek_bar)
-
- # Create CardView (X)
- card_view = CardView(context)
- layout.addView(card_view)
-
- # Create ViewPager (X)
- view_pager = ViewPager(context)
- layout.addView(view_pager)
-
- # Create DatePicker (X)
- date_picker = DatePicker(context)
- layout.addView(date_picker)
-
- # Create TimePicker (X)
- time_picker = TimePicker(context)
- layout.addView(time_picker)
-
- # Create Spinner (X)
- spinner = Spinner(context)
- layout.addView(spinner)
-
- # Create AutoCompleteTextView (X)
- auto_complete_text_view = AutoCompleteTextView(context)
- layout.addView(auto_complete_text_view)
-
- # Create RatingBar (X)
- rating_bar = RatingBar(context)
- layout.addView(rating_bar)
-
- # Return layout
- return layout
diff --git a/experiments/android_pythonnative_3/app/src/main/python/plot.py b/experiments/android_pythonnative_3/app/src/main/python/plot.py
deleted file mode 100644
index fdfdf94..0000000
--- a/experiments/android_pythonnative_3/app/src/main/python/plot.py
+++ /dev/null
@@ -1,14 +0,0 @@
-import io
-import matplotlib.pyplot as plt
-
-
-def plot(x, y):
- xa = [float(word) for word in x.split()]
- ya = [float(word) for word in y.split()]
-
- fig, ax = plt.subplots()
- ax.plot(xa, ya)
-
- f = io.BytesIO()
- plt.savefig(f, format="png")
- return f.getvalue()
diff --git a/experiments/android_pythonnative_3/app/src/main/python/ui_layout.py b/experiments/android_pythonnative_3/app/src/main/python/ui_layout.py
deleted file mode 100644
index 4dd882c..0000000
--- a/experiments/android_pythonnative_3/app/src/main/python/ui_layout.py
+++ /dev/null
@@ -1,24 +0,0 @@
-import json
-
-
-def on_button_click():
- print("Button clicked!")
-
-
-def generate_layout():
- layout = {
- "widgets": [
- {
- "type": "Button",
- "properties": {
- "text": "Click me!",
- "textColor": "#FFFFFF",
- "backgroundColor": "#DB4437",
- },
- "eventHandlers": {
- "onClick": "on_button_click",
- },
- },
- ]
- }
- return json.dumps(layout)
diff --git a/experiments/android_pythonnative_3/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/experiments/android_pythonnative_3/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
deleted file mode 100644
index 2b068d1..0000000
--- a/experiments/android_pythonnative_3/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/experiments/android_pythonnative_3/app/src/main/res/drawable/ic_launcher_background.xml b/experiments/android_pythonnative_3/app/src/main/res/drawable/ic_launcher_background.xml
deleted file mode 100644
index 07d5da9..0000000
--- a/experiments/android_pythonnative_3/app/src/main/res/drawable/ic_launcher_background.xml
+++ /dev/null
@@ -1,170 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/experiments/android_pythonnative_3/app/src/main/res/layout/activity_main.xml b/experiments/android_pythonnative_3/app/src/main/res/layout/activity_main.xml
deleted file mode 100644
index 0e81562..0000000
--- a/experiments/android_pythonnative_3/app/src/main/res/layout/activity_main.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/experiments/android_pythonnative_3/app/src/main/res/layout/activity_second.xml b/experiments/android_pythonnative_3/app/src/main/res/layout/activity_second.xml
deleted file mode 100644
index 9569f77..0000000
--- a/experiments/android_pythonnative_3/app/src/main/res/layout/activity_second.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/experiments/android_pythonnative_3/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/experiments/android_pythonnative_3/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
deleted file mode 100644
index 6f3b755..0000000
--- a/experiments/android_pythonnative_3/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/experiments/android_pythonnative_3/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/experiments/android_pythonnative_3/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
deleted file mode 100644
index 6f3b755..0000000
--- a/experiments/android_pythonnative_3/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/experiments/android_pythonnative_3/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/experiments/android_pythonnative_3/app/src/main/res/mipmap-hdpi/ic_launcher.webp
deleted file mode 100644
index c209e78..0000000
Binary files a/experiments/android_pythonnative_3/app/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ
diff --git a/experiments/android_pythonnative_3/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/experiments/android_pythonnative_3/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
deleted file mode 100644
index b2dfe3d..0000000
Binary files a/experiments/android_pythonnative_3/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/experiments/android_pythonnative_3/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/experiments/android_pythonnative_3/app/src/main/res/mipmap-mdpi/ic_launcher.webp
deleted file mode 100644
index 4f0f1d6..0000000
Binary files a/experiments/android_pythonnative_3/app/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ
diff --git a/experiments/android_pythonnative_3/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/experiments/android_pythonnative_3/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
deleted file mode 100644
index 62b611d..0000000
Binary files a/experiments/android_pythonnative_3/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/experiments/android_pythonnative_3/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/experiments/android_pythonnative_3/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
deleted file mode 100644
index 948a307..0000000
Binary files a/experiments/android_pythonnative_3/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ
diff --git a/experiments/android_pythonnative_3/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/experiments/android_pythonnative_3/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
deleted file mode 100644
index 1b9a695..0000000
Binary files a/experiments/android_pythonnative_3/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/experiments/android_pythonnative_3/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/experiments/android_pythonnative_3/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
deleted file mode 100644
index 28d4b77..0000000
Binary files a/experiments/android_pythonnative_3/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ
diff --git a/experiments/android_pythonnative_3/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/experiments/android_pythonnative_3/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
deleted file mode 100644
index 9287f50..0000000
Binary files a/experiments/android_pythonnative_3/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/experiments/android_pythonnative_3/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/experiments/android_pythonnative_3/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
deleted file mode 100644
index aa7d642..0000000
Binary files a/experiments/android_pythonnative_3/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ
diff --git a/experiments/android_pythonnative_3/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/experiments/android_pythonnative_3/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
deleted file mode 100644
index 9126ae3..0000000
Binary files a/experiments/android_pythonnative_3/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/experiments/android_pythonnative_3/app/src/main/res/values-night/themes.xml b/experiments/android_pythonnative_3/app/src/main/res/values-night/themes.xml
deleted file mode 100644
index c128015..0000000
--- a/experiments/android_pythonnative_3/app/src/main/res/values-night/themes.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/experiments/android_pythonnative_3/app/src/main/res/values/colors.xml b/experiments/android_pythonnative_3/app/src/main/res/values/colors.xml
deleted file mode 100644
index c8524cd..0000000
--- a/experiments/android_pythonnative_3/app/src/main/res/values/colors.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
- #FF000000
- #FFFFFFFF
-
\ No newline at end of file
diff --git a/experiments/android_pythonnative_3/app/src/main/res/values/strings.xml b/experiments/android_pythonnative_3/app/src/main/res/values/strings.xml
deleted file mode 100644
index 115bda4..0000000
--- a/experiments/android_pythonnative_3/app/src/main/res/values/strings.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
- pythonnative
-
\ No newline at end of file
diff --git a/experiments/android_pythonnative_3/app/src/main/res/values/themes.xml b/experiments/android_pythonnative_3/app/src/main/res/values/themes.xml
deleted file mode 100644
index 80a3862..0000000
--- a/experiments/android_pythonnative_3/app/src/main/res/values/themes.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/experiments/android_pythonnative_3/app/src/main/res/xml/backup_rules.xml b/experiments/android_pythonnative_3/app/src/main/res/xml/backup_rules.xml
deleted file mode 100644
index fa0f996..0000000
--- a/experiments/android_pythonnative_3/app/src/main/res/xml/backup_rules.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/experiments/android_pythonnative_3/app/src/main/res/xml/data_extraction_rules.xml b/experiments/android_pythonnative_3/app/src/main/res/xml/data_extraction_rules.xml
deleted file mode 100644
index 9ee9997..0000000
--- a/experiments/android_pythonnative_3/app/src/main/res/xml/data_extraction_rules.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/experiments/android_pythonnative_3/app/src/test/java/com/pythonnative/pythonnative/ExampleUnitTest.kt b/experiments/android_pythonnative_3/app/src/test/java/com/pythonnative/pythonnative/ExampleUnitTest.kt
deleted file mode 100644
index 1bb2d2b..0000000
--- a/experiments/android_pythonnative_3/app/src/test/java/com/pythonnative/pythonnative/ExampleUnitTest.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.pythonnative.pythonnative
-
-import org.junit.Test
-
-import org.junit.Assert.*
-
-/**
- * Example local unit test, which will execute on the development machine (host).
- *
- * See [testing documentation](http://d.android.com/tools/testing).
- */
-class ExampleUnitTest {
- @Test
- fun addition_isCorrect() {
- assertEquals(4, 2 + 2)
- }
-}
\ No newline at end of file
diff --git a/experiments/android_pythonnative_3/build.gradle b/experiments/android_pythonnative_3/build.gradle
deleted file mode 100644
index ff78675..0000000
--- a/experiments/android_pythonnative_3/build.gradle
+++ /dev/null
@@ -1,7 +0,0 @@
-// Top-level build file where you can add configuration options common to all sub-projects/modules.
-plugins {
- id 'com.android.application' version '8.0.2' apply false
- id 'com.android.library' version '8.0.2' apply false
- id 'org.jetbrains.kotlin.android' version '1.8.20' apply false
- id 'com.chaquo.python' version '14.0.2' apply false
-}
\ No newline at end of file
diff --git a/experiments/android_pythonnative_3/gradle.properties b/experiments/android_pythonnative_3/gradle.properties
deleted file mode 100644
index 3c5031e..0000000
--- a/experiments/android_pythonnative_3/gradle.properties
+++ /dev/null
@@ -1,23 +0,0 @@
-# Project-wide Gradle settings.
-# IDE (e.g. Android Studio) users:
-# Gradle settings configured through the IDE *will override*
-# any settings specified in this file.
-# For more details on how to configure your build environment visit
-# http://www.gradle.org/docs/current/userguide/build_environment.html
-# Specifies the JVM arguments used for the daemon process.
-# The setting is particularly useful for tweaking memory settings.
-org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
-# When configured, Gradle will run in incubating parallel mode.
-# This option should only be used with decoupled projects. More details, visit
-# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
-# org.gradle.parallel=true
-# AndroidX package structure to make it clearer which packages are bundled with the
-# Android operating system, and which are packaged with your app's APK
-# https://developer.android.com/topic/libraries/support-library/androidx-rn
-android.useAndroidX=true
-# Kotlin code style for this project: "official" or "obsolete":
-kotlin.code.style=official
-# Enables namespacing of each library's R class so that its R class includes only the
-# resources declared in the library itself and none from the library's dependencies,
-# thereby reducing the size of the R class for that library
-android.nonTransitiveRClass=true
\ No newline at end of file
diff --git a/experiments/android_pythonnative_3/gradle/wrapper/gradle-wrapper.jar b/experiments/android_pythonnative_3/gradle/wrapper/gradle-wrapper.jar
deleted file mode 100644
index e708b1c..0000000
Binary files a/experiments/android_pythonnative_3/gradle/wrapper/gradle-wrapper.jar and /dev/null differ
diff --git a/experiments/android_pythonnative_3/gradle/wrapper/gradle-wrapper.properties b/experiments/android_pythonnative_3/gradle/wrapper/gradle-wrapper.properties
deleted file mode 100644
index 0b0eb13..0000000
--- a/experiments/android_pythonnative_3/gradle/wrapper/gradle-wrapper.properties
+++ /dev/null
@@ -1,6 +0,0 @@
-#Tue May 30 12:37:43 PDT 2023
-distributionBase=GRADLE_USER_HOME
-distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
-zipStoreBase=GRADLE_USER_HOME
-zipStorePath=wrapper/dists
diff --git a/experiments/android_pythonnative_3/gradlew b/experiments/android_pythonnative_3/gradlew
deleted file mode 100755
index 4f906e0..0000000
--- a/experiments/android_pythonnative_3/gradlew
+++ /dev/null
@@ -1,185 +0,0 @@
-#!/usr/bin/env sh
-
-#
-# Copyright 2015 the original author or authors.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
-##############################################################################
-##
-## Gradle start up script for UN*X
-##
-##############################################################################
-
-# Attempt to set APP_HOME
-# Resolve links: $0 may be a link
-PRG="$0"
-# Need this for relative symlinks.
-while [ -h "$PRG" ] ; do
- ls=`ls -ld "$PRG"`
- link=`expr "$ls" : '.*-> \(.*\)$'`
- if expr "$link" : '/.*' > /dev/null; then
- PRG="$link"
- else
- PRG=`dirname "$PRG"`"/$link"
- fi
-done
-SAVED="`pwd`"
-cd "`dirname \"$PRG\"`/" >/dev/null
-APP_HOME="`pwd -P`"
-cd "$SAVED" >/dev/null
-
-APP_NAME="Gradle"
-APP_BASE_NAME=`basename "$0"`
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
-
-# Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD="maximum"
-
-warn () {
- echo "$*"
-}
-
-die () {
- echo
- echo "$*"
- echo
- exit 1
-}
-
-# OS specific support (must be 'true' or 'false').
-cygwin=false
-msys=false
-darwin=false
-nonstop=false
-case "`uname`" in
- CYGWIN* )
- cygwin=true
- ;;
- Darwin* )
- darwin=true
- ;;
- MINGW* )
- msys=true
- ;;
- NONSTOP* )
- nonstop=true
- ;;
-esac
-
-CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
-
-
-# Determine the Java command to use to start the JVM.
-if [ -n "$JAVA_HOME" ] ; then
- if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
- # IBM's JDK on AIX uses strange locations for the executables
- JAVACMD="$JAVA_HOME/jre/sh/java"
- else
- JAVACMD="$JAVA_HOME/bin/java"
- fi
- if [ ! -x "$JAVACMD" ] ; then
- die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
-
-Please set the JAVA_HOME variable in your environment to match the
-location of your Java installation."
- fi
-else
- JAVACMD="java"
- which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-
-Please set the JAVA_HOME variable in your environment to match the
-location of your Java installation."
-fi
-
-# Increase the maximum file descriptors if we can.
-if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
- MAX_FD_LIMIT=`ulimit -H -n`
- if [ $? -eq 0 ] ; then
- if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
- MAX_FD="$MAX_FD_LIMIT"
- fi
- ulimit -n $MAX_FD
- if [ $? -ne 0 ] ; then
- warn "Could not set maximum file descriptor limit: $MAX_FD"
- fi
- else
- warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
- fi
-fi
-
-# For Darwin, add options to specify how the application appears in the dock
-if $darwin; then
- GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
-fi
-
-# For Cygwin or MSYS, switch paths to Windows format before running java
-if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
- APP_HOME=`cygpath --path --mixed "$APP_HOME"`
- CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
-
- JAVACMD=`cygpath --unix "$JAVACMD"`
-
- # We build the pattern for arguments to be converted via cygpath
- ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
- SEP=""
- for dir in $ROOTDIRSRAW ; do
- ROOTDIRS="$ROOTDIRS$SEP$dir"
- SEP="|"
- done
- OURCYGPATTERN="(^($ROOTDIRS))"
- # Add a user-defined pattern to the cygpath arguments
- if [ "$GRADLE_CYGPATTERN" != "" ] ; then
- OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
- fi
- # Now convert the arguments - kludge to limit ourselves to /bin/sh
- i=0
- for arg in "$@" ; do
- CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
- CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
-
- if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
- eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
- else
- eval `echo args$i`="\"$arg\""
- fi
- i=`expr $i + 1`
- done
- case $i in
- 0) set -- ;;
- 1) set -- "$args0" ;;
- 2) set -- "$args0" "$args1" ;;
- 3) set -- "$args0" "$args1" "$args2" ;;
- 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
- 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
- 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
- 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
- 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
- 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
- esac
-fi
-
-# Escape application args
-save () {
- for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
- echo " "
-}
-APP_ARGS=`save "$@"`
-
-# Collect all arguments for the java command, following the shell quoting and substitution rules
-eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
-
-exec "$JAVACMD" "$@"
diff --git a/experiments/android_pythonnative_3/gradlew.bat b/experiments/android_pythonnative_3/gradlew.bat
deleted file mode 100644
index 107acd3..0000000
--- a/experiments/android_pythonnative_3/gradlew.bat
+++ /dev/null
@@ -1,89 +0,0 @@
-@rem
-@rem Copyright 2015 the original author or authors.
-@rem
-@rem Licensed under the Apache License, Version 2.0 (the "License");
-@rem you may not use this file except in compliance with the License.
-@rem You may obtain a copy of the License at
-@rem
-@rem https://www.apache.org/licenses/LICENSE-2.0
-@rem
-@rem Unless required by applicable law or agreed to in writing, software
-@rem distributed under the License is distributed on an "AS IS" BASIS,
-@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-@rem See the License for the specific language governing permissions and
-@rem limitations under the License.
-@rem
-
-@if "%DEBUG%" == "" @echo off
-@rem ##########################################################################
-@rem
-@rem Gradle startup script for Windows
-@rem
-@rem ##########################################################################
-
-@rem Set local scope for the variables with windows NT shell
-if "%OS%"=="Windows_NT" setlocal
-
-set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
-set APP_BASE_NAME=%~n0
-set APP_HOME=%DIRNAME%
-
-@rem Resolve any "." and ".." in APP_HOME to make it shorter.
-for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
-
-@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
-
-@rem Find java.exe
-if defined JAVA_HOME goto findJavaFromJavaHome
-
-set JAVA_EXE=java.exe
-%JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto execute
-
-echo.
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:findJavaFromJavaHome
-set JAVA_HOME=%JAVA_HOME:"=%
-set JAVA_EXE=%JAVA_HOME%/bin/java.exe
-
-if exist "%JAVA_EXE%" goto execute
-
-echo.
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:execute
-@rem Setup the command line
-
-set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
-
-
-@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
-
-:end
-@rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd
-
-:fail
-rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
-rem the _cmd.exe /c_ return code!
-if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
-exit /b 1
-
-:mainEnd
-if "%OS%"=="Windows_NT" endlocal
-
-:omega
diff --git a/experiments/android_pythonnative_3/settings.gradle b/experiments/android_pythonnative_3/settings.gradle
deleted file mode 100644
index 8d1d42f..0000000
--- a/experiments/android_pythonnative_3/settings.gradle
+++ /dev/null
@@ -1,16 +0,0 @@
-pluginManagement {
- repositories {
- google()
- mavenCentral()
- gradlePluginPortal()
- }
-}
-dependencyResolutionManagement {
- repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
- repositories {
- google()
- mavenCentral()
- }
-}
-rootProject.name = "pythonnative"
-include ':app'
diff --git a/mkdocs.yml b/mkdocs.yml
index c8a6339..9a29ca4 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -15,6 +15,7 @@ nav:
- Home: index.md
- Getting Started: getting-started.md
- Concepts:
+ - Architecture: concepts/architecture.md
- Components: concepts/components.md
- Examples:
- Overview: examples.md
@@ -22,6 +23,7 @@ nav:
- Guides:
- Android: guides/android.md
- iOS: guides/ios.md
+ - Navigation: guides/navigation.md
- API Reference:
- Package: api/pythonnative.md
- Meta:
diff --git a/pyproject.toml b/pyproject.toml
index 4ca1465..0cdbd85 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pythonnative"
-version = "0.2.0"
+version = "0.3.0"
description = "Cross-platform native UI toolkit for Android and iOS"
authors = [
{ name = "Owen Carey" }
@@ -22,7 +22,6 @@ classifiers = [
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
- "Environment :: Handhelds/PDA",
"Topic :: Software Development :: User Interfaces",
]
dependencies = [
diff --git a/src/pythonnative/__init__.py b/src/pythonnative/__init__.py
index 78d2ebc..1e2530d 100644
--- a/src/pythonnative/__init__.py
+++ b/src/pythonnative/__init__.py
@@ -1,7 +1,7 @@
from importlib import import_module
from typing import Any, Dict
-__version__ = "0.2.0"
+__version__ = "0.3.0"
__all__ = [
"ActivityIndicatorView",
diff --git a/src/pythonnative/cli/pn.py b/src/pythonnative/cli/pn.py
index 131ff38..e053f8d 100644
--- a/src/pythonnative/cli/pn.py
+++ b/src/pythonnative/cli/pn.py
@@ -41,7 +41,7 @@ def init_project(args: argparse.Namespace) -> None:
os.makedirs(app_dir, exist_ok=True)
- # Minimal hello world app scaffold
+ # Minimal hello world app scaffold (no bootstrap function; host instantiates Page directly)
main_page_py = os.path.join(app_dir, "main_page.py")
if not os.path.exists(main_page_py) or args.force:
with open(main_page_py, "w", encoding="utf-8") as f:
@@ -61,13 +61,6 @@ def on_create(self):
button.set_on_click(lambda: print("Button clicked"))
stack.add_view(button)
self.set_root_view(stack)
-
-
-def bootstrap(native_instance):
- '''Entry point called by the host app (Android Activity or iOS ViewController).'''
- page = MainPage(native_instance)
- page.on_create()
- return page
"""
)
diff --git a/src/pythonnative/page.py b/src/pythonnative/page.py
index 70301f6..e5734e6 100644
--- a/src/pythonnative/page.py
+++ b/src/pythonnative/page.py
@@ -29,7 +29,9 @@
differences and handles them appropriately.
"""
+import json
from abc import ABC, abstractmethod
+from typing import Any, Optional, Union
from .utils import IS_ANDROID, set_android_context
from .view import ViewBase
@@ -85,7 +87,25 @@ def on_restore_instance_state(self) -> None:
pass
@abstractmethod
+ def set_args(self, args: Optional[dict]) -> None:
+ pass
+
+ @abstractmethod
+ def push(self, page: Union[str, Any], args: Optional[dict] = None) -> None:
+ pass
+
+ @abstractmethod
+ def pop(self) -> None:
+ pass
+
+ def get_args(self) -> dict:
+ """Return arguments provided to this Page (empty dict if none)."""
+ # Concrete classes should set self._args; default empty
+ return getattr(self, "_args", {})
+
+ # Back-compat: navigate_to delegates to push
def navigate_to(self, page) -> None:
+ self.push(page)
pass
@@ -105,9 +125,23 @@ def __init__(self, native_instance) -> None:
# self.native_instance = self.native_class()
# Stash the Activity so child views can implicitly acquire a Context
set_android_context(native_instance)
+ self._args: dict = {}
def set_root_view(self, view) -> None:
- self.native_instance.setContentView(view.native_instance)
+ # In fragment-based navigation, attach child view to the current fragment container.
+ try:
+ from .utils import get_android_fragment_container
+
+ container = get_android_fragment_container()
+ # Remove previous children if any, then add the new root
+ try:
+ container.removeAllViews()
+ except Exception:
+ pass
+ container.addView(view.native_instance)
+ except Exception:
+ # Fallback to setting content view directly on the Activity
+ self.native_instance.setContentView(view.native_instance)
def on_create(self) -> None:
print("Android on_create() called")
@@ -136,13 +170,53 @@ def on_save_instance_state(self) -> None:
def on_restore_instance_state(self) -> None:
print("Android on_restore_instance_state() called")
- def navigate_to(self, page) -> None:
- # intent = jclass("android.content.Intent")(self.native_instance, page.native_class)
- intent = jclass("android.content.Intent")(
- self.native_instance,
- jclass("com.pythonnative.pythonnative.SecondActivity"),
- )
- self.native_instance.startActivity(intent)
+ def set_args(self, args: Optional[dict]) -> None:
+ # Accept dict or JSON string for convenience when crossing language boundaries
+ if isinstance(args, str):
+ try:
+ self._args = json.loads(args) or {}
+ return
+ except Exception:
+ self._args = {}
+ return
+ self._args = args or {}
+
+ def _resolve_page_path(self, page: Union[str, Any]) -> str:
+ if isinstance(page, str):
+ return page
+ # If a class or instance is passed, derive dotted path
+ try:
+ module = getattr(page, "__module__", None)
+ name = getattr(page, "__name__", None)
+ if module and name:
+ return f"{module}.{name}"
+ # Instance: use its class
+ cls = page.__class__
+ return f"{cls.__module__}.{cls.__name__}"
+ except Exception:
+ raise ValueError("Unsupported page reference; expected dotted string or class/instance")
+
+ def push(self, page: Union[str, Any], args: Optional[dict] = None) -> None:
+ # Delegate to Navigator.push to navigate to PageFragment with arguments
+ page_path = self._resolve_page_path(page)
+ try:
+ Navigator = jclass(f"{self.native_instance.getPackageName()}.Navigator")
+ args_json = json.dumps(args) if args else None
+ Navigator.push(self.native_instance, page_path, args_json)
+ except Exception:
+ # As a last resort, do nothing rather than crash
+ pass
+
+ def pop(self) -> None:
+ # Delegate to Navigator.pop for back-stack pop
+ try:
+ Navigator = jclass(f"{self.native_instance.getPackageName()}.Navigator")
+ Navigator.pop(self.native_instance)
+ except Exception:
+ try:
+ self.native_instance.finish()
+ except Exception:
+ pass
else:
# ========================================
@@ -150,8 +224,45 @@ def navigate_to(self, page) -> None:
# https://developer.apple.com/documentation/uikit/uiviewcontroller
# ========================================
+ from typing import Dict
+
from rubicon.objc import ObjCClass, ObjCInstance
+ # Global registry mapping native UIViewController pointer address to Page instances.
+ _IOS_PAGE_REGISTRY: Dict[int, Any] = {}
+
+ def _ios_register_page(vc_instance: Any, page_obj: Any) -> None:
+ try:
+ ptr = int(vc_instance.ptr) # rubicon ObjCInstance -> c_void_p convertible to int
+ _IOS_PAGE_REGISTRY[ptr] = page_obj
+ except Exception:
+ pass
+
+ def _ios_unregister_page(vc_instance: Any) -> None:
+ try:
+ ptr = int(vc_instance.ptr)
+ _IOS_PAGE_REGISTRY.pop(ptr, None)
+ except Exception:
+ pass
+
+ def forward_lifecycle(native_addr: int, event: str) -> None:
+ """Forward a lifecycle event from Swift ViewController to the registered Page.
+
+ :param native_addr: Integer pointer address of the UIViewController
+ :param event: One of 'on_start', 'on_resume', 'on_pause', 'on_stop', 'on_destroy',
+ 'on_save_instance_state', 'on_restore_instance_state'.
+ """
+ page = _IOS_PAGE_REGISTRY.get(int(native_addr))
+ if not page:
+ return
+ try:
+ handler = getattr(page, event, None)
+ if handler:
+ handler()
+ except Exception:
+ # Avoid surfacing exceptions across the Swift/Python boundary in lifecycle
+ pass
+
class Page(PageBase, ViewBase):
def __init__(self, native_instance) -> None:
super().__init__()
@@ -164,6 +275,10 @@ def __init__(self, native_instance) -> None:
native_instance = None
self.native_instance = native_instance
# self.native_instance = self.native_class.alloc().init()
+ self._args: dict = {}
+ # Register for lifecycle forwarding
+ if self.native_instance is not None:
+ _ios_register_page(self.native_instance, self)
def set_root_view(self, view) -> None:
# UIViewController.view is a property; access without calling.
@@ -195,6 +310,8 @@ def on_stop(self) -> None:
def on_destroy(self) -> None:
print("iOS on_destroy() called")
+ if self.native_instance is not None:
+ _ios_unregister_page(self.native_instance)
def on_restart(self) -> None:
print("iOS on_restart() called")
@@ -205,5 +322,75 @@ def on_save_instance_state(self) -> None:
def on_restore_instance_state(self) -> None:
print("iOS on_restore_instance_state() called")
- def navigate_to(self, page) -> None:
- self.native_instance.navigationController().pushViewControllerAnimated_(page.native_instance, True)
+ def set_args(self, args: Optional[dict]) -> None:
+ if isinstance(args, str):
+ try:
+ self._args = json.loads(args) or {}
+ return
+ except Exception:
+ self._args = {}
+ return
+ self._args = args or {}
+
+ def _resolve_page_path(self, page: Union[str, Any]) -> str:
+ if isinstance(page, str):
+ return page
+ try:
+ module = getattr(page, "__module__", None)
+ name = getattr(page, "__name__", None)
+ if module and name:
+ return f"{module}.{name}"
+ cls = page.__class__
+ return f"{cls.__module__}.{cls.__name__}"
+ except Exception:
+ raise ValueError("Unsupported page reference; expected dotted string or class/instance")
+
+ def push(self, page: Union[str, Any], args: Optional[dict] = None) -> None:
+ page_path = self._resolve_page_path(page)
+ # Resolve the Swift ViewController class. Swift classes are namespaced by
+ # the module name (CFBundleName). Try plain name first, then Module.Name.
+ ViewController = None
+ try:
+ ViewController = ObjCClass("ViewController")
+ except Exception:
+ try:
+ NSBundle = ObjCClass("NSBundle")
+ bundle = NSBundle.mainBundle
+ module_name = None
+ try:
+ # Prefer CFBundleName; fallback to CFBundleExecutable
+ module_name = bundle.objectForInfoDictionaryKey_("CFBundleName")
+ if module_name is None:
+ module_name = bundle.objectForInfoDictionaryKey_("CFBundleExecutable")
+ except Exception:
+ module_name = None
+ if module_name:
+ ViewController = ObjCClass(f"{module_name}.ViewController")
+ except Exception:
+ ViewController = None
+
+ if ViewController is None:
+ raise NameError("ViewController class not found; ensure Swift class is ObjC-visible")
+
+ next_vc = ViewController.alloc().init()
+ try:
+ # Use KVC to pass metadata to Swift
+ next_vc.setValue_forKey_(page_path, "requestedPagePath")
+ if args:
+ next_vc.setValue_forKey_(json.dumps(args), "requestedPageArgsJSON")
+ except Exception:
+ pass
+ # On iOS, `navigationController` is exposed as a property; treat it as such.
+ nav = getattr(self.native_instance, "navigationController", None)
+ if nav is None:
+ # If no navigation controller, this push will be a no-op; rely on template to embed one.
+ raise RuntimeError(
+ "No UINavigationController available; ensure template embeds root in navigation controller"
+ )
+ # Method name maps from pushViewController:animated:
+ nav.pushViewController_animated_(next_vc, True)
+
+ def pop(self) -> None:
+ nav = getattr(self.native_instance, "navigationController", None)
+ if nav is not None:
+ nav.popViewControllerAnimated_(True)
diff --git a/src/pythonnative/templates/android_template/app/build.gradle b/src/pythonnative/templates/android_template/app/build.gradle
index 4098326..2fb8251 100644
--- a/src/pythonnative/templates/android_template/app/build.gradle
+++ b/src/pythonnative/templates/android_template/app/build.gradle
@@ -53,6 +53,9 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.5.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
+ // AndroidX Navigation for Fragment-based navigation
+ implementation 'androidx.navigation:navigation-fragment-ktx:2.7.7'
+ implementation 'androidx.navigation:navigation-ui-ktx:2.7.7'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
diff --git a/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt b/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt
index c0bc76e..35fc3ff 100644
--- a/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt
+++ b/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt
@@ -8,25 +8,28 @@ import com.chaquo.python.Python
import com.chaquo.python.android.AndroidPlatform
class MainActivity : AppCompatActivity() {
+ private val TAG = javaClass.simpleName
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- // setContentView(R.layout.activity_main)
+ Log.d(TAG, "onCreate() called")
// Initialize Chaquopy
if (!Python.isStarted()) {
Python.start(AndroidPlatform(this))
}
try {
+ // Set content view to the NavHost layout; the initial page loads via nav_graph startDestination
+ setContentView(R.layout.activity_main)
+ // Optionally, bootstrap Python so first fragment can create the initial page onCreate
val py = Python.getInstance()
- val pyModule = py.getModule("app.main_page")
- pyModule.callAttr("bootstrap", this)
- // Python Page will set the content view via set_root_view
+ // Touch module to ensure bundled Python code is available; actual instantiation happens in PageFragment
+ py.getModule("app.main_page")
} catch (e: Exception) {
- Log.e("PythonNative", "Python bootstrap failed", e)
- // Fallback: show a simple native label if Python bootstrap fails
+ Log.e("PythonNative", "Bootstrap failed", e)
val tv = TextView(this)
tv.text = "Hello from PythonNative (Android template)"
setContentView(tv)
}
}
-}
\ No newline at end of file
+}
diff --git a/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/Navigator.kt b/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/Navigator.kt
new file mode 100644
index 0000000..2d3d394
--- /dev/null
+++ b/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/Navigator.kt
@@ -0,0 +1,26 @@
+package com.pythonnative.android_template
+
+import android.os.Bundle
+import androidx.core.os.bundleOf
+import androidx.fragment.app.FragmentActivity
+import androidx.navigation.fragment.NavHostFragment
+
+object Navigator {
+ @JvmStatic
+ fun push(activity: FragmentActivity, pagePath: String, argsJson: String?) {
+ val navHost = activity.supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
+ val navController = navHost.navController
+ val args = Bundle()
+ args.putString("page_path", pagePath)
+ if (argsJson != null) {
+ args.putString("args_json", argsJson)
+ }
+ navController.navigate(R.id.pageFragment, args)
+ }
+
+ @JvmStatic
+ fun pop(activity: FragmentActivity) {
+ val navHost = activity.supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
+ navHost.navController.popBackStack()
+ }
+}
diff --git a/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PageFragment.kt b/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PageFragment.kt
new file mode 100644
index 0000000..b5d1dab
--- /dev/null
+++ b/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PageFragment.kt
@@ -0,0 +1,111 @@
+package com.pythonnative.android_template
+
+import android.os.Bundle
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import androidx.core.os.bundleOf
+import androidx.fragment.app.Fragment
+import com.chaquo.python.PyObject
+import com.chaquo.python.Python
+import com.chaquo.python.android.AndroidPlatform
+
+class PageFragment : Fragment() {
+ private val TAG = javaClass.simpleName
+ private var page: PyObject? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (!Python.isStarted()) {
+ context?.let { Python.start(AndroidPlatform(it)) }
+ }
+ try {
+ val py = Python.getInstance()
+ val pagePath = arguments?.getString("page_path") ?: "app.main_page.MainPage"
+ val argsJson = arguments?.getString("args_json")
+ val moduleName = pagePath.substringBeforeLast('.')
+ val className = pagePath.substringAfterLast('.')
+ val pyModule = py.getModule(moduleName)
+ val pageClass = pyModule.get(className)
+ // Pass the hosting Activity as native_instance for context
+ page = pageClass?.call(requireActivity())
+ if (!argsJson.isNullOrEmpty()) {
+ page?.callAttr("set_args", argsJson)
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to instantiate page", e)
+ }
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ // Create a simple container which Python-native views can be attached to.
+ val frame = FrameLayout(requireContext())
+ frame.layoutParams = ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT
+ )
+ return frame
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ // Python side will call set_root_view to attach a native view to Activity.
+ // In fragment-based architecture, the Activity will set contentView once,
+ // so we ensure the fragment's container is available for Python to target.
+ // Expose the fragment container to Python so Page.set_root_view can attach into it
+ try {
+ val py = Python.getInstance()
+ val utils = py.getModule("pythonnative.utils")
+ utils.callAttr("set_android_fragment_container", view)
+ // Now that container exists, invoke on_create so Python can attach its root view
+ page?.callAttr("on_create")
+ } catch (_: Exception) {
+ }
+ }
+
+ override fun onStart() {
+ super.onStart()
+ try { page?.callAttr("on_start") } catch (e: Exception) { Log.w(TAG, "on_start failed", e) }
+ }
+
+ override fun onResume() {
+ super.onResume()
+ try { page?.callAttr("on_resume") } catch (e: Exception) { Log.w(TAG, "on_resume failed", e) }
+ }
+
+ override fun onPause() {
+ super.onPause()
+ try { page?.callAttr("on_pause") } catch (e: Exception) { Log.w(TAG, "on_pause failed", e) }
+ }
+
+ override fun onStop() {
+ super.onStop()
+ try { page?.callAttr("on_stop") } catch (e: Exception) { Log.w(TAG, "on_stop failed", e) }
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ try { page?.callAttr("on_destroy") } catch (e: Exception) { Log.w(TAG, "on_destroy failed", e) }
+ }
+
+ companion object {
+ fun newInstance(pagePath: String, argsJson: String?): PageFragment {
+ val f = PageFragment()
+ f.arguments = bundleOf(
+ "page_path" to pagePath,
+ "args_json" to argsJson
+ )
+ return f
+ }
+ }
+}
diff --git a/src/pythonnative/templates/android_template/app/src/main/res/layout/activity_main.xml b/src/pythonnative/templates/android_template/app/src/main/res/layout/activity_main.xml
index 17eab17..8f9a393 100644
--- a/src/pythonnative/templates/android_template/app/src/main/res/layout/activity_main.xml
+++ b/src/pythonnative/templates/android_template/app/src/main/res/layout/activity_main.xml
@@ -1,18 +1,10 @@
-
-
-
-
-
\ No newline at end of file
+ app:defaultNavHost="true"
+ app:navGraph="@navigation/nav_graph" />
\ No newline at end of file
diff --git a/src/pythonnative/templates/android_template/app/src/main/res/navigation/nav_graph.xml b/src/pythonnative/templates/android_template/app/src/main/res/navigation/nav_graph.xml
new file mode 100644
index 0000000..cbf90d7
--- /dev/null
+++ b/src/pythonnative/templates/android_template/app/src/main/res/navigation/nav_graph.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/pythonnative/templates/ios_template/ios_template/SceneDelegate.swift b/src/pythonnative/templates/ios_template/ios_template/SceneDelegate.swift
index 1302535..11a1f30 100644
--- a/src/pythonnative/templates/ios_template/ios_template/SceneDelegate.swift
+++ b/src/pythonnative/templates/ios_template/ios_template/SceneDelegate.swift
@@ -13,10 +13,13 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
- // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
- // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
- // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
- guard let _ = (scene as? UIWindowScene) else { return }
+ guard let windowScene = (scene as? UIWindowScene) else { return }
+ let window = UIWindow(windowScene: windowScene)
+ let root = ViewController()
+ let nav = UINavigationController(rootViewController: root)
+ window.rootViewController = nav
+ self.window = window
+ window.makeKeyAndVisible()
}
func sceneDidDisconnect(_ scene: UIScene) {
diff --git a/src/pythonnative/templates/ios_template/ios_template/ViewController.swift b/src/pythonnative/templates/ios_template/ios_template/ViewController.swift
index 0f3ba5e..88e7afc 100644
--- a/src/pythonnative/templates/ios_template/ios_template/ViewController.swift
+++ b/src/pythonnative/templates/ios_template/ios_template/ViewController.swift
@@ -16,9 +16,17 @@ import Python
#endif
class ViewController: UIViewController {
+ // Ensure Python.framework is configured only once per process
+ private static var hasInitializedPython: Bool = false
+ // Optional keys for dynamic page navigation
+ @objc dynamic var requestedPagePath: String? = nil
+ @objc dynamic var requestedPageArgsJSON: String? = nil
+ private var pythonReady: Bool = false
override func viewDidLoad() {
super.viewDidLoad()
+ // Ensure a visible background when created programmatically (storyboards set this automatically)
+ view.backgroundColor = .systemBackground
NSLog("[PN][ViewController] viewDidLoad")
if let bundleId = Bundle.main.bundleIdentifier {
NSLog("[PN] Bundle Identifier: \(bundleId)")
@@ -45,14 +53,19 @@ class ViewController: UIViewController {
let frameworkLib = "\(bundlePath)/Frameworks/Python.framework/Python"
setenv("PYTHON_LIBRARY", frameworkLib, 1)
if FileManager.default.fileExists(atPath: frameworkLib) {
- NSLog("[PN] Using embedded Python lib at: \(frameworkLib)")
- PythonLibrary.useLibrary(at: frameworkLib)
+ if !ViewController.hasInitializedPython {
+ NSLog("[PN] Using embedded Python lib at: \(frameworkLib)")
+ PythonLibrary.useLibrary(at: frameworkLib)
+ ViewController.hasInitializedPython = true
+ } else {
+ NSLog("[PN] Python library already initialized; skipping useLibrary")
+ }
+ pythonReady = true
} else {
NSLog("[PN] Embedded Python library not found at: \(frameworkLib)")
}
}
- NSLog("[PN] PythonKit available; attempting Python bootstrap of app.main_page.bootstrap(self)")
- // Attempt Python bootstrap of app.main_page.bootstrap(self)
+ NSLog("[PN] PythonKit available; attempting Python bootstrap")
let sys = Python.import("sys")
NSLog("[PN] Python version: \(sys.version)")
NSLog("[PN] Initial sys.path: \(sys.path)")
@@ -69,38 +82,35 @@ class ViewController: UIViewController {
NSLog("[PN] Could not list contents of \(appDir).")
}
}
+ // Determine which Python page to load
+ let pagePath: String = requestedPagePath ?? "app.main_page.MainPage"
do {
- let app = try Python.attemptImport("app.main_page")
- let pyNone = Python.None
+ let moduleName = String(pagePath.split(separator: ".").dropLast().joined(separator: "."))
+ let className = String(pagePath.split(separator: ".").last ?? "MainPage")
+ let pyModule = try Python.attemptImport(moduleName)
+ // Resolve class by name via builtins.getattr to avoid subscripting issues
let builtins = Python.import("builtins")
let getattrFn = builtins.getattr
- let bootstrap = try getattrFn.throwing.dynamicallyCall(withArguments: [app, "bootstrap", pyNone])
- if bootstrap != Python.None {
+ let pageClass = try getattrFn.throwing.dynamicallyCall(withArguments: [pyModule, className])
+ // Pass native pointer so Python Page can wrap via rubicon.objc
+ let ptr = Unmanaged.passUnretained(self).toOpaque()
+ let addr = UInt(bitPattern: ptr)
+ let page = try pageClass.throwing.dynamicallyCall(withArguments: [addr])
+ // If args provided, pass into Page via set_args(dict)
+ if let jsonStr = requestedPageArgsJSON {
+ let json = Python.import("json")
do {
- let isCallable = try Python.callable.throwing.dynamicallyCall(withArguments: [bootstrap])
- if Bool(isCallable) == true {
- // Pass the native UIViewController pointer into Python so it can be wrapped by rubicon.objc
- let ptr = Unmanaged.passUnretained(self).toOpaque()
- let addr = UInt(bitPattern: ptr)
- NSLog("[PN] Passing native UIViewController pointer to Python: 0x%llx", addr)
- _ = try bootstrap.throwing.dynamicallyCall(withArguments: [addr])
- NSLog("[PN] Python bootstrap succeeded; returning early from viewDidLoad")
- return
- } else {
- NSLog("[PN] 'bootstrap' exists but is not callable")
- }
+ let args = try json.loads.throwing.dynamicallyCall(withArguments: [jsonStr])
+ _ = try page.set_args.throwing.dynamicallyCall(withArguments: [args])
} catch {
- NSLog("[PN] Python callable/bootstrap raised error: \(error)")
- let sys = Python.import("sys")
- NSLog("[PN] sys.path at call error: \(sys.path)")
+ NSLog("[PN] Failed to decode requestedPageArgsJSON: \(error)")
}
- } else {
- NSLog("[PN] Python bootstrap function not found on app.main_page")
}
+ // Call on_create immediately so Python can insert its root view
+ _ = try page.on_create.throwing.dynamicallyCall(withArguments: [])
+ return
} catch {
- NSLog("[PN] Python bootstrap failed during import/getattr: \(error)")
- let sys = Python.import("sys")
- NSLog("[PN] sys.path at failure: \(sys.path)")
+ NSLog("[PN] Python bootstrap failed: \(error)")
}
#endif
@@ -113,6 +123,96 @@ class ViewController: UIViewController {
view.addSubview(label)
}
+ override func viewWillAppear(_ animated: Bool) {
+ super.viewWillAppear(animated)
+ #if canImport(PythonKit)
+ if pythonReady {
+ let ptr = UInt(bitPattern: Unmanaged.passUnretained(self).toOpaque())
+ do {
+ let pn = try Python.attemptImport("pythonnative.page")
+ _ = try pn.forward_lifecycle.throwing.dynamicallyCall(withArguments: [ptr, "on_start"])
+ } catch {}
+ }
+ #endif
+ }
+
+ override func viewDidAppear(_ animated: Bool) {
+ super.viewDidAppear(animated)
+ #if canImport(PythonKit)
+ if pythonReady {
+ let ptr = UInt(bitPattern: Unmanaged.passUnretained(self).toOpaque())
+ do {
+ let pn = try Python.attemptImport("pythonnative.page")
+ _ = try pn.forward_lifecycle.throwing.dynamicallyCall(withArguments: [ptr, "on_resume"])
+ } catch {}
+ }
+ #endif
+ }
+
+ override func viewWillDisappear(_ animated: Bool) {
+ super.viewWillDisappear(animated)
+ #if canImport(PythonKit)
+ if pythonReady {
+ let ptr = UInt(bitPattern: Unmanaged.passUnretained(self).toOpaque())
+ do {
+ let pn = try Python.attemptImport("pythonnative.page")
+ _ = try pn.forward_lifecycle.throwing.dynamicallyCall(withArguments: [ptr, "on_pause"])
+ } catch {}
+ }
+ #endif
+ }
+
+ override func viewDidDisappear(_ animated: Bool) {
+ super.viewDidDisappear(animated)
+ #if canImport(PythonKit)
+ if pythonReady {
+ let ptr = UInt(bitPattern: Unmanaged.passUnretained(self).toOpaque())
+ do {
+ let pn = try Python.attemptImport("pythonnative.page")
+ _ = try pn.forward_lifecycle.throwing.dynamicallyCall(withArguments: [ptr, "on_stop"])
+ } catch {}
+ }
+ #endif
+ }
+
+ override func encodeRestorableState(with coder: NSCoder) {
+ super.encodeRestorableState(with: coder)
+ #if canImport(PythonKit)
+ if pythonReady {
+ let ptr = UInt(bitPattern: Unmanaged.passUnretained(self).toOpaque())
+ do {
+ let pn = try Python.attemptImport("pythonnative.page")
+ _ = try pn.forward_lifecycle.throwing.dynamicallyCall(withArguments: [ptr, "on_save_instance_state"])
+ } catch {}
+ }
+ #endif
+ }
+
+ override func decodeRestorableState(with coder: NSCoder) {
+ super.decodeRestorableState(with: coder)
+ #if canImport(PythonKit)
+ if pythonReady {
+ let ptr = UInt(bitPattern: Unmanaged.passUnretained(self).toOpaque())
+ do {
+ let pn = try Python.attemptImport("pythonnative.page")
+ _ = try pn.forward_lifecycle.throwing.dynamicallyCall(withArguments: [ptr, "on_restore_instance_state"])
+ } catch {}
+ }
+ #endif
+ }
+
+ deinit {
+ #if canImport(PythonKit)
+ if pythonReady {
+ let ptr = UInt(bitPattern: Unmanaged.passUnretained(self).toOpaque())
+ do {
+ let pn = try Python.attemptImport("pythonnative.page")
+ _ = try pn.forward_lifecycle.throwing.dynamicallyCall(withArguments: [ptr, "on_destroy"])
+ } catch {}
+ }
+ #endif
+ }
+
}
diff --git a/src/pythonnative/utils.py b/src/pythonnative/utils.py
index 01d3101..1cbde95 100644
--- a/src/pythonnative/utils.py
+++ b/src/pythonnative/utils.py
@@ -39,8 +39,9 @@ def _get_is_android() -> bool:
IS_ANDROID: bool = _get_is_android()
-# Global hook to access the current Android Activity/Context from Python code
+# Global hooks to access current Android Activity/Context and Fragment container from Python code
_android_context: Any = None
+_android_fragment_container: Any = None
def set_android_context(context: Any) -> None:
@@ -55,6 +56,15 @@ def set_android_context(context: Any) -> None:
_android_context = context
+def set_android_fragment_container(container_view: Any) -> None:
+ """Record the current Fragment root container ViewGroup for rendering pages.
+
+ The current Page's `set_root_view` will attach its native view to this container.
+ """
+ global _android_fragment_container
+ _android_fragment_container = container_view
+
+
def get_android_context() -> Any:
"""Return the previously set Android Activity/Context or raise if missing."""
@@ -65,3 +75,17 @@ def get_android_context() -> Any:
"Android context is not set. Ensure Page is initialized from an Activity " "before constructing views."
)
return _android_context
+
+
+def get_android_fragment_container() -> Any:
+ """Return the previously set Fragment container ViewGroup or raise if missing.
+
+ This is set by the host `PageFragment` when its view is created.
+ """
+ if not IS_ANDROID:
+ raise RuntimeError("get_android_fragment_container() called on non-Android platform")
+ if _android_fragment_container is None:
+ raise RuntimeError(
+ "Android fragment container is not set. Ensure PageFragment has been created before set_root_view."
+ )
+ return _android_fragment_container
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 25b4991..f9b0eaf 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -22,7 +22,7 @@ def test_cli_init_and_clean():
assert os.path.isfile(main_page_path)
with open(main_page_path, "r", encoding="utf-8") as f:
content = f.read()
- assert "def bootstrap(" in content
+ assert "class MainPage(" in content
assert os.path.isfile(os.path.join(tmpdir, "pythonnative.json"))
assert os.path.isfile(os.path.join(tmpdir, "requirements.txt"))
assert os.path.isfile(os.path.join(tmpdir, ".gitignore"))
@@ -50,7 +50,31 @@ def test_cli_run_prepare_only_android_and_ios():
# prepare-only android
result = run_pn(["run", "android", "--prepare-only"], tmpdir)
assert result.returncode == 0, result.stderr
- assert os.path.isdir(os.path.join(tmpdir, "build", "android", "android_template"))
+ android_root = os.path.join(tmpdir, "build", "android", "android_template")
+ assert os.path.isdir(android_root)
+ # Ensure new Fragment-based navigation exists
+ page_fragment = os.path.join(
+ android_root,
+ "app",
+ "src",
+ "main",
+ "java",
+ "com",
+ "pythonnative",
+ "android_template",
+ "PageFragment.kt",
+ )
+ assert os.path.isfile(page_fragment)
+ nav_graph = os.path.join(
+ android_root,
+ "app",
+ "src",
+ "main",
+ "res",
+ "navigation",
+ "nav_graph.xml",
+ )
+ assert os.path.isfile(nav_graph)
# prepare-only ios
result = run_pn(["run", "ios", "--prepare-only"], tmpdir)