avalonia-docs icon indicating copy to clipboard operation
avalonia-docs copied to clipboard

Data Persistence sample code in documentation throws on Android?

Open JGKle opened this issue 2 years ago • 9 comments

Hi,

I've followed the Data Persistence example here: https://docs.avaloniaui.net/docs/concepts/reactiveui/data-persistence

However, the moment it calls new AutoSuspendHelper(ApplicationLifetime) on Android, it throws System.NotSupportedException: 'Don't know how to detect app exit event for Avalonia.Android.SingleViewLifetime.'

Is the sample code supposed to function on Android? If not, it would be good if there were a note in the documentation. If so, what might be causing this exception?

Note: the documentation also links to https://github.com/AvaloniaUI/Avalonia/wiki/Application-lifetimes, which not a valid link.

JGKle avatar Dec 24 '23 03:12 JGKle

Not all platforms/OS notifyus when the program exits. So it's by design. However you are right that this should be added to the docs here. If you have a free minute a PR would be welcome.

timunie avatar Dec 24 '23 13:12 timunie

Thanks for the reply.

So if the AutoSuspendHelper approach doesn't work, is there a suggested approach that works on Android? Or do we basically just need to manually write out the config each time any setting changes (in which case, there's really no sense in including the AutoSuspendHelper)?

JGKle avatar Dec 24 '23 15:12 JGKle

I got it working. If you use ReactiveUI.AutoSuspendHelper rather than Avalonia.ReactiveUI.AutoSuspendHelper (per https://www.reactiveui.net/docs/handbook/data-persistence.html), it works properly on Android. However, I couldn't get ReactiveUI.AutoSuspendHelper setup in the Desktop project due to the arg it needs, so:

  • In the Core project, I use Avalonia.ReactiveUI.AutoSuspendHelper inside a check if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime)
  • In the Android project, I use ReactiveUI.AutoSuspendHelper per their docs

JGKle avatar Dec 24 '23 21:12 JGKle

If you have a free minute a PR would be welcome.

I have made one.

https://github.com/AvaloniaUI/avalonia-docs/pull/298

thevortexcloud avatar Dec 25 '23 05:12 thevortexcloud

Trying to use the sample code per Avalonia's documentation also doesn't work on iOS - it yields Don't know how to detect app exit event for Avalonia.iOS.SingleViewLifetime

So like Android, I'm looking at using ReactiveUI instead: https://www.reactiveui.net/docs/handbook/data-persistence.html

However, it requires overriding FinishedLaunching / DidEnterBackground / OnActivated, which it seems like you can't do if you inherit AvaloniaAppDelegate<App>. So I'd thought about inheriting UIApplicationDelegate, and then just copy-pasting the code in AvaloniaAppDelegate<TApp> to make it work? But can't do that either, because AvaloniaAppDeleate is internal, so it uses some stuff I can't reference from my copy.

Not really sure how to get app persistence to work beyond Desktop - definitely doesn't work per the documentation :/

JGKle avatar Jan 24 '24 23:01 JGKle

Ok I think I worked around it like this:

        // Note: It only uses "unusedDummy" to verify that you override FinishedLaunching/OnActivated/DidEnterBackground,
        // which we can't do because they aren't virtual in Avalonia. So we're using the 3 Observers below instead.
        var unusedDummy = new UIApplicationDelegate();
        _autoSuspendHelper = new ReactiveUI.AutoSuspendHelper(unusedDummy);
        RxApp.SuspensionHost.CreateNewAppState = () => new AppState();
        RxApp.SuspensionHost.SetupDefaultSuspendResume(new AppStateDriver("."));    

        NSNotificationCenter.DefaultCenter.AddObserver(UIApplication.DidFinishLaunchingNotification, (n) =>
        {
            _autoSuspendHelper.FinishedLaunching(UIApplication.SharedApplication, n.UserInfo);
        });

        NSNotificationCenter.DefaultCenter.AddObserver(UIApplication.DidEnterBackgroundNotification, (n) =>
        {
            _autoSuspendHelper.DidEnterBackground(UIApplication.SharedApplication);
        });

        NSNotificationCenter.DefaultCenter.AddObserver(UIApplication.WillEnterForegroundNotification, (n) =>
        {
           _autoSuspendHelper.OnActivated(UIApplication.SharedApplication);
        });

JGKle avatar Jan 25 '24 02:01 JGKle

@metal450 Can you provide an example on how you were able to do this?

It works fine for desktop but I cant get Android working. It always starts up and cant find a state. I have been setting var state = RxApp.SuspensionHost.GetAppState<MainViewModel>(); with break points all over my code at this point and I can tell that on startup I am getting a "ISuspensionHost: Failed to restore app state from storage, creating from scratch - System.Collections.Generic.KeyNotFoundException: The given key 'appState' was not present in the cache." error.

However If I run that same call to get the state mid operation it succeeds and I can tell I am getting the data I created during that session. But that isnt helping when I need the state to persist between app launches/shutdowns. Its like it isnt saving on shutdown of the app.

Here is my app.axaml.cs `public partial class App : Application {

public override void Initialize()
{
    AvaloniaXamlLoader.Load(this);
}

public override void OnFrameworkInitializationCompleted()
{
    base.OnFrameworkInitializationCompleted();
    //build App

    if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
    {
        // Create the AutoSuspendHelper.
        var suspension = new Avalonia.ReactiveUI.AutoSuspendHelper(ApplicationLifetime);
        RxApp.SuspensionHost.CreateNewAppState = () => new MainViewModel();
        RxApp.SuspensionHost.SetupDefaultSuspendResume(new NewtonsoftJsonSuspensionDriver("appstate.json"));
        suspension.OnFrameworkInitializationCompleted();

        // Load the saved view model state.
        var state = RxApp.SuspensionHost.GetAppState<MainViewModel>();

        desktop.MainWindow = new MainWindow
        {
            DataContext = state
        };
        desktop.MainWindow.Show();
    }
    else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
    {
          // Load the saved view model state.
          var state = RxApp.SuspensionHost.GetAppState<MainViewModel>();
        
        singleViewPlatform.MainView = new MainView
        {
            DataContext = state
            //DataContext = new MainViewModel()
        };
    }
}

}`

AkavacheSuspensionDriver.cs ` public class AkavacheSuspensionDriver<TAppState> : ISuspensionDriver where TAppState : class { private const string AppStateKey = "appState";

    public AkavacheSuspensionDriver() => BlobCache.ApplicationName = "WM_Inventory";

    //public IObservable<Unit> InvalidateState() => BlobCache.UserAccount.InvalidateObject<TAppState>(AppStateKey);
    public IObservable<Unit> InvalidateState() => BlobCache.Secure.InvalidateObject<TAppState>(AppStateKey);
    public IObservable<object> LoadState() => BlobCache.UserAccount.GetObject<TAppState>(AppStateKey);

    public IObservable<Unit> SaveState(object state) => BlobCache.UserAccount.InsertObject(AppStateKey, (TAppState)state);


}`

I tried different BlobCache.User and BlobCahce.Secure with no better results.

baaron4 avatar Mar 14 '24 23:03 baaron4

Forgot to mention the setup under Androids MainActivity.cs

`[Activity( Label = "WM_Inventory.Android", Theme = "@style/MyTheme.NoActionBar", Icon = "@drawable/icon", MainLauncher = true, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)] public class MainActivity : AvaloniaMainActivity<App> {

// Initialize the suspension driver after AutoSuspendHelper. 
private ReactiveUI.AutoSuspendHelper autoSuspendHelper;

protected override void OnCreate(Bundle bundle)
{
    // Initialize the suspension driver after AutoSuspendHelper. 
    this.autoSuspendHelper = new ReactiveUI.AutoSuspendHelper(this.Application);
    RxApp.SuspensionHost.CreateNewAppState = () => new MainViewModel();
    RxApp.SuspensionHost.SetupDefaultSuspendResume(new AkavacheSuspensionDriver<MainViewModel>());
    base.OnCreate(bundle);
}
protected override AppBuilder CustomizeAppBuilder(AppBuilder builder)
{
    return base.CustomizeAppBuilder(builder)
        .WithInterFont()
        .UseReactiveUI();
    
}

}`

baaron4 avatar Mar 14 '24 23:03 baaron4

Don't really have time to go through all that, but just quickly, I believe this should be everything for Android:

MainActivity.cs (in the Android project)

private ReactiveUI.AutoSuspendHelper autoSuspendHelper;

protected override void OnCreate(Bundle savedInstanceState)
{
    this.autoSuspendHelper = new ReactiveUI.AutoSuspendHelper(this.Application);
    RxApp.SuspensionHost.CreateNewAppState = () => new AppState(); // Tell it how to create a new state
    RxApp.SuspensionHost.SetupDefaultSuspendResume(new AppStateDriver(FileSystem.AppDataDirectory)); // Tell it how to load & save the state

    base.OnCreate(savedInstanceState);
}

AppState.cs (in the common project)

[DataContract]
public class AppState
{
    [DataMember]
    public int ExampleProperty { get; set; }
   
   // ...other properties to serialize

AppStateDriver.cs (in the common project)

    public class AppStateDriver : ISuspensionDriver
    {
        private string _filename;

        public AppStateDriver(string path)
        {
            _filename = Path.Combine(path, "AppState.xml");
        }

        public IObservable<Unit> SaveState(object state)
        {
            using (XmlWriter writer = XmlWriter.Create(_filename, new XmlWriterSettings { Indent = true }))
            {
                DataContractSerializer ds = new DataContractSerializer(typeof(AppState));
                ds.WriteObject(writer, state);
            }
            return Observable.Return(Unit.Default);
        }

        public IObservable<object> LoadState()
        {
                using (FileStream reader = new FileStream(_filename, FileMode.Open, FileAccess.Read))
                {
                    DataContractSerializer ser = new DataContractSerializer(typeof(AppState));
                    var appState = (AppState)ser.ReadObject(reader);
                    return Observable.Return(appState);
                }
        }

        public IObservable<Unit> InvalidateState()
        {
            if (File.Exists(_filename))
                File.Delete(_filename);
            return Observable.Return(Unit.Default);
        }
    }

App.axaml.cs (in the common project)

public static AppState State { get; set; }

public override void OnFrameworkInitializationCompleted()
{
    if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime)
    {
        // Setup state persistence on Desktop mode only
        var suspension = new AutoSuspendHelper(ApplicationLifetime);
        RxApp.SuspensionHost.CreateNewAppState = () => new AppState();
        RxApp.SuspensionHost.SetupDefaultSuspendResume(new AppStateDriver("."));
        suspension.OnFrameworkInitializationCompleted();
    }

    // Load or create the state
    App.State = RxApp.SuspensionHost.GetAppState<AppState>();

   // remaining setup boilerplate
    }

JGKle avatar Mar 15 '24 00:03 JGKle