Warning
Android support in ModernFormsNext 1.11.0 is Experimental and is not recommended for production applications. See the Android platform status for the supported vertical slice and current limitations.
The Android backend includes an experimental Application/Form window host,
software Skia rendering and shared animation-runtime integration, with explicit mobile limitations.
Windows remains the primary and best-supported runtime. The Android project targets
net10.0-android with API 23 as its current minimum and has no MAUI or AndroidX dependency.
See Android platform services for the authoritative SAF, launcher, sharing, notification and message-dialog policy, including cancellation and custom Activity hooks.
ModernFormsNext.WindowKit.Backend.Android is prepared for the next coordinated release.
The already-published 1.11.1 release does not include it. After the next release, use
PackageReference with exactly the same version as the other ModernFormsNext packages:
<PackageReference Include="ModernFormsNext.WindowKit.Backend.Android"
Version="<matching ModernFormsNext version>" />Status: Experimental. APIs, behavior, and platform support may change between releases. Windows remains the primary stable backend. Package availability is not a production-readiness claim.
The package contains DLL/XML documentation for net10.0 (deterministic mapping/lifecycle logic)
and net10.0-android (native platform implementation), plus portable PDBs in .snupkg.
As a published library it now follows the central assembly/file identity policy, replacing the
source-only project's SDK default assembly version with ModernFormsNextAssemblyVersion.
The Android target retains minimum API 23. NuGet may normalize the asset folder to
net10.0-android36.0; 36 is the compile-time platform version, not the minimum device API.
Both dependency groups retain matching-version WindowKit and Backend packages. Only the Android
group adds SkiaSharp.Views 3.119.2 and System.Formats.Nrbf 10.0.10. Native assets flow through
those dependencies rather than copied third-party DLLs in this package.
The library manifest contributes narrow <queries> intents for SAF documents/directories,
HTTP(S), mail, dial, content launch, and sharing. It declares no permissions, launcher Activity,
application override, camera/microphone/storage/notification grants, or accessibility service.
The AAR explicitly records minimum SDK 23 and target SDK 36. This prevents Android's manifest
merger from treating an unspecified library target as legacy and injecting storage/phone
permissions. The application still owns its target SDK and capabilities.
Applications still declare and request their own opt-in capabilities. This does not replace the
application manifest or the permission contract.
For a local release dry-run, run scripts/Test-AndroidPackageConsumer.ps1 against the validated
local feed. It uses fresh isolated caches, no ProjectReferences, both TFMs, and real Debug APK,
Release APK/AAB builds with Release AOT/trimming enabled. No device or publication is required.
Keep the existing sample's Android-compatible HarfBuzz override when using shared text rendering.
Initialize on the Android main thread from the native Application's OnCreate, before accessing
WindowKit dispatcher or windowing services:
var backend = AndroidWindowKit.Initialize(new AndroidWindowKitOptions(this)
{
PermissionRequestTimeout = TimeSpan.FromMinutes(2),
DiagnosticSink = message => Android.Util.Log.Info("MFN.WindowKit", message)
});Use AndroidWindowActivity.OnStartApplication to call Application.Run(new MainForm(...)) once.
The backend supplies the Activity/Skia presentation and reconnects surviving Forms after Activity
recreation. See the complete startup example; normal application
startup does not manually construct a Skia host view or control surface.
Repeated initialization with the same Application Context is idempotent. A different context or a
second platform backend is rejected. The normalized AndroidApplicationContext retains the
process-lifetime Application, never the activity.
AndroidActivityTracker registers as Application.IActivityLifecycleCallbacks. It keeps the latest
activity through WeakReference<Activity> and reports Unknown, Created, Foreground,
Background, or NoActivity. Only a resumed, non-finishing, non-destroyed activity is eligible to
show permission UI or open settings.
During rotation, destruction clears the weak reference only when the destroyed activity is still
the current host. A delayed callback from the old activity therefore cannot erase a replacement
activity that has already been created or resumed. A permission request owned by the old activity
completes with a diagnostic instead of hanging; the host can retry after the replacement activity
is resumed. When initialization occurs inside the first activity's OnCreate, call
ObserveHostActivity only if a service must use that not-yet-resumed Activity immediately.
Animation surfaces must be marked active only from their normal start/resume lifecycle. Subsequent
Activity transitions are automatic.
An optional ActivityProvider exists for hosts with their own lifecycle integration. It is invoked
on demand; the delegate must not keep destroyed activities alive.
The backend registers AndroidPlatformAnimationSettings through the shared backend service
registry. When an application context exists, it reads
Settings.Global.ANIMATOR_DURATION_SCALE. Its exact scalar is combined with application policy on
the UI dispatcher; zero requests reduced motion and immediate, deterministic targets.
The provider refreshes during startup, on foreground entry, and when application code calls
AnimationScheduler.RefreshPlatformPolicy(). A main-thread ContentObserver is registered only
while the application is foregrounded and the scheduler is subscribed; it uses the application
resolver and unregisters on background or disposal. A missing context, failed read, or observer
failure uses compatibility defaults or refresh fallback, records diagnostics, and does not fail
application startup.
The scheduler uses one demand-driven Choreographer callback while an attached and resumed Skia surface exists. It does not assume 60 Hz or maintain a timer per control. Detailed ownership, frame, lifecycle, cleanup, capability, and validation guidance is in Android animation runtime architecture.
AndroidMainThreadDispatcher uses Looper.MainLooper and Handler. It provides:
CheckAccess();- asynchronous
Post(Action); InvokeAsync(Action)andInvokeAsync<T>(Func<T>);- pre-execution cancellation;
- exception propagation through returned tasks;
- inline invocation on the main thread to avoid self-deadlock.
The dispatcher is registered in PlatformServiceRegistry and as WindowKit's externally owned
dispatcher implementation. Application.Run retains application lifetime after returning to the
native Looper. AndroidWindowKit.Current.Dispatcher remains available for platform services.
AndroidSkiaHostView is one SKCanvasView that owns Android activity/surface lifecycle, density
conversion, resize, multi-pointer tracking, hardware editing keys, IME connection, coalesced
invalidation, and disposal.
SkiaControlSurface belongs to the core framework and adapts a real Control tree to that canvas,
including framework layout, paint, hit testing, pointer capture, selection, and committed-text
routing. This low-level API remains available. The cross-platform sample now uses Application.Run
and Form: its canonical adapter renders through WindowBase and borrows the same surface input
router without creating a second control or focus root.
The view renders only after invalidation or resize. It does not run a permanent frame timer; the shared animation scheduler requests Choreographer callbacks only while animation work remains. Canvas and Android objects remain platform-owned; the adapter borrows the shared control root so activity recreation can detach and reattach without discarding application state.
Activity state and native view attachment are tracked independently. A detached or paused view does not render; one pending invalidation survives until it is attached and resumed. Pointer cancellation, pause, stop, detach, and disposal clear all tracked pointers and framework capture.
Every Android pointer ID has independent framework capture. A down transition targets the deepest
enabled control and supplies coordinates local to that control. A small move remains tap-eligible;
crossing the logical-pixel drag threshold cancels the child press. If the target has an
AutoScroll ancestor, that ancestor then updates its real horizontal/vertical scrollbar values,
so content position, scrollbar thumbs, clamping, and Scroll notifications stay synchronized.
Touch movement does not synthesize desktop hover. A valid tap raises exactly one Click, before
MouseUp, while release outside capture, scrolling, cancellation, detach, and lifecycle loss do
not click. This path is shared core behavior and contains no Android API dependency.
The input connection supplies surrounding text, UTF-16 selection, and composition to Android.
Commit/composition/finish, selection, deletion in UTF-16 or code points, Enter, Delete, Backspace,
and arrow keys route into the selected framework TextBox. Surrogate pairs and complete framework
text elements are preserved. No native EditText is used.
The low-level surface remains available alongside the newer Application/Form host. Independent desktop windows, native service dialogs, clipboard, drag-and-drop and custom cursor artwork remain outside this mobile window policy. See the capability matrix.
The backend uses the supported platform Activity.RequestPermissions API without adding AndroidX.
AndroidWindowActivity forwards results to the one central coordinator automatically. Custom
Activities, including the native foundation smoke host, forward them explicitly:
public override void OnRequestPermissionsResult(
int requestCode,
string[] permissions,
Permission[] grantResults)
{
if (!AndroidWindowKit.HandleRequestPermissionsResult(requestCode, permissions, grantResults))
base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
}One coordinator admits a single native interaction: picker, permission UI, share chooser or
SystemMessageBox AlertDialog. Competing requests report Busy; there is no waiting queue. For
permission/picker/chooser UI, cancellation or timeout finishes managed waiting but holds the slot
until the matching callback or host destruction. A native message can be dismissed directly,
so cancellation cleans it and releases its slot. MessageBoxForm remains framework-rendered.
See the native message contract
for owner, Back, icon adaptation and recreation semantics.
NotDeclared, NotSupported, Granted, and PermanentlyDenied are terminal results and never
continue to RequestPermissions; in particular, a missing manifest declaration is reported to the
shared caller without attempting to display a platform dialog.
samples/ModernFormsNext.Android.SmokeTest is a native technical host. It displays backend,
Activity, lifecycle, SDK, camera, microphone, and notification state. Camera and notifications are
declared; microphone is deliberately omitted to exercise NotDeclared. See its README for the
manual rotation, denial, settings, and manifest checklist.
The separate samples/ModernFormsNext.CrossPlatform.Sample is the real shared-control vertical
slice. It is not a replacement for the native foundation smoke test; each sample has a different
validation role.
- The source-tree Application/Form host supports one main Form and owned modal/popup surfaces; desktop window-management parity is not provided.
- No complete Android clipboard, notification delivery, camera/media capture, WebView, file picker, sharing, or drag-and-drop service exists yet.
- Android 14 selected-photo access is not represented as a partial grant. Prefer a system photo picker for user-selected images until a dedicated media-selection API is designed.
- Runtime permission dialogs require callback forwarding, supplied by
AndroidWindowActivityor explicitly by a custom Activity; this avoids an AndroidX dependency in this foundation. - Platform mapping, queue, density, lifecycle, invalidation, resize, Unicode input-state, and
disposal behavior run as
net10.0tests. Deployment remains an explicit device/emulator step through repository scripts.
See the central known-limitations index for issue mapping and validation priorities. Automated host tests and successful Android builds are not physical-device evidence.
NotDeclared: add the exact<uses-permission>reported by the diagnostic to the application manifest, rebuild, and inspect the merged manifest.Unknownwith “no active Activity”: wait forOnResume; do not request from a background service.PermanentlyDenied: explain why the feature needs access and offer an explicit action that callsOpenApplicationSettingsAsync.- Request never completes: verify
OnRequestPermissionsResultis forwarded and inspect the the stableModernFormsNextlogcat tag.