{31 день с Mango} День 23: Модель выполнения

Это День 23 в серии статей 31 день с Mango (перевод оригинальной серии 31 Days of Mango), и был написан приглашенным автором Самидип Басу (Samidip Basu). Вы можете найти его в твиттере @samidip.
Просто согласитесь — наши смартфоны очень интеллектуальные на сегодняшний день. Но все электронные письма, интеграция в социальные сети, игры и приложения — все страдают от сравнительно небольших возможностей производительности и памяти смартфона, но что более важно — от батареи. Таким образом, решающее значение для любой мобильной операционной системы и приложений сторонних разработчиков имеет ответственность в оптимизации использования ресурсов смартфона, чтобы обеспечить гибкое, отзывчивое, но прочное взаимодействие с пользователем. Это как раз то место где модель выполнения приложения вступает в игру, и становятся важными для понимания различные состояния/события в жизненном цикле приложения. В этой статье мы поговорим о схожести в модели исполнения с Windows Phone (7.0) и о том, что изменилось в версии Mango для пользователей/разработчиков.

Путь Windows Phone
В то время как мы постоянно перемещаемся между различными частями операционной системы Windows Phone и сторонними приложениями, телефон имеет четко определенный подход к управлению жизненным циклом приложений от момента запуска до завершения. Модель выполнения просто позволяет только одному приложению работать на переднем плане в любой момент. В то время как мы видим, что это полезно для пользователя, позвольте мне поиграть в провокатора и злопыхателя и привести обратные доводы. Даже если бы это было возможно, можете ли вы действительно представить запуск нескольких приложений паралельно рядом друг с другом на Windows Phone? Не правда ли, что размеры экрана ограничены, чтобы попробовать это сделать? Фокусирование на одно приложение на переднем плане имеет смысл для форм-фактора телефона и создаёт захватывающий опыт использования, свободный от излишеств. Там где есть больше пространства, в подобных операционных системах с пользовательским интерфейсом в стиле Metro, есть возможность разрешить нескольким приложениям на переднем плане пристыковаться и работать рядом, как в прикрепленном режиме (snapped view) приложений для Windows 8. Но я отвлёкся 🙂
Таким образом, решение, что только одно приложение работает на переднем плане, имеет последствия для всего жизненного цикла приложения. Большинство пользователей Windows Phone широко используют для перемещения между сторонними приложениями и самой операционной системой кнопки Пуск/Назад, плитки (тайлы), тост-уведомления и др. Каждое приложение Windows Phone проходит через несколько этапов, каждый из которых вызывает события, точно также как мы перемещаемся по приложению, когда оно работает на переднем плане. Подробности приведены ниже.

Состояния приложений
В сущности, цикл каждого приложения Windows Phone проходит через следующие состояния:
Выполнение (Running) — Приложение находится на переднем плане. Операционная система и пользователь уделяют пристальное внимание тому, что оно делает.
Неактивное (Dormant) — В момент, когда приложение уходит с переднего плана, приложение переходит в стадию покоя (Dormant). Это новое усовершенствование Windows Phone Mango, где всё приложение с его историей и стеком переходов остаётся в целости и сохранности в памяти. Целью здесь является удовлетворить потребность в быстром реагировании приложения на возврат в приложение, которое только что ушло с переднего плана, если пользователь хочет вернуться в приложение. Это то, что мы называем быстрым переключением между приложениями (FAS — Fast Application Switching) в Windows Phone Mango.

Хотите попробовать? Ну, разумеется, вам нужен Windows Phone с обновлением Mango или эмулятор (из SDK 7.1 и выше). Переходите между различными частями операционной системы или через сторонние приложения, а затем нажмите и удерживайте кнопку Назад и вуаля! Вы увидите что-то вроде этого, не так ли?

То, что вы видите — это стек перехода по приложениям, след в памяти от приложений, которые перешли в неактивное состояние, но готовы мгновенно вернуться на передний план по вашему касанию экрана. Для пользователя — это многозадачность. Для вас, как для разработчика — это FAS (быстрое переключение между приложениями)… быстрое возвращение приложения без дополнительных действий с вашей стороны! Ну кроме перекомпиляции приложения созданного до выхода SDK для Mango (и предварительной версии), с помощью окончательного Windows Phone SDK 7.1 (прим. переводчика: или соответственно более позднего SDK 7.1.1). Это сразу даёт два эффекта — приложение не показывает надпись «Возобновление…» при возвращении на передний план из состояния покоя, а также вы увидите скриншот приложения в стеке перехода по приложениям. Мы увидим немного кода в дальнейшем.
Захоранивание (Tombstoned)
 — теперь, как мы знаем, среда Windows Phone сохраняет в памяти стек перехода для неактивных приложений, которые недавно были на переднем плане. В то время как мгновенное возвращение в приложение — это великолепно, любой смартфон имеет ограниченное количество доступной памяти, возможно потребуется поддержка для выполнения столь расточительной операции. Угадайте, что произойдет дальше… Да, приложение попадёт в стек перехода в режиме «последним вошёл, первым вышел» (Last-In-First-Out). Иными словами, они захоронены и считается, что они не потребляют каких-либо системных ресурсов после этого момента. Последнее утверждение является не совсем технически верным, как мы увидим через минуту.

Итак, как же влияет захоранивание на жизненный цикл приложений? Ну, а если пользователь использует приложение, а затем занялся игрой в несколько очень ресурсоемких игр, тогда есть шанс, что в конечном счёте приложения могут быть захоронены, чтобы освободить некоторый объем памяти для использования системой. Однако пользователь может решить вернуться через стек переходов обратно в приложение через некоторое время. Как следует вести себя приложению? Можем ли мы помнить то состояние приложения, в котором оно находилось, когда пользователь его покинул?
Да! Мы можем полностью предоставить пользователю опыт, который может произвести впечатление, что он никогда не покидал ваше приложение и каждый раз начинает работу с того места, на котором он прервал работу. Для достижения этого эффекта от нас, как от разработчиков, требуется поделиться некоторой ответственностью для управления состоянием нашего приложения в течение его жизненного цикла. В нижеприведенном коде мы увидим, каким образом мы можем гидратировать (прим. переводчика: метафора гидратации и дегидратации здесь применена сходно с возвращением в исходное состояние после обезвоживания веществ в реальном мире) состояние нашего приложения при возобновлении из состояния захоранивания, чтобы дать конечному пользователю впечатление, что приложение на самом деле никогда не было убито. На самом деле, даже если приложение захоронено, немного данных (в словаре) о состоянии приложения и стек перехода по страницам внутри приложения сохраняется в памяти ОС Windows Phone.
Прекращено – приложение в этом состоянии занимает абсолютно нулевой объем памяти в ОС Windows Phone. Ничто, в том числе словари состояния, о которых мы поговорим позже, не сохраняются и любое последующее использование приложению приводит к созданию нового экземпляра. Приложение достигает этой стадии после его захоронения или если пользователь совершает навигацию назад дальше первой страницы или же если приложение убивается после необработанного исключения.

