Друзья! Мы рады представить новый материал на тему разработки мобильных приложений на 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.
Другие статьи автора:
- Авторизация OAuth для Xamarin-приложений
- Деплоим мобильный софт с помощью devops-конвейера Microsoft
- DevOps на службе человека
- Автоматизируем неавтоматизируемое, или про Xamarin в реальных проектах
- Удобный REST для Xamarin-приложений
- Быстрое создание MVP (minimum viable product) на базе Microsoft Azure и Xamarin.Forms
- Готовим Xamarin.Forms: настройка окружения и первые шаги
- Повышаем эффективность работы в Xamarin.Forms
- Подключаем Facebook SDK для Xamarin.Forms
- Подключаем ВКонтакте SDK для Xamarin.Forms
Полезные ссылки
- Анонс бесплатных инструментов Xamarin для разработки кроссплатформенных приложений на С# по ссылке
- Visual Studio 2015 Community, Visual Studio Team Services, Visual Studio Code: бесплатные предложения для разработчиков
- Дополнительные бесплатные инструменты и службы в программе Visual Studio Dev Essentials