Kaynağa Gözat

Fix android and add top level info early (#15158)

* Add and implement ISingleTopLevelApplicationLifetime (internal API)

* Fix Android initialization order, so we can have AfterSetup with usable TopLevel in the callback

* Fix android and futher simplify its initialization

* Return removed public API, make it all work together, introduce AvaloniaActivity

* Adjust some comments and minor bug fixes

* Add CreateAppBuilder to iOS and Tizen as well for consistency

* Add AfterApplicationSetup private api, so our backends have a safe place to setup avalonia views.

* Keep number of breaking changes minimal
Max Katz 1 yıl önce
ebeveyn
işleme
cc52217c09

+ 170 - 0
src/Android/Avalonia.Android/AvaloniaActivity.cs

@@ -0,0 +1,170 @@
+#nullable enable
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.Versioning;
+using Android.App;
+using Android.Content;
+using Android.Content.PM;
+using Android.OS;
+using Android.Runtime;
+using Android.Views;
+using AndroidX.AppCompat.App;
+using Avalonia.Controls.ApplicationLifetimes;
+
+namespace Avalonia.Android;
+
+/// <summary>
+/// Common implementation of android activity that is integrated with Avalonia views.
+/// If you need a base class for main activity of Avalonia app, see <see cref="AvaloniaMainActivity"/> or <see cref="AvaloniaMainActivity{TApp}"/>.  
+/// </summary>
+public class AvaloniaActivity : AppCompatActivity, IAvaloniaActivity
+{
+    private EventHandler<ActivatedEventArgs>? _onActivated, _onDeactivated;
+    private GlobalLayoutListener? _listener;
+    private object? _content;
+    internal AvaloniaView? _view;
+
+    public Action<int, Result, Intent?>? ActivityResult { get; set; }
+    public Action<int, string[], Permission[]>? RequestPermissionsResult { get; set; }
+
+    public event EventHandler<AndroidBackRequestedEventArgs>? BackRequested;
+
+    public object? Content
+    {
+        get => _content;
+        set
+        {
+            if (_content != value)
+            {
+                _content = value;
+                if (_view is not null)
+                {
+                    _view.Content = _content;
+                }
+            }
+        }
+    }
+
+    event EventHandler<ActivatedEventArgs>? IAvaloniaActivity.Activated
+    {
+        add { _onActivated += value; }
+        remove { _onActivated -= value; }
+    }
+
+    event EventHandler<ActivatedEventArgs>? IAvaloniaActivity.Deactivated
+    {
+        add { _onDeactivated += value; }
+        remove { _onDeactivated -= value; }
+    }
+
+    [ObsoletedOSPlatform("android33.0")]
+    public override void OnBackPressed()
+    {
+        var eventArgs = new AndroidBackRequestedEventArgs();
+
+        BackRequested?.Invoke(this, eventArgs);
+
+        if (!eventArgs.Handled)
+        {
+            base.OnBackPressed();
+        }
+    }
+
+    protected override void OnCreate(Bundle? savedInstanceState)
+    {
+        InitializeAvaloniaView(_content);
+
+        base.OnCreate(savedInstanceState);
+
+        SetContentView(_view);
+
+        _listener = new GlobalLayoutListener(_view);
+
+        _view.ViewTreeObserver?.AddOnGlobalLayoutListener(_listener);
+            
+        if (Intent?.Data is {} androidUri
+            && androidUri.IsAbsolute
+            && Uri.TryCreate(androidUri.ToString(), UriKind.Absolute, out var protocolUri))
+        {
+            _onActivated?.Invoke(this, new ProtocolActivatedEventArgs(ActivationKind.OpenUri, protocolUri));
+        }
+    }
+
+    protected override void OnStop()
+    {
+        _onDeactivated?.Invoke(this, new ActivatedEventArgs(ActivationKind.Background));
+        base.OnStop();
+    }
+
+    protected override void OnStart()
+    {
+        _onActivated?.Invoke(this, new ActivatedEventArgs(ActivationKind.Background));
+        base.OnStart();
+    }
+
+    protected override void OnResume()
+    {
+        base.OnResume();
+
+        // Android only respects LayoutInDisplayCutoutMode value if it has been set once before window becomes visible.
+        if (OperatingSystem.IsAndroidVersionAtLeast(28) && Window is { Attributes: { } attributes })
+        {
+            attributes.LayoutInDisplayCutoutMode = LayoutInDisplayCutoutMode.ShortEdges;
+        }
+    }
+
+    protected override void OnDestroy()
+    {
+        if (_view is not null)
+        {
+            _view.Content = null;
+            _view.ViewTreeObserver?.RemoveOnGlobalLayoutListener(_listener);
+            _view.Dispose();
+            _view = null;
+        }
+
+        base.OnDestroy();
+    }
+        
+    protected override void OnActivityResult(int requestCode, [GeneratedEnum] Result resultCode, Intent? data)
+    {
+        base.OnActivityResult(requestCode, resultCode, data);
+
+        ActivityResult?.Invoke(requestCode, resultCode, data);
+    }
+
+    [SupportedOSPlatform("android23.0")]
+    public override void OnRequestPermissionsResult(int requestCode, string[] permissions, Permission[] grantResults)
+    {
+        base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
+
+        RequestPermissionsResult?.Invoke(requestCode, permissions, grantResults);
+    }
+
+    [MemberNotNull(nameof(_view))]
+    private protected virtual void InitializeAvaloniaView(object? initialContent)
+    {
+        if (Avalonia.Application.Current is null)
+        {
+            throw new InvalidOperationException(
+                "Avalonia Application was not initialized. Make sure you have created AvaloniaMainActivity.");
+        }
+
+        _view = new AvaloniaView(this) { Content = initialContent };
+    }
+
+    private class GlobalLayoutListener : Java.Lang.Object, ViewTreeObserver.IOnGlobalLayoutListener
+    {
+        private readonly AvaloniaView _view;
+
+        public GlobalLayoutListener(AvaloniaView view)
+        {
+            _view = view;
+        }
+
+        public void OnGlobalLayout()
+        {
+            _view.TopLevelImpl?.Resize(_view.TopLevelImpl.ClientSize);
+        }
+    }
+}

+ 7 - 58
src/Android/Avalonia.Android/AvaloniaMainActivity.App.cs

@@ -1,66 +1,15 @@
 #nullable enable
 
+using Android.OS;
+using Android.Views;
 using Avalonia.Android.Platform;
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Platform;
 
-namespace Avalonia.Android
-{
-    partial class AvaloniaMainActivity<TApp> where TApp : Application, new()
-    {
-        protected virtual AppBuilder CustomizeAppBuilder(AppBuilder builder) => builder.UseAndroid();
-
-        private static AppBuilder? s_appBuilder;
-        internal static object? ViewContent;
-
-        public object? Content
-        {
-            get
-            {
-                return ViewContent;
-            }
-            set
-            {
-                ViewContent = value;
-                if (View != null)
-                    View.Content = value;
-            }
-        }
-
-        protected AppBuilder CreateAppBuilder()
-        {
-            var builder = AppBuilder.Configure<TApp>();
-
-            return CustomizeAppBuilder(builder);
-        }
-
-        private void InitializeApp()
-        {
-            if (s_appBuilder == null)
-            {
-                var builder = CreateAppBuilder();
+namespace Avalonia.Android;
 
-                builder.SetupWithLifetime(new SingleViewLifetime());
-
-                s_appBuilder = builder;
-            }
-
-            if (Avalonia.Application.Current?.TryGetFeature<IActivatableLifetime>()
-                is AndroidActivatableLifetime activatableLifetime)
-            {
-                activatableLifetime.Activity = this;
-            }            
-
-            View = new AvaloniaView(this);
-            if (ViewContent != null)
-            {
-                View.Content = ViewContent;
-            }
-
-            if (Avalonia.Application.Current?.ApplicationLifetime is SingleViewLifetime lifetime)
-            {
-                lifetime.View = View;
-            }
-        }
-    }
+public class AvaloniaMainActivity<TApp> : AvaloniaMainActivity
+    where TApp : Application, new()
+{
+    protected override AppBuilder CreateAppBuilder() => AppBuilder.Configure<TApp>();
 }

+ 48 - 126
src/Android/Avalonia.Android/AvaloniaMainActivity.cs

@@ -1,141 +1,63 @@
+#nullable enable
+
 using System;
-using System.Runtime.Versioning;
-using Android.App;
-using Android.Content;
-using Android.Content.PM;
+using System.Diagnostics.CodeAnalysis;
 using Android.OS;
-using Android.Runtime;
-using Android.Views;
-using AndroidX.AppCompat.App;
+using Avalonia.Android.Platform;
 using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Platform;
 
-namespace Avalonia.Android
-{
-    public class AvaloniaMainActivity : AppCompatActivity, IAvaloniaActivity
-    {
-        private EventHandler<ActivatedEventArgs> _onActivated, _onDeactivated;
-        
-        public Action<int, Result, Intent> ActivityResult { get; set; }
-        public Action<int, string[], Permission[]> RequestPermissionsResult { get; set; }
+namespace Avalonia.Android;
 
-        public event EventHandler<AndroidBackRequestedEventArgs> BackRequested;
-        event EventHandler<ActivatedEventArgs> IAvaloniaActivity.Activated
-        {
-            add { _onActivated += value; }
-            remove { _onActivated -= value; }
-        }
-
-        event EventHandler<ActivatedEventArgs> IAvaloniaActivity.Deactivated
-        {
-            add { _onDeactivated += value; }
-            remove { _onDeactivated -= value; }
-        }
-
-        [ObsoletedOSPlatform("android33.0")]
-        public override void OnBackPressed()
-        {
-            var eventArgs = new AndroidBackRequestedEventArgs();
-
-            BackRequested?.Invoke(this, eventArgs);
-
-            if (!eventArgs.Handled)
-            {
-                base.OnBackPressed();
-            }
-        }
-
-        protected override void OnCreate(Bundle savedInstanceState)
-        {
-            base.OnCreate(savedInstanceState);
-            
-            if (Intent?.Data is {} androidUri
-                && androidUri.IsAbsolute
-                && Uri.TryCreate(androidUri.ToString(), UriKind.Absolute, out var protocolUri))
-            {
-                _onActivated?.Invoke(this, new ProtocolActivatedEventArgs(ActivationKind.OpenUri, protocolUri));
-            }
-        }
-
-        protected override void OnStop()
-        {
-            _onDeactivated?.Invoke(this, new ActivatedEventArgs(ActivationKind.Background));
-            base.OnStop();
-        }
-
-        protected override void OnStart()
-        {
-            _onActivated?.Invoke(this, new ActivatedEventArgs(ActivationKind.Background));
-            base.OnStart();
-        }
+public class AvaloniaMainActivity : AvaloniaActivity
+{
+    private protected static SingleViewLifetime? Lifetime;
 
-        protected override void OnActivityResult(int requestCode, [GeneratedEnum] Result resultCode, Intent data)
+    public override void OnCreate(Bundle? savedInstanceState, PersistableBundle? persistentState)
+    {
+        // Global IActivatableLifetime expects a main activity, so we need to replace it on each OnCreate.
+        if (Avalonia.Application.Current?.TryGetFeature<IActivatableLifetime>()
+            is AndroidActivatableLifetime activatableLifetime)
         {
-            base.OnActivityResult(requestCode, resultCode, data);
-
-            ActivityResult?.Invoke(requestCode, resultCode, data);
+            activatableLifetime.Activity = this;
         }
 
-        [SupportedOSPlatform("android23.0")]
-        public override void OnRequestPermissionsResult(int requestCode, string[] permissions, Permission[] grantResults)
-        {
-            base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
-
-            RequestPermissionsResult?.Invoke(requestCode, permissions, grantResults);
-        }
+        base.OnCreate(savedInstanceState, persistentState);
     }
 
-    public abstract partial class AvaloniaMainActivity<TApp> : AvaloniaMainActivity  where TApp : Application, new()
+    private protected override void InitializeAvaloniaView(object? initialContent)
     {
-        internal AvaloniaView View { get; set; }
-
-        private GlobalLayoutListener _listener;
-
-        protected override void OnCreate(Bundle savedInstanceState)
-        {
-            InitializeApp();
-
-            base.OnCreate(savedInstanceState);
-
-            SetContentView(View);
-
-            _listener = new GlobalLayoutListener(View);
-
-            View.ViewTreeObserver?.AddOnGlobalLayoutListener(_listener);
-        }
-
-        protected override void OnResume()
-        {
-            base.OnResume();
-
-            // Android only respects LayoutInDisplayCutoutMode value if it has been set once before window becomes visible.
-            if (OperatingSystem.IsAndroidVersionAtLeast(28) && Window is { Attributes: { } attributes })
-            {
-                attributes.LayoutInDisplayCutoutMode = LayoutInDisplayCutoutMode.ShortEdges;
-            }
-        }
-
-        protected override void OnDestroy()
-        {
-            View.Content = null;
-
-            View.ViewTreeObserver?.RemoveOnGlobalLayoutListener(_listener);
-
-            base.OnDestroy();
-        }
-
-        private class GlobalLayoutListener : Java.Lang.Object, ViewTreeObserver.IOnGlobalLayoutListener
-        {
-            private readonly AvaloniaView _view;
-
-            public GlobalLayoutListener(AvaloniaView view)
-            {
-                _view = view;
-            }
-
-            public void OnGlobalLayout()
-            {
-                _view.TopLevelImpl?.Resize(_view.TopLevelImpl.ClientSize);
-            }
+        // Android can run OnCreate + InitializeAvaloniaView multiple times per process lifetime.
+        // On each call we need to create new AvaloniaView, but we can't recreate Avalonia nor Avalonia controls.
+        // So, if lifetime was already created previously - recreate AvaloniaView.
+        // If not, initialize Avalonia, and create AvaloniaView inside of AfterSetup callback.
+        // We need this AfterSetup callback to match iOS/Browser behavior and ensure that view/toplevel is available in custom AfterSetup calls.
+        if (Lifetime is not null)
+        {
+            Lifetime.Activity = this;
+            _view = new AvaloniaView(this) { Content = initialContent };
+        }
+        else
+        {
+            var builder = CreateAppBuilder();
+            builder = CustomizeAppBuilder(builder);
+
+            Lifetime = new SingleViewLifetime();
+            Lifetime.Activity = this;
+ 
+            builder
+                .AfterApplicationSetup(_ =>
+                {
+                    _view = new AvaloniaView(this) { Content = initialContent };
+                })
+                .SetupWithLifetime(Lifetime);
+
+            // AfterPlatformServicesSetup should always be called. If it wasn't, we have an unusual problem.
+            if (_view is null)
+                throw new InvalidOperationException("Unknown error: AvaloniaView initialization has failed.");
         }
     }
+
+    protected virtual AppBuilder CreateAppBuilder() => AppBuilder.Configure<Application>().UseAndroid();
+    protected virtual AppBuilder CustomizeAppBuilder(AppBuilder builder) => builder;
 }

+ 1 - 3
src/Android/Avalonia.Android/AvaloniaView.cs

@@ -35,6 +35,7 @@ namespace Avalonia.Android
         }
 
         internal TopLevelImpl TopLevelImpl => _view;
+        internal TopLevel TopLevel => _root;
 
         public object Content
         {
@@ -121,9 +122,6 @@ namespace Avalonia.Android
                 MaxClientSize = size;
                 base.OnResized(size);
             }
-
-            public WindowState WindowState { get; set; }
-            public IDisposable ShowDialog() => null;
         }
     }
 }

+ 5 - 3
src/Android/Avalonia.Android/IAvaloniaActivity.cs

@@ -1,10 +1,12 @@
-using System;
+#nullable enable
+using System;
 using Avalonia.Controls.ApplicationLifetimes;
 
 namespace Avalonia.Android;
 
 public interface IAvaloniaActivity : IActivityResultHandler, IActivityNavigationService
 {
-    event EventHandler<ActivatedEventArgs> Activated;
-    event EventHandler<ActivatedEventArgs> Deactivated;
+    object? Content { get; set; }
+    event EventHandler<ActivatedEventArgs>? Activated;
+    event EventHandler<ActivatedEventArgs>? Deactivated;
 }

+ 35 - 14
src/Android/Avalonia.Android/SingleViewLifetime.cs

@@ -1,26 +1,47 @@
-using Avalonia.Controls;
+#nullable enable
+using System.Diagnostics.CodeAnalysis;
+using Avalonia.Controls;
 using Avalonia.Controls.ApplicationLifetimes;
 
-namespace Avalonia.Android
+namespace Avalonia.Android;
+
+internal class SingleViewLifetime : ISingleViewApplicationLifetime, ISingleTopLevelApplicationLifetime
 {
-    internal class SingleViewLifetime : ISingleViewApplicationLifetime
-    {
-        private AvaloniaView _view;
+    private Control? _mainView;
+    private AvaloniaMainActivity? _activity;
         
-        public AvaloniaView View
+    /// <summary>
+    /// Since Main Activity can be swapped, we should adjust litetime as well.  
+    /// </summary>
+    public AvaloniaMainActivity Activity
+    {
+        [return: MaybeNull] get => _activity!;
+        internal set
+        {
+            if (_activity != null)
+            {
+                _activity.Content = null;
+            }
+            _activity = value;
+            _activity.Content = _mainView;
+        }
+    }
+
+    public Control? MainView
+    {
+        get => _mainView;
+        set
         {
-            get => _view; internal set
+            if (_mainView != value)
             {
-                if (_view != null)
+                _mainView = value;
+                if (_activity != null)
                 {
-                    _view.Content = null;
-                    _view.Dispose();
+                    _activity.Content = _mainView;
                 }
-                _view = value;
-                _view.Content = MainView;
             }
         }
-
-        public Control MainView { get; set; }
     }
+
+    public TopLevel? TopLevel => _activity?._view?.TopLevel;
 }

+ 14 - 3
src/Avalonia.Controls/AppBuilder.cs

@@ -69,10 +69,14 @@ namespace Avalonia
         public string? RenderingSubsystemName { get; private set; }
 
         /// <summary>
-        /// Gets or sets a method to call after the <see cref="Application"/> is setup.
+        /// Gets a method to call after the <see cref="Application"/> is setup.
         /// </summary>
         public Action<AppBuilder> AfterSetupCallback { get; private set; } = builder => { };
 
+        /// <summary>
+        /// Callbacks that are commonly used by backends to initialize avalonia views.
+        /// </summary>
+        private Action<AppBuilder> AfterApplicationSetupCallback { get; set; } = builder => { };
 
         public Action<AppBuilder> AfterPlatformServicesSetupCallback { get; private set; } = builder => { };
         
@@ -159,8 +163,14 @@ namespace Avalonia
             AfterSetupCallback = (Action<AppBuilder>)Delegate.Combine(AfterSetupCallback, callback);
             return Self;
         }
-        
-        
+
+        [PrivateApi]
+        public AppBuilder AfterApplicationSetup(Action<AppBuilder> callback)
+        {
+            AfterApplicationSetupCallback = (Action<AppBuilder>)Delegate.Combine(AfterPlatformServicesSetupCallback, callback);
+            return Self;
+        }
+
         public AppBuilder AfterPlatformServicesSetup(Action<AppBuilder> callback)
         {
             AfterPlatformServicesSetupCallback = (Action<AppBuilder>)Delegate.Combine(AfterPlatformServicesSetupCallback, callback);
@@ -328,6 +338,7 @@ namespace Avalonia
             AvaloniaLocator.CurrentMutable.BindToSelf(Instance);
             Instance.RegisterServices();
             Instance.Initialize();
+            AfterApplicationSetupCallback?.Invoke(Self);
             AfterSetupCallback?.Invoke(Self);
             Instance.OnFrameworkInitializationCompleted();
         }

+ 15 - 0
src/Avalonia.Controls/ApplicationLifetimes/ISingleTopLevelApplicationLifetime.cs

@@ -0,0 +1,15 @@
+using System.ComponentModel;
+using Avalonia.Metadata;
+
+namespace Avalonia.Controls.ApplicationLifetimes;
+
+/// <summary>
+/// Used in our internal projects. Until we figure out way to add this information to the public API. 
+/// </summary>
+[NotClientImplementable]
+[PrivateApi]
+[EditorBrowsable(EditorBrowsableState.Never)]
+public interface ISingleTopLevelApplicationLifetime : IApplicationLifetime
+{
+    TopLevel? TopLevel { get; }
+}

+ 11 - 9
src/Browser/Avalonia.Browser/AvaloniaView.cs

@@ -140,6 +140,16 @@ namespace Avalonia.Browser
             InputHelper.FocusElement(_containerElement);
         }
 
+        public Control? Content
+        {
+            get => (Control)_topLevel.Content!;
+            set => _topLevel.Content = value;
+        }
+
+        public bool IsComposing { get; private set; }
+
+        internal TopLevel TopLevel => _topLevel;
+
         private static RawPointerPoint ExtractRawPointerFromJSArgs(JSObject args)
         {
             var point = new RawPointerPoint
@@ -429,15 +439,7 @@ namespace Avalonia.Browser
             Dispatcher.UIThread.RunJobs(DispatcherPriority.UiThreadRender);
             ManualTriggerRenderTimer.Instance.RaiseTick();
         }
-
-        public Control? Content
-        {
-            get => (Control)_topLevel.Content!;
-            set => _topLevel.Content = value;
-        }
-
-        public bool IsComposing { get; private set; }
-
+        
         internal INativeControlHostImpl GetNativeControlHostImpl()
         {
             return new BrowserNativeControlHost(_nativeControlsContainer);

+ 1 - 1
src/Browser/Avalonia.Browser/BrowserAppBuilder.cs

@@ -55,7 +55,7 @@ public static class BrowserAppBuilder
 
         var lifetime = new BrowserSingleViewLifetime();
         builder
-            .AfterSetup(_ =>
+            .AfterApplicationSetup(_ =>
             {
                 lifetime.View = new AvaloniaView(mainDivId);
             })

+ 3 - 1
src/Browser/Avalonia.Browser/BrowserSingleViewLifetime.cs

@@ -6,7 +6,7 @@ using Avalonia.Browser;
 
 namespace Avalonia;
 
-internal class BrowserSingleViewLifetime : ISingleViewApplicationLifetime
+internal class BrowserSingleViewLifetime : ISingleViewApplicationLifetime, ISingleTopLevelApplicationLifetime
 {
     public AvaloniaView? View;
 
@@ -32,4 +32,6 @@ internal class BrowserSingleViewLifetime : ISingleViewApplicationLifetime
             throw new InvalidOperationException("Browser lifetime was not initialized. Make sure AppBuilder.StartBrowserApp was called.");
         }
     }
+
+    public TopLevel? TopLevel => View?.TopLevel;
 }

+ 1 - 1
src/Headless/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs

@@ -26,7 +26,7 @@ namespace Avalonia
                     UseHeadlessDrawing = false,
                     FrameBufferFormat = PixelFormat.Bgra8888
                 })
-                .AfterSetup(_ =>
+                .AfterApplicationSetup(_ =>
                 {
                     var lt = ((IClassicDesktopStyleApplicationLifetime)builder.Instance!.ApplicationLifetime!);
                     lt.Startup += async delegate

+ 35 - 20
src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
 using System.IO;
 using System.Threading;
 using Avalonia;
@@ -88,7 +89,7 @@ namespace Avalonia.LinuxFramebuffer
         }
     }
 
-    class LinuxFramebufferLifetime : IControlledApplicationLifetime, ISingleViewApplicationLifetime
+    class LinuxFramebufferLifetime : IControlledApplicationLifetime, ISingleViewApplicationLifetime, ISingleTopLevelApplicationLifetime
     {
         private readonly IOutputBackend _fb;
         private readonly IInputBackend? _inputBackend;
@@ -114,25 +115,7 @@ namespace Avalonia.LinuxFramebuffer
             {
                 if (_topLevel == null)
                 {
-                    var inputBackend = _inputBackend;
-                    if (inputBackend == null)
-                    {
-                        if (Environment.GetEnvironmentVariable("AVALONIA_USE_EVDEV") == "1")
-                            inputBackend = EvDevBackend.CreateFromEnvironment();
-                        else
-                            inputBackend = new LibInputBackend();
-                    }
-
-                    var tl = new EmbeddableControlRoot(new FramebufferToplevelImpl(_fb, inputBackend));
-                    tl.Prepare();
-                    tl.StartRendering();
-                    _topLevel = tl;
-                    
-
-                    if (_topLevel is IFocusScope scope && _topLevel.FocusManager is FocusManager focusManager)
-                    {
-                        focusManager.SetFocusScope(scope);
-                    }
+                    EnsureTopLevel();
                 }
 
                 _topLevel.Content = value;
@@ -156,6 +139,38 @@ namespace Avalonia.LinuxFramebuffer
             ExitCode = e.ApplicationExitCode;
             _cts.Cancel();
         }
+
+        public TopLevel? TopLevel
+        {
+            get
+            {
+                EnsureTopLevel();
+                return _topLevel;
+            }
+        }
+
+        [MemberNotNull(nameof(_topLevel))]
+        private void EnsureTopLevel()
+        {
+            var inputBackend = _inputBackend;
+            if (inputBackend == null)
+            {
+                if (Environment.GetEnvironmentVariable("AVALONIA_USE_EVDEV") == "1")
+                    inputBackend = EvDevBackend.CreateFromEnvironment();
+                else
+                    inputBackend = new LibInputBackend();
+            }
+
+            var tl = new EmbeddableControlRoot(new FramebufferToplevelImpl(_fb, inputBackend));
+            tl.Prepare();
+            tl.StartRendering();
+            _topLevel = tl;
+
+            if (_topLevel is IFocusScope scope && _topLevel.FocusManager is FocusManager focusManager)
+            {
+                focusManager.SetFocusScope(scope);
+            }
+        }
     }
 }
 

+ 1 - 1
src/Tizen/Avalonia.Tizen/NuiAvaloniaView.cs

@@ -36,7 +36,7 @@ public class NuiAvaloniaView : GLView, ITizenView, ITextInputMethodImpl
         set => _inputRoot = value;
     }
 
-    private TopLevel TopLevel
+    internal TopLevel TopLevel
         => _topLevel ?? throw new InvalidOperationException($"{nameof(NuiAvaloniaView)} hasn't been initialized");
 
     internal TopLevelImpl TopLevelImpl

+ 7 - 5
src/Tizen/Avalonia.Tizen/NuiTizenApplication.cs

@@ -13,7 +13,7 @@ public class NuiTizenApplication<TApp> : NUIApplication
 
     private SingleViewLifetime? _lifetime;
 
-    private class SingleViewLifetime : ISingleViewApplicationLifetime
+    private class SingleViewLifetime : ISingleViewApplicationLifetime, ISingleTopLevelApplicationLifetime
     {
         public NuiAvaloniaView View { get; }
 
@@ -27,8 +27,11 @@ public class NuiTizenApplication<TApp> : NUIApplication
             get => View.Content;
             set => View.Content = value;
         }
+
+        public TopLevel? TopLevel => View.TopLevel;
     }
 
+    protected virtual AppBuilder CreateAppBuilder() => AppBuilder.Configure<Application>().UseTizen();
     protected virtual AppBuilder CustomizeAppBuilder(AppBuilder builder) => builder;
 
     protected override void OnCreate()
