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

Slava Chernikoff
Feb 24, 2017 · 4 min read

Четвертая статья в цикле “Рецепты для Xamarin-разработчиков”.

Оригинал: https://habrahabr.ru/company/microsoft/blog/307890/


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

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

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

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

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

У программиста могут начать шевелиться волосы при мыслях о том, сколько кода надо будет написать, чтобы заменять содержимое одного экрана, при расчете, что таких экранов могут быть десятки, а каждое из состояний может быть достаточно сложным. Рано паниковать, простое и элегантное решение предложил 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.

Slava Chernikoff

Written by

Tech Lead at Binwell / Microsoft MVP

Binwell

Binwell

Разработка облачных и мобильных решений под заказ

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade