O tym jak metoda Initialize MvvmCross napsuła mi krwi

W tym wpisie podzielę się historią problemu, który przydarzył się mi i mojemu zespołowi podczas prac nad aplikacją Xamarin przy wykorzystaniu MvvmCross. Problem ten znacznie utrudniał nam pracę i pojawiał się losowo, a co gorsze nie zawsze dawał o sobie znać.

Wstęp

Nasza aplikacja pobierała po uruchomieniu konfigurację początkową z backendu. Standardowo skorzystaliśmy z Refita. Problem polegał na tym, że raz na kilka uruchomień aplikacji dostawaliśmy TaskCanceledException w miejscu wywołania żądania. Co kończyło się niepobraniem konfiguracji i w efekcie braku poprawnej inicjalizacji aplikacji.

Byliśmy kompletnie zdezorientowani tym, dlaczego dostajemy wyjątek, skoro nie wykonywaliśmy nigdzie skomplikowanej logiki. Wysyłaliśmy tylko jeden request. Nasze żądanie wykonywaliśmy w metodzie Initialize modelu widoku, więc tej, która jest polecana przez twórców MvvmCross do wykonywania tych cięższych operacji.

Uznaliśmy, że problem może leżeć w bibliotece Refit, więc zaczęliśmy od jej aktualizacji, sprawdzania issues na GitHub, przeszukiwaliśmy fora internetowe. Po prostu szukaliśmy przyczyny gdzie to tylko możliwe, ale bez skutku. Jednocześnie sprawdzaliśmy sam framework MvvmCross pod kątem problemów z metodą Initialize. Bingo! Tak, to tutaj leżał problem. Na czym polegał?

Wyjaśnienie

Według wątku na GitHubie frameworka problem pojawił się w MvvmCross po wydaniu wersji 6. Aplikacje, które miały zadania asynchroniczne w metodzie Initialize pierwszego modelu widoku, nie działały poprawnie. Zmiany w wersji 6 spowodowały, że model pierwszego widoku jest skutecznie ładowany synchronicznie. Twórcy wprowadzili te zmiany, ponieważ aplikacje nie powinny wykonywać ciężkich operacji przed wyświetleniem pierwszego widoku aplikacji, gdyż istnieją wymagania platformy, które powodują zakończenie aplikacji, jeśli nie wyświetli pierwszego widoku po odpowiednim czasie. Ta zmiana oznacza, że ​​każdy kod, który próbuje przeskoczyć do innego wątku, spowoduje przerwanie pierwszego widoku aplikacji. 

W istocie tak było. W naszym projekcie mamy kilka prostych aplikacji, które składają się maksymalnie z dwóch widoków. W pierwszych z nich zawsze pobieramy konfigurację i korzystaliśmy właśnie z metody Initialize. 

Przekonaj się o tym i stwórz prostą solucję Xamarin wraz z MvvmCross. Możesz skorzystać z gotowego rozwiązania, które stworzyłem w ramach artykułu Solucja Xamarin Native ze wsparciem MvvmCross. Gotową solucję pobierzesz z mojego GitHub. Do FirstViewModel dodaj następujący kod.

public async override Task Initialize()
{
    Debug.WriteLine("Initialize started!");

    await Task.Delay(100);

    Debug.WriteLine("Initialize finished!");
}

Jeżeli uruchomisz aplikację na iOS w konsoli zobaczysz tylko pierwszą informację. Natomiast na Androidzie jeżeli korzystasz ze splash screena wszystko zadziała poprawnie, ponieważ wtedy nasz FirstView będzie już drugim widokiem.

Rozwiązanie

W takim razie co zrobić, aby móc skorzystać z asynchronicznej metody w pierwszym modelu widoku? Musimy wykonać pewną sztuczkę, a na imię jej…

CustomAppStart.

Pierwszą nawigację wywołamy asynchronicznie, powielając domyślną pierwszą synchroniczną nawigację MvvmCross:

protected override async Task NavigateToFirstViewModel(object hint = null)
{
    try
    {
        if (hint is TParameter parameter)
            NavigationService.Navigate<TViewModel, TParameter>(parameter).GetAwaiter().GetResult();
        else
        {
            MvxLog.Instance.Trace($"Hint is not matching type of {nameof(TParameter)}. Doing navigation without typed parameter instead.");
            await base.NavigateToFirstViewModel(hint);
        }
    }
    catch (System.Exception exception)
    {
        throw exception.MvxWrap("Problem navigating to ViewModel {0}", typeof(TViewModel).Name);
    }
}

Poniżej nasza nowa implementacja startu aplikacji.

public class App : MvxApplication
{
    public override void Initialize()
    {
        RegisterCustomAppStart<AppStart<FirstViewModel>>();
    }
}

public class AppStart<TViewModel> : MvxAppStart<TViewModel> where TViewModel : IMvxViewModel
{
    public AppStart(IMvxApplication app, IMvxNavigationService mvxNavigationService) : base(app, mvxNavigationService)
    {
    }

    protected override Task NavigateToFirstViewModel(object hint = null)
    {
        NavigationService.Navigate<TViewModel>();

        return Task.CompletedTask;
    }
}

Ciekawy przypadek

Innym rozwiązaniem tego problemu na iOS mogłoby być stworzenie widoku pośredniego. Takiego odpowiednika splash screena z Androida.

O rozwiązaniu tego problemu dowiedziałem się czytając ten wątek oraz znajdując rozwiązanie na blogu Nick’s .NET Travels.

Liczę na to, że przypadek jest ciekawy i w przyszłości komuś pomoże.

Mam coś dla Ciebie

Zapisz się do mojego newslettera, a ja prześlę Ci zbiór kilkunastu praktycznych wskazówek dla programisty aplikacji mobilnych.

Menu