Skip to content

Bind the Android main looper via swift-android-native#69

Merged
colemancda merged 2 commits into
masterfrom
feature/android-native
Jul 24, 2026
Merged

Bind the Android main looper via swift-android-native#69
colemancda merged 2 commits into
masterfrom
feature/android-native

Conversation

@colemancda

Copy link
Copy Markdown
Member

Imports swift-android-native and binds the Android main looper to its AndroidMainActor at app launch, so @MainActor / DispatchQueue.main / Swift concurrency dispatch correctly on Android without hand-draining RunLoop.main.

What changed

  • Dependency: added swift-android-native to the AndroidSwiftUI package, with the AndroidLooper product on the AndroidSwiftUI target (Android-only). Same package identity as the PureSwift/Android dependency, so it's pinned to the same fork/branch (MillerTechnologyPeru @ feature/pureswift) to keep the graph on a single revision. The CoreFoundation trait is enabled — that's the setting that drains the dispatch main queue through CFRunLoopRunInMode.
  • Launch: Application.onCreateSwift() (runs on the main thread at process start) now calls AndroidMainActor.setupMainLooper().
  • Cleanup: removed MainActivity.runAsync() — the fragile RunLoop.main.run(until:) warmup, now redundant with the looper bound.

Reviewer note — why the render scheduler is still Handler.post

I attempted to simplify the interpreter's re-render scheduler from the manual Handler.post to DispatchQueue.main.async now that the looper is bound. It crashes, and the trait does not fix it:

JNI NewObjectArray called with pending exception java.lang.NoClassDefFoundError: Class not found using the boot class loader

Rendering makes JNI calls (materializing the ViewNode tree), and JNI FindClass resolves against the class loader of the Java frame on the stack. Handler.post runs the render inside a Runnable.run() frame carrying the app's class loader; the dispatch-queue drain runs in a native context whose fallback boot class loader can't see the app's classes. The CoreFoundation trait changes how the queue drains, not the class-loader context — so it can't help here. This is a swift-java limitation (would need FindClass to use a cached app ClassLoader on native threads). The scheduler therefore stays Handler.post, with the reason documented inline. AndroidMainActor / DispatchQueue.main remain correct for non-JNI main-thread work.

Verification

  • Android: app launches (AndroidMainActor.setupMainLooper() -> true in logcat), navigation works, and state re-renders (tapped a button, counter reached Taps: 3) — no crash.
  • Desktop rig builds (no dependency-resolution conflict); 117 SwiftUICore tests pass.

@colemancda
colemancda merged commit c3ce9e1 into master Jul 24, 2026
6 checks passed
@colemancda
colemancda deleted the feature/android-native branch July 24, 2026 16:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant