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.
Files changed (99) hide show
  1. {pythonnative-0.2.0/src/pythonnative.egg-info → pythonnative-0.3.0}/PKG-INFO +1 -1
  2. {pythonnative-0.2.0 → pythonnative-0.3.0}/pyproject.toml +1 -1
  3. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/__init__.py +1 -1
  4. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/cli/pn.py +1 -8
  5. pythonnative-0.3.0/src/pythonnative/page.py +396 -0
  6. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/app/build.gradle +3 -0
  7. {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
  8. pythonnative-0.3.0/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/Navigator.kt +26 -0
  9. pythonnative-0.3.0/src/pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PageFragment.kt +111 -0
  10. pythonnative-0.3.0/src/pythonnative/templates/android_template/app/src/main/res/layout/activity_main.xml +10 -0
  11. pythonnative-0.3.0/src/pythonnative/templates/android_template/app/src/main/res/navigation/nav_graph.xml +22 -0
  12. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +7 -4
  13. pythonnative-0.3.0/src/pythonnative/templates/ios_template/ios_template/ViewController.swift +218 -0
  14. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/utils.py +25 -1
  15. {pythonnative-0.2.0 → pythonnative-0.3.0/src/pythonnative.egg-info}/PKG-INFO +1 -1
  16. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative.egg-info/SOURCES.txt +3 -0
  17. {pythonnative-0.2.0 → pythonnative-0.3.0}/tests/test_cli.py +26 -2
  18. pythonnative-0.2.0/src/pythonnative/page.py +0 -209
  19. pythonnative-0.2.0/src/pythonnative/templates/android_template/app/src/main/res/layout/activity_main.xml +0 -18
  20. pythonnative-0.2.0/src/pythonnative/templates/ios_template/ios_template/ViewController.swift +0 -118
  21. {pythonnative-0.2.0 → pythonnative-0.3.0}/LICENSE +0 -0
  22. {pythonnative-0.2.0 → pythonnative-0.3.0}/README.md +0 -0
  23. {pythonnative-0.2.0 → pythonnative-0.3.0}/setup.cfg +0 -0
  24. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/activity_indicator_view.py +0 -0
  25. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/button.py +0 -0
  26. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/cli/__init__.py +0 -0
  27. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/collection_view.py +0 -0
  28. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/date_picker.py +0 -0
  29. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/image_view.py +0 -0
  30. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/label.py +0 -0
  31. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/list_view.py +0 -0
  32. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/material_activity_indicator_view.py +0 -0
  33. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/material_bottom_navigation_view.py +0 -0
  34. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/material_button.py +0 -0
  35. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/material_date_picker.py +0 -0
  36. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/material_progress_view.py +0 -0
  37. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/material_search_bar.py +0 -0
  38. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/material_switch.py +0 -0
  39. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/material_time_picker.py +0 -0
  40. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/material_toolbar.py +0 -0
  41. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/picker_view.py +0 -0
  42. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/progress_view.py +0 -0
  43. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/scroll_view.py +0 -0
  44. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/search_bar.py +0 -0
  45. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/stack_view.py +0 -0
  46. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/switch.py +0 -0
  47. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/app/proguard-rules.pro +0 -0
  48. {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
  49. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +0 -0
  50. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/app/src/main/res/drawable/ic_launcher_background.xml +0 -0
  51. {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
  52. {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
  53. {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
  54. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher.webp +0 -0
  55. {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
  56. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher.webp +0 -0
  57. {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
  58. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher.webp +0 -0
  59. {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
  60. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp +0 -0
  61. {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
  62. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp +0 -0
  63. {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
  64. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/app/src/main/res/values/colors.xml +0 -0
  65. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/app/src/main/res/values/strings.xml +0 -0
  66. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/app/src/main/res/values/themes.xml +0 -0
  67. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/app/src/main/res/values-night/themes.xml +0 -0
  68. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/app/src/main/res/xml/backup_rules.xml +0 -0
  69. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/app/src/main/res/xml/data_extraction_rules.xml +0 -0
  70. {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
  71. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/build.gradle +0 -0
  72. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.jar +0 -0
  73. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.properties +0 -0
  74. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/gradle.properties +0 -0
  75. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/gradlew +0 -0
  76. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/gradlew.bat +0 -0
  77. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/android_template/settings.gradle +0 -0
  78. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/ios_template/ios_template/AppDelegate.swift +0 -0
  79. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/AccentColor.colorset/Contents.json +0 -0
  80. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/AppIcon.appiconset/Contents.json +0 -0
  81. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/ios_template/ios_template/Assets.xcassets/Contents.json +0 -0
  82. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/ios_template/ios_template/Base.lproj/LaunchScreen.storyboard +0 -0
  83. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/ios_template/ios_template/Base.lproj/Main.storyboard +0 -0
  84. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/ios_template/ios_template/Info.plist +0 -0
  85. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +0 -0
  86. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/ios_template/ios_template.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -0
  87. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/ios_template/ios_templateTests/ios_templateTests.swift +0 -0
  88. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITests.swift +0 -0
  89. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITestsLaunchTests.swift +0 -0
  90. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/text_field.py +0 -0
  91. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/text_view.py +0 -0
  92. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/time_picker.py +0 -0
  93. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/view.py +0 -0
  94. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative/web_view.py +0 -0
  95. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative.egg-info/dependency_links.txt +0 -0
  96. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative.egg-info/entry_points.txt +0 -0
  97. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative.egg-info/requires.txt +0 -0
  98. {pythonnative-0.2.0 → pythonnative-0.3.0}/src/pythonnative.egg-info/top_level.txt +0 -0
  99. {pythonnative-0.2.0 → pythonnative-0.3.0}/tests/test_smoke.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythonnative
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Cross-platform native UI toolkit for Android and iOS
5
5
  Author: Owen Carey
6
6
  License: MIT License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pythonnative"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Cross-platform native UI toolkit for Android and iOS"
9
9
  authors = [
10
10
  { name = "Owen Carey" }
@@ -1,7 +1,7 @@
1
1
  from importlib import import_module
2
2
  from typing import Any, Dict
3
3
 
4
- __version__ = "0.2.0"
4
+ __version__ = "0.3.0"
5
5
 
6
6
  __all__ = [
7
7
  "ActivityIndicatorView",
@@ -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
- // setContentView(R.layout.activity_main)
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
- val pyModule = py.getModule("app.main_page")
22
- pyModule.callAttr("bootstrap", this)
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", "Python bootstrap failed", e)
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>