Страницы и события
Прежде чем мы перейдем к событиям жизненного цикла приложения, вернёмся на шаг назад, чтобы убедиться, что наши основные принципы справедливы.  Все Windows Phone Silverlight приложения выполняютя как набор страниц XAML, которые загружаются внутрь фрейма приложения. В то время как приложение на переднем плане, пользователь может свободно перемещаться между страницами внутри приложения. Нажатие кнопки Назад на телефоне позволяет пользователю пройти по стеку перехода между страницами в приложении, до первой страницы приложения, пока не дойдёт до первой страницы приложения, а затем нажатие кнопки Назад приведет к выходу из приложения. Нам, как разработчикам, не нужно ничего делать, чтобы это произошло, так как ОС Windows Phone  автоматически заботится об этом при выполнении приложения на переднем плане.
Но учтите… пользователь во время ввода информации в элемент управления для ввода текста на одной XAML странице, может отвлечься и на самом деле выйти из приложения для выполнения некоторых операций. Когда он вернется, могут произойти две вещи. Во-первых, приложение могло перейти в неактивное состояние и быстро вернуться обратно на передний план, сохраняя то, что пользователь печатал… вашему приложению ничего не нужно, чтобы поддержать это, за исключением проверки через API, что ваше приложение было в неактивном состоянии.  Во-вторых, приложение может быть захоронено и теперь у вас есть небольшая проблема. Вам нужно привести приложение в жизненное состояние и гидратировать состояние страницы XAML в точности в то состояние, в котором пользователь оставил… мы это увидим в коде.
Теперь, когда мы говорим о состоянии страницы, самое время поговорить о двух событиях во время жизни самой страницы:
OnNavigatedTo — Это событие,  когда страница в настоящее время находится на переднем плане для отображения. Это даёт возможность нам, разработчикам, читать строки запросов, передаваемых странице, или гидратировать надлежащим образом состояние и содержимое контролов при выходе из захороненного состояния.
OnNavigatedFrom — Это событие возникает, когда пользователь покидает текущую страницу, чтобы перейти на другую страницу в приложении или при выходе из приложения в целом. В любом случае это служит хорошим контрольно-пропускной пунктом для сохранения всех сведений о состоянии текущей страницы, которые в случае восстановления могут потребоваться в будущем. Мы увидим примеры в коде.

Данные и словари состояния
Теперь давайте взглянем на то, какие данные мы пытаемся сохранить и гидратировать/дегидратировать в модели выполнения нашего приложения. На мой взгляд, данные приложения на Windows Phone бывают двух видов:
Настройки и постоянных данных — Этот тип данных является сутью вашего приложения и их необходимо хранить для всех запусков/экземпляров приложения. Мы не должны терять время на сохранении этих данных прямо в изолированное хранилище или ввиде пары имя/значение, или в SQL CE, если данные реляционные.
Данные страницы и переходные —  это одноразовые, но удобные для сохранения данные и в основном занимающие память. Данные этого типа могут быть чем угодно, начиная с введенных пользователем несохраненных данных на странице или ViewModel или временных данных в жизненном цикле приложения, принесенных извне. Это данные, с которыми мы должны обращаться осторожно, чтобы предоставить конечным пользователям впечатление, что наше приложение живое и  стоит на ногах на протяжении его жизненного цикла. Теперь у нас есть некоторая помощь. ОС Windows Phone предоставляет словарь для каждого приложения и для каждой страницы, которые могут быть использованы для хранения данных, сериализуемых в пару ключ-значение. Этот словарь состояния — из PhoneApplicationService и должен использоваться только для хранения данных переходных состояний. Не следует использовать это свойство для хранения чрезмерно больших данных, потому что есть предел 2 Мб для каждой страницы и 4 Мб для всего приложения, но, как мы увидим в коде, это довольно полезно для поддержания состояния в условиях модели выполнения нашего приложения. И это именно те словари, которые сохраняются в ОС Windows Phone даже после захоронения приложения. Если приложение активизируется из состояния захоронения, этот словарь состояния будет заполнен данными, которые мы сохранили в нём во время деактивации (Deactivation) приложения. Так как эта информация присутствует в памяти, мы можем использовать её для восстановления состояния без ресурсоемких операций с файлами.

События приложения
Теперь мы знаем все подробности и внутреннюю работу, давайте посмотрим что мы делаем в течение жизненного цикла приложения. PhoneApplicationService выдает 4 основных события в условиях модели выполнения приложения, которые помогают нам управлять состоянием:
Запуск (Launching) — это событие, при котором создается новый экземпляр приложения, в результате действий пользователя, таких как запуск из списка приложений, или нажатие на плитку (тайл)/тост или другими средствами. В этом случае, мы начинаем с чистого листа, а не как каое-то продолжение предыдущего жизненного цикла приложения.
Деактивирование (Deactivated) — это событие, когда пользователь перемещается из нашего приложения или запускается задача выбора (Chooser). Это лучшая возможность для нас, чтобы сохранить состояние приложения в словарь состояния для дальнейшего использования при восстановлении приложения. Если возобновление происходит из неактивного состояния (Dormant), то это не будет необходимо; но абсолютно необходимо при выходе из режима захоронения и событие деактивации — это единственный шанс, который мы можем получить, чтобы сохранить состояние приложения.
Активирование (Activated) — это событие происходит, когда приложение возвращается на передний план, либо из неактивного состояния или состояния захоронения. Угадайте что… простой API-флаг под названием IsApplicationInstancePreserved говорит нам о возобновлении нашего приложения по аргументам события. Если флаг имеет значение истина, то наше приложение только что перешло в неактивное состояние и хранится в памяти, поэтому никаких мер не требуется — оно опирается на FAS. Если флаг принимает значение ложь, мы возвращаемся из захороненного состояния и будем использовать словарь состояния для восстановления состояния приложения.
Закрытие (Closing) — это событие происходит, когда пользователь перемещается по страницам приложения по стеку навигации мимо первой страниц приложения. В этот  момент приложение должно уйти с переднего плана, и прекратить своё выполнение после создания события. Сведения о состоянии не сохраняются и последующие запуски приложения приведут к запускам свежих экземпляров.

Вцелом, в ключе этой темы, следует избегать тяжелых ресурсоемких задач во время любого из вышеуказанных событий, так как у нас есть ограниченное количество времени (10 секунд, если быть точным), чтобы ОС ждала завершение наших методов. Для лучшего пользовательского опыта некоторые ресурсоемкие операции сохранения состояния должны проводиться в фоновом потоке во время выполнения приложения.
Хотите понять всю вышесказанную информацию? Просто посмотрите на общую схему модели выполнения для Windows Phone, которая лучше всего описана в статье на MSDN:


Демонстрационное приложение
До сих пор мы только говорили, не так ли? Давайте посмотрим код! Мы собираемся создать простое Windows Phone приложение, которое выделяется управлением состоянием через жизненный цикл приложения. Создаваемое приложение полностью доступно в конце статьи; просто убедитесь что у вас установлен Windows Phone SDK 7.1 и скачайте проект.
Итак, давайте начнем. File -> New и мы выбираем основной шаблон по умолчанию для приложения с только одной XAML-страницей, которая представлена как MainPage.xaml/cs. Мы собираемся открыть XAML-страницу и отказаться от ряда демонстрационных элементов управления для нашего же блага. Конечное состояние будет примерно таким (кликните для увеличения):

Вот цель… Мы хотим разделить элементы управления для управления состояниями различными типами данных, как описано ниже:
App Setting (настройки приложения) — Это пример настроек уровня приложения, которые необходимы для сохранения всеми экземплярами приложения. Наиболее распространенным примером будут пользовательские настройки вашего приложения.
App Transient Data (переходные данные приложения) — Этот набор элементов управления будет имитировать данные на уровне приложения, что применяется только для экземпляра приложения, но может быть полезным в нескольких XAML-страницах во всем приложении. Лучшим примером могут быть данные веб-сервисов, которые приложение получает каждый раз при новом запуске.
Page Transient Data  (переходные данные страницы) — Этот набор элементов управления выступает для определенных данных, которые, возможно, мы не хотим потерять если пользователь выходит из нашего приложения и возвращается в него через стек переходов по приложениям.

Мы собираемся обрабатывать каждый тип данных отдельно. Итак, давайте начнем с App.xaml.cs — места для обработки глобальной логики приложения/данных. Давайте добавим некоторые пользовательские свойства/инициализаторы, наряду с небольшим количеством кода в конструкторе:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.IO.IsolatedStorage;

using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;

namespace Day_24___Execution_Model
{
public partial class App : Application
{
public PhoneApplicationFrame RootFrame { get; private set; }

public bool AppSetting { get; set; }
public string AppData { get; set; }

public static new App Current
{
get { return Application.Current as App; }
}

// Constructor.
public App()
{
// All other initialization code ..
AppData = string.Empty;

if (IsolatedStorageSettings.ApplicationSettings.Contains(«AppSetting»))
{
AppSetting = (bool)IsolatedStorageSettings.ApplicationSettings[«AppSetting»];
}
}
}
}

