Correct Xamarin Forms Shell initializing with async data loading

Viewed 523

I am working on a rather complex xamarin forms app for a while now and now I came over a problem related to the async initialization of the app. Some context before I come to the problem.

  1. I am using the current version of Xamarin Forms, my main target is xamarin android
  2. I am building on top of the xamarin forms Shell pattern
  3. I am using a sqlite database for the main part of the apps configuration
  4. The app layout is mainly dependent on the configuration stored in the sqlite database (what I call a data driven app)
  5. Therefore I am building the whole shell page layout not in xaml but in code behind of the AppShell.xaml.cs - in the constructure to be concrete (first weird point in my personal opinion).

So first remarkable point here - I need the fully loaded configuration BEFORE the AppShell is initiated.

At first a started with a mockup data service where the whole config was inside a large mocked object so it ran fully synchronized - everything perfect!

After switching over to the sqlite implementation where I am using async methods the problem started to show up because I try to load the configuration in the constructor (second weird point because such a task should never be part of a constructor in my point of view)

So my constructor of AppShellView.xaml looks like this.

    public App()
    {
        InitializeComponent();

        InitApp().SafeFireAndForget(onException: ErrorHandler);

        MainPage = new AppShellView();
    }

the InitApp Method where the database is queried looks like this.

    private async Task InitApp()
    {
        _settingsService = ViewModelLocator.Resolve<ISettingsService>();

        ViewModelLocator.UpdateDependencies(_settingsService.UseDemoMode);

        _dataService = ViewModelLocator.Resolve<IDataService>();

        ActiveConfiguration = await _dataService.GetActiveConfigurationAsync().ConfigureAwait(false);

        //MainPage = new AppShellView(); this was added to reload the MainPage after the config was fully loaded
    }

what obviously happens is that the MainPage = new AppShellView() in the App's constructor is called before the config is loaded - because its called in a fireandforget way. Therefore the following construction of the AppShellView does not work as intendet because the pages cannot be added because of the missing config. I hope this is clear so far.

To overcome this issue I added another MainPage = new AppShelLView() as last step of the InitApp method.

The constructor of the AppShellView looks like this:

    public AppShellView()
    {
        InitializeComponent();

        BuildMainNavigation();

        BuildSettingsNavigation();

        InitRoutes();

        this.CurrentItem.CurrentItem = startSection;
    }

This seems to work on thirst sight but triggers side effects when the app is started from AppLink Shortcuts and not to forget I ended up initializing the same object again and again which causes performance issues and simply smells...

So I hope I made my point clear so far - I know that I have a view issues in my implementation but I simply don't see the correct way of doing it.

What I would naturally would try to do is doing the configuration loading in the android init part behind the splash screen which I implemented like this: https://docs.microsoft.com/en-us/xamarin/android/user-interface/splash-screen but I also found no way to pass the data from the android Activity to the App class.

To sum up and ask a concrete question:

  1. What is the best practice way of loading async configuration BEFORE initializing the Shell

@Jason: thanks for your fast response. So this idea seems promising, sounds like another Splash Screen but thats ok.

I tried that - for the initial setup that works fine.

App's constructor is simplified to this:

    public App()
    {
        InitializeComponent();

        MainPage = new Splashscreen();
    }

The OnStart looks like this now:

protected async override void OnStart()
    {
        base.OnStart();

        if (!IsInitiated)
        {
            _settingsService = ViewModelLocator.Resolve<ISettingsService>();

            ViewModelLocator.UpdateDependencies(_settingsService.UseDemoMode);

            _dataService = ViewModelLocator.Resolve<IDataService>();

            ActiveConfiguration = await _dataService.GetActiveConfigurationAsync().ConfigureAwait(true);

            MainPage = new AppShellView();

            App.Current.UserAppTheme = _settingsService.OSAppTheme;
        }
        else
        {
            App.Current.UserAppTheme = _settingsService.OSAppTheme;
        }

        base.OnResume();
    }

But I have app link functionality, where OnAppLinkRequestReceived is called.

    protected override async void OnAppLinkRequestReceived(Uri uri)
    {
        if(MainPage is Splashscreen)
        {
            MainPage = new AppShellView();
        }
        IsInitiated = true;

        var targets = uri.ToString().Replace(GlobalSetting.AppShortcutUriBase, "");
        var subtargets = targets.Split('/');

        App.CurrentPageId = new Guid(subtargets[0]);
        if(subtargets.Length > 1 && !string.IsNullOrWhiteSpace(subtargets[1])){
            await Shell.Current.GoToAsync($"//{App.CurrentPageId}?buildingPartId={new Guid(subtargets[1])}");
        }
        else
        {
            await Shell.Current.GoToAsync("//" + App.CurrentPageId);
        }
    }

The Problem is that its called before OnStart. So its calling the correct page first but the reinit in OnStarts resets the app to base. I could overcome this issue with performing the init only of OnAppLinkRequestReceived is not called but then I have one last issue.

I call App.Current.UserAppTheme = _settingsService.OSAppTheme; as displayed above inside OnStart end the call inside the else black is failing with NullpointerException because App.Current seems to be null in this case. Do you know why?

1 Answers

Here are the final code bits that made the whole thing work. The key aspects are as follows:

  • Add another SplashScreen which is initiated inside App.xaml.cs constructor

  • Perform the long running async method in the OnStart()

  • Perform path based navigation which is triggered by an AppLinkRequest also inside the OnStart after storing the AppLinkRequest Uri inside a property

     public App()
     {
         InitializeComponent();
    
         _settingsService = ViewModelLocator.Resolve<ISettingsService>();
    
         ViewModelLocator.UpdateDependencies(_settingsService.UseDemoMode);
    
         _dataService = ViewModelLocator.Resolve<IDataService>();
    
         AppLinkUri = null;
    
         MainPage = new SplashScreen();
     }
    
     private async Task InitApp()
     {
         ActiveConfiguration = await _dataService.GetActiveConfigurationAsync().ConfigureAwait(true);
     }
    
     protected override async void OnStart()
     {
         base.OnStart();
    
         if (ActiveConfiguration == null)
         {
             await InitApp().ConfigureAwait(true);
         }
    
         MainPage = new AppShellView();
    
         if (AppLinkUri != null)
         {
             var targets = AppLinkUri.ToString().Replace(GlobalSetting.AppShortcutUriBase, "");
             var subtargets = targets.Split('/');
    
             App.CurrentPageId = new Guid(subtargets[0]);
             if (subtargets.Length > 1 && !string.IsNullOrWhiteSpace(subtargets[1]))
             {
                 await Shell.Current.GoToAsync($"//{App.CurrentPageId}?buildingPartId={new Guid(subtargets[1])}");
             }
             else
             {
                 await Shell.Current.GoToAsync("//" + App.CurrentPageId);
             }
         }
    
         base.OnResume();
     }
    
     protected override void OnAppLinkRequestReceived(Uri uri)
     {
         AppLinkUri = uri;
     }
    
Related