dotnet / maui

.NET MAUI is the .NET Multi-platform App UI, a framework for building native device applications spanning mobile, tablet, and desktop.
https://dot.net/maui
MIT License
22.14k stars 1.74k forks source link

Performance of navigation between two pages in a MAUI Shell app is really very poor. Taking more than 4-5 secs to navigate to a page #19605

Open nsood9 opened 10 months ago

nsood9 commented 10 months ago

Description

I'm developing a simple app in MAUI Shell for android and iOS with custom UI controls, api consumption and local database. But the performance of MAUI Shell app is really very poor. I'm just navigating between screens and it is taking really long time to navigate to a different screen. It gets stuck on the button click for 4-5 secs before navigating forward. I'm not doing any heavy processing or resource or I/O operations etc, simply passing object/dictionary between the screens. Navigating to a blank screen is smooth but as soon as I add the UI controls even without any sort of bindings, it starts to lag while navigating. Xamarin forms apps never behaved in this manner and their performance has never been this poor. The bare minimum we can do in an app is to have a basic design, but MAUI shell app is not even handling the simple UI elements without performance lag. Is there any solution to this problem? Does anybody else has faced this issue? Can someone please help?

Steps to Reproduce

I'm doing simple navigation on either button click or listview item selection like this:

Page 1 ViewModel:

[RelayCommand]
        public async Task SelectCrewMember(ItemTapCommandContext context)
        {
var crew = context.Item as MobileFlightLogCrew;
                if (crew != null)
                {
                    await NavigationService?.NavigateToAsync(NavigationConstants.CrewDetails, new Dictionary<string, object> { { "CrewData", crew } });
                }
                else
                {
                    await DialogService.ShowAlertAsync("Error", "Unable to perform this action at the moment!", AppResources.OkButton);
                }
}

First Page Xaml:

 <telerik:RadListView x:Name="listView" ItemsSource="{Binding Mflog.Crews}" VerticalOptions="FillAndExpand" Margin="0">
                <telerik:RadListView.Commands>
                    <telerik:ListViewUserCommand Id="ItemTap" Command="{Binding SelectCrewMemberCommand}"/>
                </telerik:RadListView.Commands>

Second Page ViewModel:

[QueryProperty(nameof(Crew), "CrewData")]
    public partial class CrewDetailsViewModel : ViewModelBase
    {
        [ObservableProperty]
        MobileFlightLogCrew crew;

        public CrewDetailsViewModel(INavigationService navigationService, IDialogService dialogService, IMobileCrewDataService crewDataService) : base(navigationService, dialogService)
        {
            mobileCrewDataService = crewDataService;
            if (Crew == null)
                Crew = new MobileFlightLogCrew();
        }

         public override async Task InitializeAsync()
        {
           // await IsBusyFor(
           //async () =>
           //{
           //    if (Crew != null)
           //     {
           //         FullName = $"{Crew?.Person?.FirstName} {Crew?.Person?.LastName}";
           //         Crew.Approaches.ForEach(async x => x.ApproachType = await mobileCrewDataService.GetMobileCodeBase(x.ApproachTypeId));
           //         if (Approaches == null)
           //             Approaches = Crew.Approaches?.ToObservableCollection();
           //     }
           //     //return base.InitializeAsync();
           // });

        }
}

This is the second page which takes a lot of time to be navigated to from the first page, even if I'm not doing any long running processes like data fetch etc. It's simple navigation to a simple page with custom controls in the UI. It is one example, I have similar examples for all other page navigations in my app

Link to public reproduction project repository

No response

Version with bug

8.0.3

Is this a regression from previous behavior?

Not sure, did not test other versions

Last version that worked well

Unknown/Other

Affected platforms

iOS, Android

Affected platform versions

iOS 17+, Android 33

Did you find any workaround?

Couldn't find any work around as I'm doing a simple page navigation. The only work around I found is to navigate to a blank page without any UI element/binding or data processing in VM, then only navigation works smoothly as expected.

Relevant log output

No response

drasticactions commented 10 months ago

From my memory, I believe that an invoked RelayCommand and constructor code are invoked on the UI Thread, and are therefor blocking calls. As you didn't provide a full example of what you wrote, my guess would be that you're blocking the UI Thread somewhere in code (As in, creating your ViewModel while on the UI Thread) you didn't show and that's why the navigation isn't happening sooner. I think we would need a fuller sample of how you implemented your code to know for sure what you're doing.

ghost commented 10 months ago

Hi @nsood9. We have added the "s/needs-repro" label to this issue, which indicates that we require steps and sample code to reproduce the issue before we can take further action. Please try to create a minimal sample project/solution or code samples which reproduce the issue, ideally as a GitHub repo that we can clone. See more details about creating repros here: https://github.com/dotnet/maui/blob/main/.github/repro.md

This issue will be closed automatically in 7 days if we do not hear back from you by then - please feel free to re-open it if you come back to this issue after that time.

nsood9 commented 10 months ago

@drasticactions It's a big project with lots of other backend/database projects as well, cannot provide the project, I can share some more relevant code. I'm selecting a Crew item from the listview on First screen which is PostFlight and navigating to second page CrewDetails, this navigation is taking too long. Relevant code:

First Page ViewModel:

 public partial class PostFlightViewModel : ViewModelBase, IDisposable
    {
        #region Services fields
        private IMobileCrewDataService mcds;
        IPopupService popUpService;
        #endregion

        #region Fields & Properties
        [ObservableProperty]
        MobileFlightLog mflog;
        [ObservableProperty]
        MobileLeg legDetails;
        [ObservableProperty]
        [NotifyPropertyChangedFor(nameof(TotalHobbs))]
        public decimal? beginHobbs;
        [ObservableProperty]
        [NotifyPropertyChangedFor(nameof(TotalHobbs))]
        public decimal? endHobbs;
        public decimal? TotalHobbs { get { return EndHobbs - BeginHobbs; } }
        [ObservableProperty]
        private ObservableCollection<MobileFlightLogDelay> deptDelays;
        [ObservableProperty]
        private ObservableCollection<MobileFlightLogDelay> arrivalDelays;
        [ObservableProperty]
        private ObservableCollection<MobileFlightLogApproach> approaches;
        [ObservableProperty]
        private MobileFlightLogApproach approach;
        [ObservableProperty]
        private string diversionAirport;
        [ObservableProperty]
        private bool isDiverted;

        private DivertType selectedDivertType;
        public DivertType SelectedDivertType
        {
            get => selectedDivertType;
            set => SetProperty(ref selectedDivertType, value);
        }

        public List<string> DiverTypes
        {
            get => Enum.GetNames(typeof(DivertType)).ToList();
        }

        #endregion

        #region Event Handlers
        readonly WeakEventManager cancelEventManager = new();
        public event EventHandler<EventArgs> Canceled
        {
            add => cancelEventManager.AddEventHandler(value);
            remove => cancelEventManager.RemoveEventHandler(value);
        }

        readonly WeakEventManager showPopupEventManager = new();
        public event EventHandler<EventArgs> ShowPopup
        {
            add => showPopupEventManager.AddEventHandler(value);
            remove => showPopupEventManager.RemoveEventHandler(value);
        }
        readonly WeakEventManager saveEventManager = new();
        public event EventHandler<MobileFlightLogApproach> Saved
        {
            add => saveEventManager.AddEventHandler(value);
            remove => saveEventManager.RemoveEventHandler(value);
        }
        #endregion

        public PostFlightViewModel(INavigationService navigationService, IDialogService dialogService, IFlightDataService flightDataService, IMobileCrewDataService mobileCrewDataService, IPopupService popupService) : base(navigationService, dialogService, flightDataService)
        {
            mcds = mobileCrewDataService;
            popUpService = popupService;
            LegDetails = new MobileLeg();
            Mflog = new MobileFlightLog();
        }

        [RelayCommand]
        public async Task SelectCrewMember(ItemTapCommandContext context)
        {
var crew = context.Item as MobileFlightLogCrew;
                if (crew != null)
                {
                    await NavigationService?.NavigateToAsync(NavigationConstants.CrewDetails, new Dictionary<string, object> { { "CrewData", crew } });
                }
                else
                {
                    await DialogService.ShowAlertAsync("Error", "Unable to perform this action at the moment!", AppResources.OkButton);
                }
}

Second Page code-behind to set ViewModel:

public partial class CrewDetails : ContentPageBase
{
    public CrewDetails(CrewDetailsViewModel crewDetailsViewModel)
    {
        InitializeComponent();
        BindingContext = crewDetailsViewModel;
    }
}

NavigationService:

public class NavigationService: INavigationService
    {
        public NavigationService()
        {
        }

        public void InitializeAsync() =>
             SetUpRoot();

        private void SetUpRoot()
        {
            if (AppGlobals.IsTokenSet())
            {
                //this.MainPage = new LoginPage();
                Application.Current.MainPage = new AppShell();
            }
            else
            {
                Application.Current.MainPage = new Pages.LoginPage(ServiceHelper.GetService<ViewModels.LoginViewModel>());
            }
        }

        public Task NavigateToAsync(string route, IDictionary<string, object> routeParameters = null)
        {
            var shellNavigation = new ShellNavigationState(route);

            return routeParameters != null
                ? Shell.Current.GoToAsync(shellNavigation, routeParameters)
                : Shell.Current.GoToAsync(shellNavigation);
        }

        public async Task NavigateBackAsync()
        {
            await Shell.Current.GoToAsync("..");
        }
    }