Далее, мы собираемся добавить код для обработчиков событий жизненного цикла приложения. Обратите внимание на использование флага IsApplicationInstancePreserved для определения будем ли мы выходить из неактивного/захороненного состояния в обработчике события активирование (Activated) приложения. У меня нет веских оснований, чтобы добавить код для обработки событий запуска(Launching)/закрытия (Closing) приложения; но вы можете это сделать, если ваше приложение нуждается в таком коде. Также обратите внимание на использование словаря состояния от PhoneApplicationService:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.IO.IsolatedStorage;

using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;

namespace Day_24___Execution_Model
{
public partial class App : Application
{
public PhoneApplicationFrame RootFrame { get; private set; }

public bool AppSetting { get; set; }
public string AppData { get; set; }

public static new App Current
{
get { return Application.Current as App; }
}

// Constructor.
public App()
{
// All other initialization code ..
AppData = string.Empty;

if (IsolatedStorageSettings.ApplicationSettings.Contains(«AppSetting»))
{
AppSetting = (bool)IsolatedStorageSettings.ApplicationSettings[«AppSetting»];
}
}

private void Application_Launching(object sender, LaunchingEventArgs e)
{
}

private void Application_Activated(object sender, ActivatedEventArgs e)
{
if (e.IsApplicationInstancePreserved)
{
// Returning from Dormancy.
// Do nothing.
}
else
{
// Returning from Tombstone.
if (PhoneApplicationService.Current.State.ContainsKey(«AppData»))
{
AppData = PhoneApplicationService.Current.State[«AppData»].ToString();
}
}
}

private void Application_Deactivated(object sender, DeactivatedEventArgs e)
{
PhoneApplicationService.Current.State[«AppData»] = AppData;
}

private void Application_Closing(object sender, ClosingEventArgs e)
{
}

// Other auto-generated code here ..
}
}

Теперь, вернувшись в наш код MainPage.xaml, давайте добавим обработчики событий для события загрузки страницы и нажатия на кнопку, для получения некоторых данных (которые у нас полностью искуственные), а также изменение состояния чекбокса для настройки приложения. Обратите внимание, как мы зависим от выбранных данных на уровне приложения, а также как мы сразу сохраняем настройки пользователя/приложения в изолированном хранилище телефона:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.IO.IsolatedStorage;

using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;

namespace Day_24___Execution_Model
{
public partial class MainPage : PhoneApplicationPage
{
// Constructor.
public MainPage()
{
InitializeComponent();
}

private void chkAppSetting_Tap(object sender, GestureEventArgs e)
{
// Store App Setting changes immediately in Isolated Storage for persistence.
if (IsolatedStorageSettings.ApplicationSettings.Contains(«AppSetting»))
{
IsolatedStorageSettings.ApplicationSettings.Remove(«AppSetting»);
}

if ((bool)this.chkAppSetting.IsChecked)
{
IsolatedStorageSettings.ApplicationSettings.Add(«AppSetting», true);
}
else
{
IsolatedStorageSettings.ApplicationSettings.Add(«AppSetting», false);
}
}

private void Button_Click(object sender, RoutedEventArgs e)
{
this.txtAppData.Text = «This is some sample data ..»;

// As if we are caching the data.
App.Current.AppData = this.txtAppData.Text;
}

private void PhoneApplicationPage_Loaded(object sender, RoutedEventArgs e)
{
this.chkAppSetting.IsChecked = App.Current.AppSetting;
}
}
}

Теперь, давайте добавим все важные события навигации страницы. Здесь мы можем контролировать, как мы гидратируем/дегидратируем переходные данные XAML-страницы при выходе приложения на первый план и уходе с него:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.IO.IsolatedStorage;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;

