Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Data Persistence sample code in documentation throws on Android? #368

Open
metal450 opened this issue Dec 24, 2023 · 9 comments
Open

Data Persistence sample code in documentation throws on Android? #368

metal450 opened this issue Dec 24, 2023 · 9 comments

Comments

@metal450
Copy link

metal450 commented Dec 24, 2023

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.

@timunie
Copy link
Contributor

timunie commented Dec 24, 2023

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.

@metal450
Copy link
Author

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)?

@metal450
Copy link
Author

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

@thevortexcloud
Copy link
Contributor

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

I have made one.

#298

@metal450
Copy link
Author

metal450 commented Jan 24, 2024

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 :/

@metal450
Copy link
Author

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);
        });

@timunie timunie transferred this issue from AvaloniaUI/Avalonia Feb 20, 2024
@baaron4
Copy link

baaron4 commented Mar 14, 2024

@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(); 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 : 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
Copy link

baaron4 commented Mar 14, 2024

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
{

// 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();
    
}

}`

@metal450
Copy link
Author

metal450 commented Mar 15, 2024

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
    }

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

No branches or pull requests

4 participants