@@ -55,13 +58,12 @@ public class NuiTizenApplication<TApp> : NUIApplication
         SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
 
         Logger.TryGet(LogEventLevel.Debug, LogKey)?.Log(null, "App builder");
-        var builder = AppBuilder.Configure<TApp>().UseTizen();
+        var builder = CreateAppBuilder();
 
         TizenThreadingInterface.MainloopContext.Post(_ =>
         {
-            CustomizeAppBuilder(builder);
-
-            builder.AfterSetup(_ => _lifetime!.View.Initialise());
+            builder = CustomizeAppBuilder(builder);
+            builder.AfterApplicationSetup(_ => _lifetime!.View.Initialise());
 
             Logger.TryGet(LogEventLevel.Debug, LogKey)?.Log(null, "Setup lifetime");
             builder.SetupWithLifetime(_lifetime!);

+ 6 - 6
src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs

@@ -33,20 +33,22 @@ namespace Avalonia.iOS
             add { _onDeactivated += value; }
             remove { _onDeactivated -= value; }
         }
-        
+
+        protected virtual AppBuilder CreateAppBuilder() => AppBuilder.Configure<Application>().UseiOS();
         protected virtual AppBuilder CustomizeAppBuilder(AppBuilder builder) => builder;
-        
+
         [Export("window")]
         public UIWindow? Window { get; set; }
 
         [Export("application:didFinishLaunchingWithOptions:")]
         public bool FinishedLaunching(UIApplication application, NSDictionary launchOptions)
         {
-            var builder = AppBuilder.Configure<TApp>().UseiOS(this);
+            var builder = CreateAppBuilder();
+            builder = CustomizeAppBuilder(builder);
 
             var lifetime = new SingleViewLifetime();
 
-            builder.AfterSetup(_ =>
+            builder.AfterApplicationSetup(_ =>
             {
                 Window = new UIWindow();
 
@@ -60,8 +62,6 @@ namespace Avalonia.iOS
                 view.InitWithController(controller);
             });
 
-            CustomizeAppBuilder(builder);
-
             builder.SetupWithLifetime(lifetime);
 
             Window!.MakeKeyAndVisible();

+ 1 - 0
src/iOS/Avalonia.iOS/AvaloniaView.cs

@@ -29,6 +29,7 @@ namespace Avalonia.iOS
     {
         internal IInputRoot InputRoot
             => _inputRoot ?? throw new InvalidOperationException($"{nameof(IWindowImpl.SetInputRoot)} must have been called");
+        internal TopLevel TopLevel => _topLevel;
 
         private readonly TopLevelImpl _topLevelImpl;
         private readonly EmbeddableControlRoot _topLevel;

+ 34 - 5
src/iOS/Avalonia.iOS/SingleViewLifetime.cs

@@ -1,16 +1,45 @@
 using System;
+using System.Diagnostics.CodeAnalysis;
 using Avalonia.Controls;
 using Avalonia.Controls.ApplicationLifetimes;
 
 namespace Avalonia.iOS;
 
-internal class SingleViewLifetime : ISingleViewApplicationLifetime
-{       
-    public AvaloniaView? View;
+internal class SingleViewLifetime : ISingleViewApplicationLifetime, ISingleTopLevelApplicationLifetime
+{
+    private Control? _mainView;
+    private AvaloniaView? _view;
+
+    public AvaloniaView View
+    {
+        [return: MaybeNull] get => _view!;
+        internal set
+        {
+            if (_view != null)
+            {
+                _view.Content = null;
+                _view.Dispose();
+            }
+            _view = value;
+            _view.Content = _mainView;
+        }
+    }
 
     public Control? MainView
     {
-        get => View!.Content;
-        set => View!.Content = value;
+        get => _mainView;
+        set
+        {
+            if (_mainView != value)
+            {
+                _mainView = value;
+                if (_view != null)
+                {
+                    _view.Content = _mainView;
+                }
+            }
+        }
     }
+
+    public TopLevel? TopLevel => View?.TopLevel;
 }