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.
- I am using the current version of Xamarin Forms, my main target is xamarin android
- I am building on top of the xamarin forms Shell pattern
- I am using a sqlite database for the main part of the apps configuration
- The app layout is mainly dependent on the configuration stored in the sqlite database (what I call a data driven app)
- 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:
- 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?