namespace Day_24___Execution_Model
{
public partial class MainPage : PhoneApplicationPage
{
// Constructor.
public MainPage()
{
InitializeComponent();
}

private void chkAppSetting_Tap(object sender, GestureEventArgs e)
{
// Store App Setting changes immediately in Isolated Storage for persistence.
if (IsolatedStorageSettings.ApplicationSettings.Contains(«AppSetting»))
{
IsolatedStorageSettings.ApplicationSettings.Remove(«AppSetting»);
}

if ((bool)this.chkAppSetting.IsChecked)
{
IsolatedStorageSettings.ApplicationSettings.Add(«AppSetting», true);
}
else
{
IsolatedStorageSettings.ApplicationSettings.Add(«AppSetting», false);
}
}

private void Button_Click(object sender, RoutedEventArgs e)
{
// Fake data fetch..
this.txtAppData.Text = «This is some sample data ..»;

// As if we are caching the data.
App.Current.AppData = this.txtAppData.Text;
}

private void PhoneApplicationPage_Loaded(object sender, RoutedEventArgs e)
{
// Load user setting from Isolated Storage.
this.chkAppSetting.IsChecked = App.Current.AppSetting;
}

protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
{
base.OnNavigatedTo(e);

// Load in App Data, if available.
if (App.Current.AppData != null && App.Current.AppData != string.Empty)
{
this.txtAppData.Text = App.Current.AppData;
}

// Load any Page State, if available.
if (PhoneApplicationService.Current.State.ContainsKey(«PageData»))
{
this.txtPageData.Text = PhoneApplicationService.Current.State[«PageData»].ToString();
}
if (PhoneApplicationService.Current.State.ContainsKey(«PageSetting»))
{
this.chkPageSetting.IsChecked = (bool)PhoneApplicationService.Current.State[«PageSetting»];
}
}

protected override void OnNavigatedFrom(System.Windows.Navigation.NavigationEventArgs e)
{
base.OnNavigatedFrom(e);

// Clear past state, if any.
if (PhoneApplicationService.Current.State.ContainsKey(«PageData»))
{
PhoneApplicationService.Current.State.Remove(«PageData»);
}

if (PhoneApplicationService.Current.State.ContainsKey(«PageSetting»))
{
PhoneApplicationService.Current.State.Remove(«PageSetting»);
}

// Save off Page state.
PhoneApplicationService.Current.State[«PageData»] = this.txtPageData.Text;
PhoneApplicationService.Current.State[«PageSetting»] = this.chkPageSetting.IsChecked;
}
}
}

Вот и все! Мы добавили достаточно кода, чтобы различные типы данных/состояний управляли всем жизненным циклом приложения, каждый по-своему в зависимости от обстоятельств. Пойдём вперёд и заставим его работать. Запустите приложение, измените настройки, перейдите в другое место и вернитесь, или выйдите и запустите свежий экземпляр приложения.  Данные и информация о состоянии должны быть сохранены и уничтожены, как ожидалось. Типичный запуск с установленными параметрами выглядит примерно так:

Теперь вы можете посмотреть как это выглядит на эмуляторе и на реальном устройстве. Когда мы перемещаемся через приложение, выходим и возвращаемся к нему в среде выполнения Windows Phone Mango, трудно предсказать, когда наше приложение будет фактически захоронено. В большинстве случаев, если приложение деактивировано, то оно просто находится в неактивном состоянии и возвращается обратно к жизни. Таким образом, хотя у нас есть код для гидратации настроек, при возвращении из захороненного состояния нет определенного способа протестировать это.
Не беспокойтесь! Существуют простые отладочные настройки, которые будут принудительно переводить приложение в захороненное состояние в момент его отключения. Таким образом, даже если приложение может оставаться в неактивном  состоянии, этот параметр, как показано ниже, заставляет его перейти в захороненное состояние. Это, очевидно, помогает нашему тестированию.

Итоги
Добавьте точки останова в различные обработчики событий и посмотрите как они ведут себя в течение жизненного цикла приложения — это отличный способ понять модель выполнения. Итак, что же вы ждете? Идите вперед и в полной мере используйте быстрое переключение между приложениями (FAS) и гидратацию/дегидратацию состояния вашего приложения/страницы правильно в течение всего жизненного цикла приложения.
Результат — счастливые пользователи, а в конечном счёте — вы. Удачи!
Полный проект описанного в статье приложения доступен по следующей ссылке.

Перевод: Сергей Урусов

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *