company_banner

Работаем с состояниями экранов в Xamarin.Forms

    Друзья! Мы рады представить новый материал на тему разработки мобильных приложений на Xamarin.

    В новой статье мы рассмотрим, как в Xamarin.Forms реализовывать управлениями состояниями окон (идет загрузка данных, отсутствует интернет и другие) на XAML.

    Все статьи из колонки можно найти и прочитать по ссылке #xamarincolumn

    Один экран, много состояний


    Подавляющее большинство современных бизнес-приложений активно взаимодействуют с внешними интернет-сервисами с целью получения данных для отображения. Помимо этого, часто возникают ситуации, в которых один и тот же экран может показывать разные наборы данных, вплоть до смены оформления.



    Мобильные приложения, в отличие от веб-сайтов должны гораздо быстрее взаимодействовать с пользователем, поэтому показывать длительное время пустой экран во время загрузки данных, считается не очень правильным. Дополнительно, приложение должно уведомлять пользователя об ошибках загрузки данных или отсутствии интернет-соединения. Ленивые разработчики могут обойтись отображением всплывающих уведомлений в духе “Ошибка загрузки данных”, но мы пойдем другим путем.




    Итак, давайте выделим основные состояния одного (!) экрана:



    • загрузка данных (индикатор загрузки по центру экрана)
    • отсутствует интернет-соединение (сопроводительный текст, возможно красивая картинка и кнопка “Повторить”)
    • ошибка загрузки данных (сопроводительный текст, возможно красивая картинка и кнопка “Повторить”)
    • нет данных (например, пустая корзина покупок)
    • отображение данных (например, загруженный список товаров)



    У программиста могут начать шевелиться волосы при мыслях о том, сколько кода надо будет написать, чтобы заменять содержимое одного экрана, при расчете, что таких экранов могут быть десятки, а каждое из состояний может быть достаточно сложным. Рано паниковать, простое и элегантное решение предложил Patrick McCurley. Мы возьмем это решение за основу и немного доработаем.



    Знакомьтесь — это StateContainer


    В основе данного подхода лежит идея описывать все состояния экрана в XAML и управлять их сменой с помощью ViewModel. Забегая вперед, отметим, что решение достаточно простое и может быть использовано не только для управлениями состояниями всего окна, но и отдельных его частей.

    Вот так будет выглядеть XAML-описание одной страницы с поддержкой смены состояний:

    <?xml version="1.0" encoding="utf-8" ?>
    <ContentPage x:Class="ApiDemo.DemoPage"
                 xmlns="http://xamarin.com/schemas/2014/forms"
                 xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                 xmlns:stateContainerDemo="clr-namespace:StateContainerDemo;assembly=ApiDemo">
        <stateContainerDemo:StateContainer State="{Binding State}">
    
            <stateContainerDemo:StateCondition State="Loading">
                <ActivityIndicator IsRunning="True" />
            </stateContainerDemo:StateCondition>
    
            <stateContainerDemo:StateCondition State="Normal">
                <Label Text="Данные загружены и можем их отобразить"/>
            </stateContainerDemo:StateCondition>
    
            <stateContainerDemo:StateCondition State="Error">
                <StackLayout>
                    <Label Text="Ошибка загрузки данных" />
                    <Button Command="{Binding LoadDataCommand}" Text="ПОВТОРИТЬ" />
                </StackLayout>
            </stateContainerDemo:StateCondition>
    
            <stateContainerDemo:StateCondition State="NoInternet">
                <StackLayout>
                    <Label Text="Отсутствует интернет-соединение" />
                    <Button Command="{Binding LoadDataCommand}" Text="ПОВТОРИТЬ" />
                </StackLayout>
            </stateContainerDemo:StateCondition>
    
            <stateContainerDemo:StateCondition State="NoData">
                <Label Text="Нет данных, показываем пользователю приглашение к действию" />
            </stateContainerDemo:StateCondition>
        </stateContainerDemo:StateContainer>
    </ContentPage>
    

    Просто и понятно. При этом крупные блоки для состояний можно вынести в виде отдельных View для повторного использования.

    Вот так будет описан враппер для одного состояния:

    [ContentProperty("Content")]
        public class StateCondition : View
        {
            public object State { get; set; }
            public View Content { get; set; }
        }
    

    А вот перечисление (enum) возможных состояний экрана:

    public enum States
        {
            Loading,
            Normal,
            Error,
            NoInternet,
            NoData
        }
    

    Мы немного доработали State Container от Patrick McCurley, добавив простые анимации смены состояния, чтобы все работало плавно:

    [ContentProperty("Conditions")]
        public class StateContainer : ContentView {
            public List<StateCondition> Conditions { get; set; } = new List<StateCondition>();
    
            public static readonly BindableProperty StateProperty = BindableProperty.Create(nameof(State), typeof(object), typeof(StateContainer), null, BindingMode.Default, null, StateChanged);
           
            public static void Init()
            {
                //for linker
            }
    
            private static async void StateChanged(BindableObject bindable, object oldValue, object newValue)
            {
                var parent = bindable as StateContainer;
                if (parent != null)
                    await parent.ChooseStateProperty(newValue);
            }
    
            public object State
            {
                get { return GetValue(StateProperty); }
                set { SetValue(StateProperty, value); }
            }
    
            private async Task ChooseStateProperty(object newValue)
            {
                if (Conditions == null && Conditions?.Count == 0) return;
    
                try
                {
                    foreach (var stateCondition in Conditions.Where(stateCondition => stateCondition.State != null && stateCondition.State.ToString().Equals(newValue.ToString()))) {
                        if (Content != null)
                        {
                            await Content.FadeTo(0, 100U); //быстрая анимация скрытия
                            Content.IsVisible = false; //Полностью скрываем с экрана старое состояние
                            await Task.Delay(30); //Позволяем UI-потоку отработать свою очередь сообщений и гарантировано скрыть предыдущее состояние
                        }
                
                        // Плавно показываем новое состояние   
                        stateCondition.Content.Opacity = 0;
                        Content = stateCondition.Content;
                        Content.IsVisible = true;
                        await Content.FadeTo(1);
    
                        break;
                    }
                } catch (Exception e)
                {
                    Debug.WriteLine($"StateContainer ChooseStateProperty {newValue} error: {e}");
                }
            }
        }

    Для получения статуса интернет-соединения мы подключили библиотеку ConnectivityPlugin.

    if (!CrossConnectivity.Current.IsConnected)
                {
                    State = States.NoInternet; // Меняем свойство у ViewModel
                    return;
                }

    Как видим, StateContainer это не надстройка над Page, а обычная ContentView и может вполне спокойно размещаться на экране со статическим или уже загруженным контентом. Это позволит реализовать механизмы частичной дозагрузки данных, например, когда у нас уже есть название и ссылка на фотографию, которые можно отображать пользователю без необходимости ожидания.




    Заключение


    Итак, сегодня мы рассмотрели работу с состояниями экранов с помощью простого и элегантного StateContainer, который позволяет улучшить пользовательский опыт. А простые анимации смены состояний добавляют плавности и придают приложению законченный вид.



    В следующей статье мы рассмотрим вопросы интеграции с внешним REST API с помощью Refit, ModernHttpClient и Polly.



    Об авторах



    Вячеслав Черников — руководитель отдела разработки компании Binwell. В прошлом — один из Nokia Champion и Qt Certified Specialist, в настоящее время — специалист по платформам Xamarin и Azure. В сферу mobile пришел в 2005 году, с 2008 года занимается разработкой мобильных приложений: начинал с Symbian, Maemo, Meego, Windows Mobile, потом перешел на iOS, Android и Windows Phone.

    Другие статьи автора:


    Полезные ссылки


    Microsoft
    Microsoft — мировой лидер в области ПО и ИТ-услуг

    Комментарии 5

      0
      Спасибо за познавательную статью.

      Так как XAML немного ухудшает производительность приложения (из опыта) возникает вопрос:
      Возможно ли использовать состояния не пользуясь XAML, а обычным кодом C#. И если да, то на сколько сложнее это будет.
        +1
        Протестировал. Состояния прекрасно работают из кода.

        public Page() {
                    var stateContainer = new StateContainer();
                    stateContainer.SetBinding(StateContainer.StateProperty, "State");
        
                    stateContainer.Conditions.Add(new StateCondition { State = "Loading", Content = new ActivityIndicator { IsRunning = true } });
                    stateContainer.Conditions.Add(new StateCondition { State = "Normal", Content = new Label { Text = "Данные загружены и можем их отобразить" } });
        
                    ...
        
                    Content = stateContainer;
        
                    BindingContext = this;
                }
        
          0
          Спасибо, обязательно попробую :)
        0
        if (Conditions == null && Conditions?.Count == 0) return;
        

        Вы это серьезно? В оригинале такой проверки нет, хотя Conditions зачем-то доступно для записи и там. Лучше уж newValue проверять на null.
          0
          Серьезно ;)

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое