pythonnative 0.2.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pythonnative-0.2.0/src/pythonnative.egg-info → pythonnative-0.3.0}/PKG-INFO +1 -1
- {pythonnative-0.2.0 → pythonnative-0.3.0}/pyproject.toml +1 -1
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/__init__.py +1 -1
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/cli/pn.py +1 -8
- pythonnative-0.3.0/src/pythonnative/page.py +396 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/app/build.gradle +3 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +10 -7
- pythonnative-0.3.0/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/Navigator.kt +26 -0
- pythonnative-0.3.0/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PageFragment.kt +111 -0
- pythonnative-0.3.0/src/pythonnative/templates/android_template/app/src/main/res/layout/activity_main.xml +10 -0
- pythonnative-0.3.0/src/pythonnative/templates/android_template/app/src/main/res/navigation/nav_graph.xml +22 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +7 -4
- pythonnative-0.3.0/src/pythonnative/templates/ios_template/ios_template/ViewController.swift +218 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/utils.py +25 -1
- {pythonnative-0.2.0 → pythonnative-0.3.0/src/pythonnative.egg-info}/PKG-INFO +1 -1
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative.egg-info/SOURCES.txt +3 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/tests/test_cli.py +26 -2
- pythonnative-0.2.0/src/pythonnative/page.py +0 -209
- pythonnative-0.2.0/src/pythonnative/templates/android_template/app/src/main/res/layout/activity_main.xml +0 -18
- pythonnative-0.2.0/src/pythonnative/templates/ios_template/ios_template/ViewController.swift +0 -118
- {pythonnative-0.2.0 → pythonnative-0.3.0}/LICENSE +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/README.md +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/setup.cfg +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/activity_indicator_view.py +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/button.py +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/cli/__init__.py +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/collection_view.py +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/date_picker.py +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/image_view.py +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/label.py +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/list_view.py +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/material_activity_indicator_view.py +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/material_bottom_navigation_view.py +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/material_button.py +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/material_date_picker.py +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/material_progress_view.py +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/material_search_bar.py +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/material_switch.py +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/material_time_picker.py +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/material_toolbar.py +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/picker_view.py +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/progress_view.py +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/scroll_view.py +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/search_bar.py +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/stack_view.py +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/switch.py +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/app/proguard-rules.pro +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/app/src/androidTest/java/com/pythonnative/android_template/ExampleInstrumentedTest.kt +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/app/src/main/res/drawable/ic_launcher_background.xml +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher.webp +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher.webp +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher.webp +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/app/src/main/res/values/colors.xml +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/app/src/main/res/values/strings.xml +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/app/src/main/res/values/themes.xml +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/app/src/main/res/values-night/themes.xml +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/app/src/main/res/xml/backup_rules.xml +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/app/src/main/res/xml/data_extraction_rules.xml +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/app/src/test/java/com/pythonnative/android_template/ExampleUnitTest.kt +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/build.gradle +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.jar +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.properties +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/gradle.properties +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/gradlew +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/gradlew.bat +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/settings.gradle +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/ios_template/ios_template/AppDelegate.swift +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/AccentColor.colorset/Contents.json +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/AppIcon.appiconset/Contents.json +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/Contents.json +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/ios_template/ios_template/Base.lproj/LaunchScreen.storyboard +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/ios_template/ios_template/Base.lproj/Main.storyboard +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/ios_template/ios_template/Info.plist +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/ios_template/ios_template.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/ios_template/ios_templateTests/ios_templateTests.swift +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITests.swift +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITestsLaunchTests.swift +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/text_field.py +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/text_view.py +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/time_picker.py +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/view.py +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/web_view.py +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative.egg-info/dependency_links.txt +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative.egg-info/entry_points.txt +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative.egg-info/requires.txt +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative.egg-info/top_level.txt +0 -0
- {pythonnative-0.2.0 → pythonnative-0.3.0}/tests/test_smoke.py +0 -0
|
@@ -41,7 +41,7 @@ def init_project(args: argparse.Namespace) -> None:
|
|
|
41
41
|
|
|
42
42
|
os.makedirs(app_dir, exist_ok=True)
|
|
43
43
|
|
|
44
|
-
# Minimal hello world app scaffold
|
|
44
|
+
# Minimal hello world app scaffold (no bootstrap function; host instantiates Page directly)
|
|
45
45
|
main_page_py = os.path.join(app_dir, "main_page.py")
|
|
46
46
|
if not os.path.exists(main_page_py) or args.force:
|
|
47
47
|
with open(main_page_py, "w", encoding="utf-8") as f:
|
|
@@ -61,13 +61,6 @@ class MainPage(pn.Page):
|
|
|
61
61
|
button.set_on_click(lambda: print("Button clicked"))
|
|
62
62
|
stack.add_view(button)
|
|
63
63
|
self.set_root_view(stack)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def bootstrap(native_instance):
|
|
67
|
-
'''Entry point called by the host app (Android Activity or iOS ViewController).'''
|
|
68
|
-
page = MainPage(native_instance)
|
|
69
|
-
page.on_create()
|
|
70
|
-
return page
|
|
71
64
|
"""
|
|
72
65
|
)
|
|
73
66
|
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Your current approach, which involves creating an Android Activity in Kotlin
|
|
3
|
+
and then passing it to Python, is necessary due to the restrictions inherent
|
|
4
|
+
in Android's lifecycle. You are correctly following the Android way of managing
|
|
5
|
+
Activities. In Android, the system is in control of when and how Activities are
|
|
6
|
+
created and destroyed. It is not possible to directly create an instance of an
|
|
7
|
+
Activity from Python because that would bypass Android's lifecycle management,
|
|
8
|
+
leading to unpredictable results.
|
|
9
|
+
|
|
10
|
+
Your Button example works because Button is a View, not an Activity. View
|
|
11
|
+
instances in Android can be created and managed directly by your code. This is
|
|
12
|
+
why you are able to create an instance of Button from Python.
|
|
13
|
+
|
|
14
|
+
Remember that Activities in Android are not just containers for your UI like a
|
|
15
|
+
ViewGroup, they are also the main entry points into your app and are closely
|
|
16
|
+
tied to the app's lifecycle. Therefore, Android needs to maintain tight control
|
|
17
|
+
over them. Activities aren't something you instantiate whenever you need them;
|
|
18
|
+
they are created in response to a specific intent and their lifecycle is
|
|
19
|
+
managed by Android.
|
|
20
|
+
|
|
21
|
+
So, to answer your question: Yes, you need to follow this approach for
|
|
22
|
+
Activities in Android. You cannot instantiate an Activity from Python like you
|
|
23
|
+
do for Views.
|
|
24
|
+
|
|
25
|
+
On the other hand, for iOS, you can instantiate a UIViewController directly
|
|
26
|
+
from Python. The example code you provided for this is correct.
|
|
27
|
+
|
|
28
|
+
Just ensure that your PythonNative UI framework is aware of these platform
|
|
29
|
+
differences and handles them appropriately.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
import json
|
|
33
|
+
from abc import ABC, abstractmethod
|
|
34
|
+
from typing import Any, Optional, Union
|
|
35
|
+
|
|
36
|
+
from .utils import IS_ANDROID, set_android_context
|
|
37
|
+
from .view import ViewBase
|
|
38
|
+
|
|
39
|
+
# ========================================
|
|
40
|
+
# Base class
|
|
41
|
+
# ========================================
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class PageBase(ABC):
|
|
45
|
+
@abstractmethod
|
|
46
|
+
def __init__(self) -> None:
|
|
47
|
+
super().__init__()
|
|
48
|
+
|
|
49
|
+
@abstractmethod
|
|
50
|
+
def set_root_view(self, view) -> None:
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
@abstractmethod
|
|
54
|
+
def on_create(self) -> None:
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
@abstractmethod
|
|
58
|
+
def on_start(self) -> None:
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
@abstractmethod
|
|
62
|
+
def on_resume(self) -> None:
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
@abstractmethod
|
|
66
|
+
def on_pause(self) -> None:
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
@abstractmethod
|
|
70
|
+
def on_stop(self) -> None:
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
@abstractmethod
|
|
74
|
+
def on_destroy(self) -> None:
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
@abstractmethod
|
|
78
|
+
def on_restart(self) -> None:
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
@abstractmethod
|
|
82
|
+
def on_save_instance_state(self) -> None:
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
@abstractmethod
|
|
86
|
+
def on_restore_instance_state(self) -> None:
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
@abstractmethod
|
|
90
|
+
def set_args(self, args: Optional[dict]) -> None:
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
@abstractmethod
|
|
94
|
+
def push(self, page: Union[str, Any], args: Optional[dict] = None) -> None:
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
@abstractmethod
|
|
98
|
+
def pop(self) -> None:
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
def get_args(self) -> dict:
|
|
102
|
+
"""Return arguments provided to this Page (empty dict if none)."""
|
|
103
|
+
# Concrete classes should set self._args; default empty
|
|
104
|
+
return getattr(self, "_args", {})
|
|
105
|
+
|
|
106
|
+
# Back-compat: navigate_to delegates to push
|
|
107
|
+
def navigate_to(self, page) -> None:
|
|
108
|
+
self.push(page)
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
if IS_ANDROID:
|
|
113
|
+
# ========================================
|
|
114
|
+
# Android class
|
|
115
|
+
# https://developer.android.com/reference/android/app/Activity
|
|
116
|
+
# ========================================
|
|
117
|
+
|
|
118
|
+
from java import jclass
|
|
119
|
+
|
|
120
|
+
class Page(PageBase, ViewBase):
|
|
121
|
+
def __init__(self, native_instance) -> None:
|
|
122
|
+
super().__init__()
|
|
123
|
+
self.native_class = jclass("android.app.Activity")
|
|
124
|
+
self.native_instance = native_instance
|
|
125
|
+
# self.native_instance = self.native_class()
|
|
126
|
+
# Stash the Activity so child views can implicitly acquire a Context
|
|
127
|
+
set_android_context(native_instance)
|
|
128
|
+
self._args: dict = {}
|
|
129
|
+
|
|
130
|
+
def set_root_view(self, view) -> None:
|
|
131
|
+
# In fragment-based navigation, attach child view to the current fragment container.
|
|
132
|
+
try:
|
|
133
|
+
from .utils import get_android_fragment_container
|
|
134
|
+
|
|
135
|
+
container = get_android_fragment_container()
|
|
136
|
+
# Remove previous children if any, then add the new root
|
|
137
|
+
try:
|
|
138
|
+
container.removeAllViews()
|
|
139
|
+
except Exception:
|
|
140
|
+
pass
|
|
141
|
+
container.addView(view.native_instance)
|
|
142
|
+
except Exception:
|
|
143
|
+
# Fallback to setting content view directly on the Activity
|
|
144
|
+
self.native_instance.setContentView(view.native_instance)
|
|
145
|
+
|
|
146
|
+
def on_create(self) -> None:
|
|
147
|
+
print("Android on_create() called")
|
|
148
|
+
|
|
149
|
+
def on_start(self) -> None:
|
|
150
|
+
print("Android on_start() called")
|
|
151
|
+
|
|
152
|
+
def on_resume(self) -> None:
|
|
153
|
+
print("Android on_resume() called")
|
|
154
|
+
|
|
155
|
+
def on_pause(self) -> None:
|
|
156
|
+
print("Android on_pause() called")
|
|
157
|
+
|
|
158
|
+
def on_stop(self) -> None:
|
|
159
|
+
print("Android on_stop() called")
|
|
160
|
+
|
|
161
|
+
def on_destroy(self) -> None:
|
|
162
|
+
print("Android on_destroy() called")
|
|
163
|
+
|
|
164
|
+
def on_restart(self) -> None:
|
|
165
|
+
print("Android on_restart() called")
|
|
166
|
+
|
|
167
|
+
def on_save_instance_state(self) -> None:
|
|
168
|
+
print("Android on_save_instance_state() called")
|
|
169
|
+
|
|
170
|
+
def on_restore_instance_state(self) -> None:
|
|
171
|
+
print("Android on_restore_instance_state() called")
|
|
172
|
+
|
|
173
|
+
def set_args(self, args: Optional[dict]) -> None:
|
|
174
|
+
# Accept dict or JSON string for convenience when crossing language boundaries
|
|
175
|
+
if isinstance(args, str):
|
|
176
|
+
try:
|
|
177
|
+
self._args = json.loads(args) or {}
|
|
178
|
+
return
|
|
179
|
+
except Exception:
|
|
180
|
+
self._args = {}
|
|
181
|
+
return
|
|
182
|
+
self._args = args or {}
|
|
183
|
+
|
|
184
|
+
def _resolve_page_path(self, page: Union[str, Any]) -> str:
|
|
185
|
+
if isinstance(page, str):
|
|
186
|
+
return page
|
|
187
|
+
# If a class or instance is passed, derive dotted path
|
|
188
|
+
try:
|
|
189
|
+
module = getattr(page, "__module__", None)
|
|
190
|
+
name = getattr(page, "__name__", None)
|
|
191
|
+
if module and name:
|
|
192
|
+
return f"{module}.{name}"
|
|
193
|
+
# Instance: use its class
|
|
194
|
+
cls = page.__class__
|
|
195
|
+
return f"{cls.__module__}.{cls.__name__}"
|
|
196
|
+
except Exception:
|
|
197
|
+
raise ValueError("Unsupported page reference; expected dotted string or class/instance")
|
|
198
|
+
|
|
199
|
+
def push(self, page: Union[str, Any], args: Optional[dict] = None) -> None:
|
|
200
|
+
# Delegate to Navigator.push to navigate to PageFragment with arguments
|
|
201
|
+
page_path = self._resolve_page_path(page)
|
|
202
|
+
try:
|
|
203
|
+
Navigator = jclass(f"{self.native_instance.getPackageName()}.Navigator")
|
|
204
|
+
args_json = json.dumps(args) if args else None
|
|
205
|
+
Navigator.push(self.native_instance, page_path, args_json)
|
|
206
|
+
except Exception:
|
|
207
|
+
# As a last resort, do nothing rather than crash
|
|
208
|
+
pass
|
|
209
|
+
|
|
210
|
+
def pop(self) -> None:
|
|
211
|
+
# Delegate to Navigator.pop for back-stack pop
|
|
212
|
+
try:
|
|
213
|
+
Navigator = jclass(f"{self.native_instance.getPackageName()}.Navigator")
|
|
214
|
+
Navigator.pop(self.native_instance)
|
|
215
|
+
except Exception:
|
|
216
|
+
try:
|
|
217
|
+
self.native_instance.finish()
|
|
218
|
+
except Exception:
|
|
219
|
+
pass
|
|
220
|
+
|
|
221
|
+
else:
|
|
222
|
+
# ========================================
|
|
223
|
+
# iOS class
|
|
224
|
+
# https://developer.apple.com/documentation/uikit/uiviewcontroller
|
|
225
|
+
# ========================================
|
|
226
|
+
|
|
227
|
+
from typing import Dict
|
|
228
|
+
|
|
229
|
+
from rubicon.objc import ObjCClass, ObjCInstance
|
|
230
|
+
|
|
231
|
+
# Global registry mapping native UIViewController pointer address to Page instances.
|
|
232
|
+
_IOS_PAGE_REGISTRY: Dict[int, Any] = {}
|
|
233
|
+
|
|
234
|
+
def _ios_register_page(vc_instance: Any, page_obj: Any) -> None:
|
|
235
|
+
try:
|
|
236
|
+
ptr = int(vc_instance.ptr) # rubicon ObjCInstance -> c_void_p convertible to int
|
|
237
|
+
_IOS_PAGE_REGISTRY[ptr] = page_obj
|
|
238
|
+
except Exception:
|
|
239
|
+
pass
|
|
240
|
+
|
|
241
|
+
def _ios_unregister_page(vc_instance: Any) -> None:
|
|
242
|
+
try:
|
|
243
|
+
ptr = int(vc_instance.ptr)
|
|
244
|
+
_IOS_PAGE_REGISTRY.pop(ptr, None)
|
|
245
|
+
except Exception:
|
|
246
|
+
pass
|
|
247
|
+
|
|
248
|
+
def forward_lifecycle(native_addr: int, event: str) -> None:
|
|
249
|
+
"""Forward a lifecycle event from Swift ViewController to the registered Page.
|
|
250
|
+
|
|
251
|
+
:param native_addr: Integer pointer address of the UIViewController
|
|
252
|
+
:param event: One of 'on_start', 'on_resume', 'on_pause', 'on_stop', 'on_destroy',
|
|
253
|
+
'on_save_instance_state', 'on_restore_instance_state'.
|
|
254
|
+
"""
|
|
255
|
+
page = _IOS_PAGE_REGISTRY.get(int(native_addr))
|
|
256
|
+
if not page:
|
|
257
|
+
return
|
|
258
|
+
try:
|
|
259
|
+
handler = getattr(page, event, None)
|
|
260
|
+
if handler:
|
|
261
|
+
handler()
|
|
262
|
+
except Exception:
|
|
263
|
+
# Avoid surfacing exceptions across the Swift/Python boundary in lifecycle
|
|
264
|
+
pass
|
|
265
|
+
|
|
266
|
+
class Page(PageBase, ViewBase):
|
|
267
|
+
def __init__(self, native_instance) -> None:
|
|
268
|
+
super().__init__()
|
|
269
|
+
self.native_class = ObjCClass("UIViewController")
|
|
270
|
+
# If Swift passed us an integer pointer, wrap it as an ObjCInstance.
|
|
271
|
+
if isinstance(native_instance, int):
|
|
272
|
+
try:
|
|
273
|
+
native_instance = ObjCInstance(native_instance)
|
|
274
|
+
except Exception:
|
|
275
|
+
native_instance = None
|
|
276
|
+
self.native_instance = native_instance
|
|
277
|
+
# self.native_instance = self.native_class.alloc().init()
|
|
278
|
+
self._args: dict = {}
|
|
279
|
+
# Register for lifecycle forwarding
|
|
280
|
+
if self.native_instance is not None:
|
|
281
|
+
_ios_register_page(self.native_instance, self)
|
|
282
|
+
|
|
283
|
+
def set_root_view(self, view) -> None:
|
|
284
|
+
# UIViewController.view is a property; access without calling.
|
|
285
|
+
root_view = self.native_instance.view
|
|
286
|
+
# Size the root child to fill the controller's view and enable autoresizing
|
|
287
|
+
try:
|
|
288
|
+
bounds = root_view.bounds
|
|
289
|
+
view.native_instance.setFrame_(bounds)
|
|
290
|
+
# UIViewAutoresizingFlexibleWidth (2) | UIViewAutoresizingFlexibleHeight (16)
|
|
291
|
+
view.native_instance.setAutoresizingMask_(2 | 16)
|
|
292
|
+
except Exception:
|
|
293
|
+
pass
|
|
294
|
+
root_view.addSubview_(view.native_instance)
|
|
295
|
+
|
|
296
|
+
def on_create(self) -> None:
|
|
297
|
+
print("iOS on_create() called")
|
|
298
|
+
|
|
299
|
+
def on_start(self) -> None:
|
|
300
|
+
print("iOS on_start() called")
|
|
301
|
+
|
|
302
|
+
def on_resume(self) -> None:
|
|
303
|
+
print("iOS on_resume() called")
|
|
304
|
+
|
|
305
|
+
def on_pause(self) -> None:
|
|
306
|
+
print("iOS on_pause() called")
|
|
307
|
+
|
|
308
|
+
def on_stop(self) -> None:
|
|
309
|
+
print("iOS on_stop() called")
|
|
310
|
+
|
|
311
|
+
def on_destroy(self) -> None:
|
|
312
|
+
print("iOS on_destroy() called")
|
|
313
|
+
if self.native_instance is not None:
|
|
314
|
+
_ios_unregister_page(self.native_instance)
|
|
315
|
+
|
|
316
|
+
def on_restart(self) -> None:
|
|
317
|
+
print("iOS on_restart() called")
|
|
318
|
+
|
|
319
|
+
def on_save_instance_state(self) -> None:
|
|
320
|
+
print("iOS on_save_instance_state() called")
|
|
321
|
+
|
|
322
|
+
def on_restore_instance_state(self) -> None:
|
|
323
|
+
print("iOS on_restore_instance_state() called")
|
|
324
|
+
|
|
325
|
+
def set_args(self, args: Optional[dict]) -> None:
|
|
326
|
+
if isinstance(args, str):
|
|
327
|
+
try:
|
|
328
|
+
self._args = json.loads(args) or {}
|
|
329
|
+
return
|
|
330
|
+
except Exception:
|
|
331
|
+
self._args = {}
|
|
332
|
+
return
|
|
333
|
+
self._args = args or {}
|
|
334
|
+
|
|
335
|
+
def _resolve_page_path(self, page: Union[str, Any]) -> str:
|
|
336
|
+
if isinstance(page, str):
|
|
337
|
+
return page
|
|
338
|
+
try:
|
|
339
|
+
module = getattr(page, "__module__", None)
|
|
340
|
+
name = getattr(page, "__name__", None)
|
|
341
|
+
if module and name:
|
|
342
|
+
return f"{module}.{name}"
|
|
343
|
+
cls = page.__class__
|
|
344
|
+
return f"{cls.__module__}.{cls.__name__}"
|
|
345
|
+
except Exception:
|
|
346
|
+
raise ValueError("Unsupported page reference; expected dotted string or class/instance")
|
|
347
|
+
|
|
348
|
+
def push(self, page: Union[str, Any], args: Optional[dict] = None) -> None:
|
|
349
|
+
page_path = self._resolve_page_path(page)
|
|
350
|
+
# Resolve the Swift ViewController class. Swift classes are namespaced by
|
|
351
|
+
# the module name (CFBundleName). Try plain name first, then Module.Name.
|
|
352
|
+
ViewController = None
|
|
353
|
+
try:
|
|
354
|
+
ViewController = ObjCClass("ViewController")
|
|
355
|
+
except Exception:
|
|
356
|
+
try:
|
|
357
|
+
NSBundle = ObjCClass("NSBundle")
|
|
358
|
+
bundle = NSBundle.mainBundle
|
|
359
|
+
module_name = None
|
|
360
|
+
try:
|
|
361
|
+
# Prefer CFBundleName; fallback to CFBundleExecutable
|
|
362
|
+
module_name = bundle.objectForInfoDictionaryKey_("CFBundleName")
|
|
363
|
+
if module_name is None:
|
|
364
|
+
module_name = bundle.objectForInfoDictionaryKey_("CFBundleExecutable")
|
|
365
|
+
except Exception:
|
|
366
|
+
module_name = None
|
|
367
|
+
if module_name:
|
|
368
|
+
ViewController = ObjCClass(f"{module_name}.ViewController")
|
|
369
|
+
except Exception:
|
|
370
|
+
ViewController = None
|
|
371
|
+
|
|
372
|
+
if ViewController is None:
|
|
373
|
+
raise NameError("ViewController class not found; ensure Swift class is ObjC-visible")
|
|
374
|
+
|
|
375
|
+
next_vc = ViewController.alloc().init()
|
|
376
|
+
try:
|
|
377
|
+
# Use KVC to pass metadata to Swift
|
|
378
|
+
next_vc.setValue_forKey_(page_path, "requestedPagePath")
|
|
379
|
+
if args:
|
|
380
|
+
next_vc.setValue_forKey_(json.dumps(args), "requestedPageArgsJSON")
|
|
381
|
+
except Exception:
|
|
382
|
+
pass
|
|
383
|
+
# On iOS, `navigationController` is exposed as a property; treat it as such.
|
|
384
|
+
nav = getattr(self.native_instance, "navigationController", None)
|
|
385
|
+
if nav is None:
|
|
386
|
+
# If no navigation controller, this push will be a no-op; rely on template to embed one.
|
|
387
|
+
raise RuntimeError(
|
|
388
|
+
"No UINavigationController available; ensure template embeds root in navigation controller"
|
|
389
|
+
)
|
|
390
|
+
# Method name maps from pushViewController:animated:
|
|
391
|
+
nav.pushViewController_animated_(next_vc, True)
|
|
392
|
+
|
|
393
|
+
def pop(self) -> None:
|
|
394
|
+
nav = getattr(self.native_instance, "navigationController", None)
|
|
395
|
+
if nav is not None:
|
|
396
|
+
nav.popViewControllerAnimated_(True)
|
|
@@ -53,6 +53,9 @@ dependencies {
|
|
|
53
53
|
implementation 'androidx.appcompat:appcompat:1.4.1'
|
|
54
54
|
implementation 'com.google.android.material:material:1.5.0'
|
|
55
55
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
|
56
|
+
// AndroidX Navigation for Fragment-based navigation
|
|
57
|
+
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.7'
|
|
58
|
+
implementation 'androidx.navigation:navigation-ui-ktx:2.7.7'
|
|
56
59
|
testImplementation 'junit:junit:4.13.2'
|
|
57
60
|
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
|
58
61
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
|
@@ -8,25 +8,28 @@ import com.chaquo.python.Python
|
|
|
8
8
|
import com.chaquo.python.android.AndroidPlatform
|
|
9
9
|
|
|
10
10
|
class MainActivity : AppCompatActivity() {
|
|
11
|
+
private val TAG = javaClass.simpleName
|
|
12
|
+
|
|
11
13
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
12
14
|
super.onCreate(savedInstanceState)
|
|
13
|
-
|
|
15
|
+
Log.d(TAG, "onCreate() called")
|
|
14
16
|
|
|
15
17
|
// Initialize Chaquopy
|
|
16
18
|
if (!Python.isStarted()) {
|
|
17
19
|
Python.start(AndroidPlatform(this))
|
|
18
20
|
}
|
|
19
21
|
try {
|
|
22
|
+
// Set content view to the NavHost layout; the initial page loads via nav_graph startDestination
|
|
23
|
+
setContentView(R.layout.activity_main)
|
|
24
|
+
// Optionally, bootstrap Python so first fragment can create the initial page onCreate
|
|
20
25
|
val py = Python.getInstance()
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
// Python Page will set the content view via set_root_view
|
|
26
|
+
// Touch module to ensure bundled Python code is available; actual instantiation happens in PageFragment
|
|
27
|
+
py.getModule("app.main_page")
|
|
24
28
|
} catch (e: Exception) {
|
|
25
|
-
Log.e("PythonNative", "
|
|
26
|
-
// Fallback: show a simple native label if Python bootstrap fails
|
|
29
|
+
Log.e("PythonNative", "Bootstrap failed", e)
|
|
27
30
|
val tv = TextView(this)
|
|
28
31
|
tv.text = "Hello from PythonNative (Android template)"
|
|
29
32
|
setContentView(tv)
|
|
30
33
|
}
|
|
31
34
|
}
|
|
32
|
-
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
package com.pythonnative.android_template
|
|
2
|
+
|
|
3
|
+
import android.os.Bundle
|
|
4
|
+
import androidx.core.os.bundleOf
|
|
5
|
+
import androidx.fragment.app.FragmentActivity
|
|
6
|
+
import androidx.navigation.fragment.NavHostFragment
|
|
7
|
+
|
|
8
|
+
object Navigator {
|
|
9
|
+
@JvmStatic
|
|
10
|
+
fun push(activity: FragmentActivity, pagePath: String, argsJson: String?) {
|
|
11
|
+
val navHost = activity.supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
|
|
12
|
+
val navController = navHost.navController
|
|
13
|
+
val args = Bundle()
|
|
14
|
+
args.putString("page_path", pagePath)
|
|
15
|
+
if (argsJson != null) {
|
|
16
|
+
args.putString("args_json", argsJson)
|
|
17
|
+
}
|
|
18
|
+
navController.navigate(R.id.pageFragment, args)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@JvmStatic
|
|
22
|
+
fun pop(activity: FragmentActivity) {
|
|
23
|
+
val navHost = activity.supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
|
|
24
|
+
navHost.navController.popBackStack()
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
package com.pythonnative.android_template
|
|
2
|
+
|
|
3
|
+
import android.os.Bundle
|
|
4
|
+
import android.util.Log
|
|
5
|
+
import android.view.LayoutInflater
|
|
6
|
+
import android.view.View
|
|
7
|
+
import android.view.ViewGroup
|
|
8
|
+
import android.widget.FrameLayout
|
|
9
|
+
import androidx.core.os.bundleOf
|
|
10
|
+
import androidx.fragment.app.Fragment
|
|
11
|
+
import com.chaquo.python.PyObject
|
|
12
|
+
import com.chaquo.python.Python
|
|
13
|
+
import com.chaquo.python.android.AndroidPlatform
|
|
14
|
+
|
|
15
|
+
class PageFragment : Fragment() {
|
|
16
|
+
private val TAG = javaClass.simpleName
|
|
17
|
+
private var page: PyObject? = null
|
|
18
|
+
|
|
19
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
20
|
+
super.onCreate(savedInstanceState)
|
|
21
|
+
if (!Python.isStarted()) {
|
|
22
|
+
context?.let { Python.start(AndroidPlatform(it)) }
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
val py = Python.getInstance()
|
|
26
|
+
val pagePath = arguments?.getString("page_path") ?: "app.main_page.MainPage"
|
|
27
|
+
val argsJson = arguments?.getString("args_json")
|
|
28
|
+
val moduleName = pagePath.substringBeforeLast('.')
|
|
29
|
+
val className = pagePath.substringAfterLast('.')
|
|
30
|
+
val pyModule = py.getModule(moduleName)
|
|
31
|
+
val pageClass = pyModule.get(className)
|
|
32
|
+
// Pass the hosting Activity as native_instance for context
|
|
33
|
+
page = pageClass?.call(requireActivity())
|
|
34
|
+
if (!argsJson.isNullOrEmpty()) {
|
|
35
|
+
page?.callAttr("set_args", argsJson)
|
|
36
|
+
}
|
|
37
|
+
} catch (e: Exception) {
|
|
38
|
+
Log.e(TAG, "Failed to instantiate page", e)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
override fun onCreateView(
|
|
43
|
+
inflater: LayoutInflater,
|
|
44
|
+
container: ViewGroup?,
|
|
45
|
+
savedInstanceState: Bundle?
|
|
46
|
+
): View? {
|
|
47
|
+
// Create a simple container which Python-native views can be attached to.
|
|
48
|
+
val frame = FrameLayout(requireContext())
|
|
49
|
+
frame.layoutParams = ViewGroup.LayoutParams(
|
|
50
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
51
|
+
ViewGroup.LayoutParams.MATCH_PARENT
|
|
52
|
+
)
|
|
53
|
+
return frame
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
57
|
+
super.onViewCreated(view, savedInstanceState)
|
|
58
|
+
// Python side will call set_root_view to attach a native view to Activity.
|
|
59
|
+
// In fragment-based architecture, the Activity will set contentView once,
|
|
60
|
+
// so we ensure the fragment's container is available for Python to target.
|
|
61
|
+
// Expose the fragment container to Python so Page.set_root_view can attach into it
|
|
62
|
+
try {
|
|
63
|
+
val py = Python.getInstance()
|
|
64
|
+
val utils = py.getModule("pythonnative.utils")
|
|
65
|
+
utils.callAttr("set_android_fragment_container", view)
|
|
66
|
+
// Now that container exists, invoke on_create so Python can attach its root view
|
|
67
|
+
page?.callAttr("on_create")
|
|
68
|
+
} catch (_: Exception) {
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
override fun onStart() {
|
|
73
|
+
super.onStart()
|
|
74
|
+
try { page?.callAttr("on_start") } catch (e: Exception) { Log.w(TAG, "on_start failed", e) }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
override fun onResume() {
|
|
78
|
+
super.onResume()
|
|
79
|
+
try { page?.callAttr("on_resume") } catch (e: Exception) { Log.w(TAG, "on_resume failed", e) }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
override fun onPause() {
|
|
83
|
+
super.onPause()
|
|
84
|
+
try { page?.callAttr("on_pause") } catch (e: Exception) { Log.w(TAG, "on_pause failed", e) }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
override fun onStop() {
|
|
88
|
+
super.onStop()
|
|
89
|
+
try { page?.callAttr("on_stop") } catch (e: Exception) { Log.w(TAG, "on_stop failed", e) }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
override fun onDestroyView() {
|
|
93
|
+
super.onDestroyView()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
override fun onDestroy() {
|
|
97
|
+
super.onDestroy()
|
|
98
|
+
try { page?.callAttr("on_destroy") } catch (e: Exception) { Log.w(TAG, "on_destroy failed", e) }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
companion object {
|
|
102
|
+
fun newInstance(pagePath: String, argsJson: String?): PageFragment {
|
|
103
|
+
val f = PageFragment()
|
|
104
|
+
f.arguments = bundleOf(
|
|
105
|
+
"page_path" to pagePath,
|
|
106
|
+
"args_json" to argsJson
|
|
107
|
+
)
|
|
108
|
+
return f
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<androidx.fragment.app.FragmentContainerView
|
|
3
|
+
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
4
|
+
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
5
|
+
android:id="@+id/nav_host_fragment"
|
|
6
|
+
android:name="androidx.navigation.fragment.NavHostFragment"
|
|
7
|
+
android:layout_width="match_parent"
|
|
8
|
+
android:layout_height="match_parent"
|
|
9
|
+
app:defaultNavHost="true"
|
|
10
|
+
app:navGraph="@navigation/nav_graph" />
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
|
3
|
+
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
4
|
+
xmlns:tools="http://schemas.android.com/tools"
|
|
5
|
+
android:id="@+id/nav_graph"
|
|
6
|
+
app:startDestination="@id/pageFragment">
|
|
7
|
+
|
|
8
|
+
<fragment
|
|
9
|
+
android:id="@+id/pageFragment"
|
|
10
|
+
android:name="com.pythonnative.android_template.PageFragment"
|
|
11
|
+
android:label="PageFragment">
|
|
12
|
+
<argument
|
|
13
|
+
android:name="page_path"
|
|
14
|
+
app:argType="string"
|
|
15
|
+
android:defaultValue="app.main_page.MainPage" />
|
|
16
|
+
<argument
|
|
17
|
+
android:name="args_json"
|
|
18
|
+
app:argType="string"
|
|
19
|
+
android:nullable="true" />
|
|
20
|
+
</fragment>
|
|
21
|
+
|
|
22
|
+
</navigation>
|