Портування додатку Windows 8.1 на Windows Phone 8.0 з розбором проблем

    
 
На прикладі простого Windows 8.1 додатка подивимося наскільки просто переносити додатки з WinRT (Windows 8.1) на Silverlight (WP8.0) і по ходу розберемо декілька підводних каменів.
 
 
Ви напевно чули про просувається способі розробки Windows / Windows Phone додатків — Universal Apps. Підхід здоровий, але ринкова частка WP8.1 поки ще тільки починає рости, а додаток потрібно робити зараз, тому зупинимося на WP8.0 (Silverlight). Переваги: ​​підтримка девайсів як на WP8.0 так і на WP8.1, підтримка всіх типів екранів без «чорних смуг» (на відміну від WP7 додатків), стабільні сторонні бібліотеки т.д.
 
 

Коротко про вихідний додатку

 
Піддослідним кроликом виступатиме майже готове односторінкове додаток Windows 8.1 для відображення основних курсів валют.
 
Оскільки додаток безкоштовний і без реклами, функціонал жорстко лімітований. Тільки найнеобхідніші функції: відображення середнього курсу валют (USD, EUR, RUB), графік флуктуації за останні 30 днів, відображення курсів по банках, конвертер валют і підтримка Live Tiles.
Серверну частину залишимо за кадром т.к. там немає нічого цікавого.
 
 

Структура проекту

 
З прицілом на Universal Apps основні блоки винесені в юзер контроли, класи даних винесені в Portable Library, а код відповідає за отримання даних відділений від UI.
 
 
Переносимо код на Windows Phone 8.0
Створюємо порожній проект, викидаємо все зайве з MainPage, додаємо потрібні референс і NuGet пакети. Насамперед додаємо як посилання (Add -> Existing Item -> Add As Link ) файли класів для обробки даних з Windows 8 проекту.
 
 
Проблеми:
◊ Очікувати що неймспейси зійдуться не доводиться (WinRT vs Silverlight), тому використовуємо теплий ламповий # if # endif, виходить що то типу такого:
 
#if NETFX_CORE
using Windows.Web.Http;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Networking.BackgroundTransfer;
#endif
#if WINDOWS_PHONE
using System.Net.Http;
#endif

Мушу зазначити, що хоч ми і не використовуємо Universal Apps, Visual Studio 2013 Update 2 сильно спрощує роботу з # if # endif для різних платформ. З'явився ще один список, що випадає прямо над кодом, що дозволяє швидко перемкнути платформу без перевідкриття файлу. IntelliSense більше не падає, Resharper не відвалюється в не самий відповідний момент. Ніяких більше «This document is opened by another project.» І т.п.
 
◊ Оскільки дані ми завантажуємо за допомогою HttpClient (пізніше буде зрозуміло, чому саме HttpClient), який відсутній в WP8.0, додаємо NuGet пакет Microsoft.Net.Http. Але й тут не без сюрпризів, без # if не обійшлося:
 
var client = new HttpClient();
            byte[] buff;
#if NETFX_CORE
            var ibuff = await client.GetBufferAsync(uri);
            buff = ibuff.ToArray();
#endif
#if WINDOWS_PHONE
            buff = await client.GetByteArrayAsync(uri);
#endif

Питання на засипку, скільки офіційних реалізацій HttpClient ви знаєте?
 
◊ Отримані дані ми зберігаємо в пам'яті пристрою, що б було що показати наступного разу за відсутності інтернет підключення, але… ну ви зрозуміли:
 
#if NETFX_CORE
                var file = await ApplicationData.Current.LocalFolder.CreateFileAsync(LOCAL_DATA_FILENAME, CreationCollisionOption.ReplaceExisting);
                await FileIO.WriteTextAsync(file, json);
#endif
#if WINDOWS_PHONE
                var fs = await ApplicationData.Current.LocalFolder.OpenStreamForWriteAsync(LOCAL_DATA_FILENAME, CreationCollisionOption.ReplaceExisting);

                using (StreamWriter streamWriter = new StreamWriter(fs))
                {
                    await streamWriter.WriteAsync(json);
                }
#endif

Тут варто пам'ятати про різницю між LocalFolder і LocalCache (починаючи з WP8.1), основна відмінність в тому, що LocalFolder на відміну від LocalCache схильний вбудованому beckup / restore (не плутати з roaming) і як наслідок ваші дані можуть виявитися на зовсім іншому пристрої (або кількох) з усіма витікаючими. Детальніше як це працює можна подивитися тут . У нашому випадку LocalCache на WP8.0 не доступний, тому використовуємо що маємо.
 
◊ Окремо варто згадати підтримку шифрування даних, історично склалося що у мене вже був самопісний багатоплатформовий бінарно сумісний клас шифрування використовуючи AES , який кочує з проекту в проект. Опис цього класу виходить за рамки статті, скажу тільки що під WP8.0 існує чудовий клас AesManaged, а під WinRT використовую мапперов на нативну реалізацію:
 
#if NETFX_CORE
using Windows.Security.Cryptography;
using Windows.Security.Cryptography.Core;
using Windows.Storage.Streams;
#else
using System.Security.Cryptography;
#endif   

 
 
Переносимо UI на Windows Phone 8.0
 
 
Після того як допоміжний код перенесений і успішно компілюється, беремося за інтерфейс. Т.к. телефон, планшет або десктоп це зовсім не одне і те ж. Тому і додаток буде виглядати по-різному. Для цього Windows 8 і Windows Phone проекти будуть мати свої власні MainPage а юзер контроли ми будемо додавати як посилання (Add As Link, в майбутньому все це просто переїде в Shared project ). Плюс самі контроли будуть адаптуватися під ситуацію, наприклад ширину доступного простору (не забуває про snapped mode в Windows 8).
 
 
Проблеми:
◊ Перше що потрібно зробити, це розділити неймспейси в code behind класах з допомогою все тих же # if # endif, приклад наводити не буду, просто користуємося Shift + Alt + F10 на всьому що підкреслено червоним.
 
◊ Так склалося що в WP8.0 немає події DataContextChanged, тому трохи змінюємо логіку, що б від нього позбавиться.
 
◊ Оскільки в WP8.0 немає вбудованих стилів з WinRTшного XAMLа, таких як «SymbolThemeFontFamily», «BaseTextBlockStyle», «BodyTextBlockStyle» та ін створюємо окремий ResourceDictionary в WP8.0 проекті. Відкриваємо «c: \ Program Files (x86) \ Windows Kits \ 8.1 \ Include \ winrt \ xaml \ design \ generic.xaml» і видирає все що нам потрібно і кладемо у щойно створений ResourceDictionary, попутно замінюючи або викидаючи те що не підтримується на телефоні (CharacterEllipsis -> WordEllipsis, SemiLight -> Light, Typography. * -> c: \ NUL).
Потім Мержа цей довідник в App.xaml:
 
<Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="CommonStyles.xaml"/>
            </ResourceDictionary.MergedDictionaries>
            <local:LocalizedStrings xmlns:local="clr-namespace:BXFinanceWP" x:Key="LocalizedStrings"/>
        </ResourceDictionary>
    </Application.Resources>

Варто звернути увагу, якщо у вас вже є щось в Application.Resources (наприклад, LocalizedStrings, якщо ви його не викинули на першому етапі) його потрібно буде помістити всередину ResourceDictionary, як показано вище.
 
◊ Вже не знаю чому, але IValueConverter інтерфейси трохи відрізняються між WinRT і Silverlight, так що правимо всі загальні (додані As Link) конвертори що є в проекті:
 
public object Convert(object value, Type targetType, object parameter 
#if NETFX_CORE
            ,string language 
#endif
#if WINDOWS_PHONE
            ,System.Globalization.CultureInfo culture
#endif
            )

 
◊ Наступне з чим довелося зіткнутися, це різниця оголошення сторонніх неймспейсов в XAML. У WinRT неймспейси описуються через «using:», а в Silverlight через «clr-namespace:», приклад:
 
xmlns:Common="using:BXFinanceDashboard.Common"
xmlns:Common="clr-namespace:BXFinanceDashboard.Common"

Рішень цієї проблеми не так багато, і жодного доброго, # ifdef для XAML немає. У таких випадках, виходячи з конкретної ситуації, можна створювати необхідні контроли в коді, морочитися з кстомнимі build action, виносити все що можна в стилі і / або розділяти інтерфейс на загальні та платформо-залежні файли. На крайній випадок copy-paste.
Але в нашому випадку не все так складно, т.к. в більшості випадків оголошення неймспейса було необхідно що б підключити конвертори. Тому я просто створив по одному ResourceDictionary в кожному проекті і оголосив конвертори там. І оскільки програма не велика, підключив його глобально в App.xaml. Плюс в тому, що конвертори не створюються повторно, але потрібно пам'ятати, що імена тепер глобальні і не можуть повторюватися.
На всякий випадок приведу лістинг, WinRT (Windows 8.1):
 
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:Common="using:BXFinanceDashboard.Common">
    <Common:HighlightValueConverter x:Key="HighlightUSDBuyConverter" HighlightBrush="#FFFF4646"  />
    <Common:HighlightValueConverter x:Key="HighlightUSDSellConverter" HighlightBrush="#FFFF4646"  />
    <Common:HighlightValueConverter x:Key="HighlightEURBuyConverter" HighlightBrush="#FFFF4646"  />
    <Common:HighlightValueConverter x:Key="HighlightEURSellConverter" HighlightBrush="#FFFF4646"  />
    <Common:HighlightValueConverter x:Key="HighlightRUBBuyConverter" HighlightBrush="#FFFF4646"  />
    <Common:HighlightValueConverter x:Key="HighlightRUBSellConverter" HighlightBrush="#FFFF4646"  />
    <Common:BoolToVisibilityConverter x:Key="BoolVisibilityConverter"/>
    <Common:BoolToVisibilityConverter x:Key="BoolNotVisibilityConverter" IsReversed="True"/>
    <Common:OpacityConverter x:Key="OpacityConverter"/>
    <Common:OpacityConverter x:Key="OpacityNotConverter" IsReversed="True"/>
</ResourceDictionary>

Silverlight (WP8.0):
 
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:Common="clr-namespace:BXFinanceDashboard.Common">

    <Common:HighlightValueConverter x:Key="HighlightUSDBuyConverter" HighlightBrush="#FFFF4646" NormalBrush="{StaticResource PhoneForegroundBrush}"  />
    <Common:HighlightValueConverter x:Key="HighlightUSDSellConverter" HighlightBrush="#FFFF4646" NormalBrush="{StaticResource PhoneForegroundBrush}"  />
    <Common:HighlightValueConverter x:Key="HighlightEURBuyConverter" HighlightBrush="#FFFF4646" NormalBrush="{StaticResource PhoneForegroundBrush}"  />
    <Common:HighlightValueConverter x:Key="HighlightEURSellConverter" HighlightBrush="#FFFF4646" NormalBrush="{StaticResource PhoneForegroundBrush}"  />
    <Common:HighlightValueConverter x:Key="HighlightRUBBuyConverter" HighlightBrush="#FFFF4646" NormalBrush="{StaticResource PhoneForegroundBrush}"  />
    <Common:HighlightValueConverter x:Key="HighlightRUBSellConverter" HighlightBrush="#FFFF4646" NormalBrush="{StaticResource PhoneForegroundBrush}"  />
    <Common:BoolToVisibilityConverter x:Key="BoolVisibilityConverter"/>
    <Common:BoolToVisibilityConverter x:Key="BoolNotVisibilityConverter" IsReversed="True"/>
    <Common:OpacityConverter x:Key="OpacityConverter"/>
    <Common:OpacityConverter x:Key="OpacityNotConverter" IsReversed="True"/>
</ResourceDictionary>

 
◊ Сюрпризом стало відміну шрифтів на Windows і Windows Phone. З якоїсь причини для відображення стрелочек зміни курсу я вибрав символи # 128314 / # 128315 з «Segoe UI Symbol» але недогледів, що вони називаються «Up / Down-Pointing Red Triangle». На Windows вони виглядають саме так як потрібно, а на Windows Phone вони обидва червоні що не перефарбовуй, і до того ж символ вже по ширині. Порившись ще, знайшов більш відповідні # 9650 / # 9660. Не відразу було зрозуміло в чому справа, довелося навіть провести порівняння на телефоні:
 
 
До речі не забувайте використовувати саме гліфи (особливо в кнопках на апп барі ), а не свої картинки. Задовбав генеріть десяток картинок під різні DPI, для прикладу скрін іншої програми з Nokia 1520:
 
 
 

Адаптуємо додаток під телефон

Додаток зібралося, добре, але зупинятися не варто. Все-таки мобільний це не те ж саме що планшет або десктоп (до речі додаток на десктопі саме оновлюється якщо висить на екрані, зручно для всяких інформаційних панелей).
 
 
Layout
Для перемикання графіка курсів, списку банків і конвертера валют беремо більш звичний на Windows Phone контрол Pivot. Включаємо трей (ховати трей без вагомої причини погано), підганяємо верхній відступ, що б трей НЕ з'їдав багато місця. Благо в WP8 з треєм менше проблем ніж на WP7, стрибає рідше, але не забуваємо що в коді колір трея можна вказувати не раніше Page.Loaded. Що б уникнути некрасивою смужки між треєм і сторінкою ствім прозорість 0.99.
  
Оскільки екран телефону вже по ширині, необхідно що б всі основні контроли адаптувалися відповідно (так само це корисно для snapped mode на десктопі).
 
Список банків автоматично відображає тільки одну валюту якщо місця недостатньо, а внизу з'являється перемикач. Причому важливою вимогою тут було, що б прокрутка списку залишалася на місці при перемиканні валют. Тобто що б не доводилося заново гортати до необхідного банку.
 
 
Конвертер валют, в свою чергу, загортає деякі елементи і вирівнюється по ширині:
 
 
Домогтися такої поведінки можна кількома способами, наприклад, використовуючи стану (states), але цього разу я просто прібінділ властивості HorizontalAlignment до самого контролу, головне розкидати елементи грід а не StackPanel. Приклад:
 
<UserControl
…
    x:Name="Parent" HorizontalAlignment="Left">

    <Grid x:Name="RootPanel">
…
        <Grid Grid.Column="1" HorizontalAlignment="{Binding HorizontalAlignment, ElementName=Parent}">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <ToggleButton x:Name="btnActionSell" Content="Продажа" Grid.Column="0" VerticalAlignment="Center" />
            <ToggleButton x:Name="btnActionBuy" Content="Покупка" Grid.Column="1" VerticalAlignment="Center" />
        </Grid>

А в MainPage на телефоні додаємо Stretch:
 
<Controls:CurrencyConverter x:Name="currencyConverterControl" HorizontalAlignment="Stretch" />

Також з точки зору юзабіліті, було дуже бажано, що б на всіх підтримуваних DPI клавіатура не закривала кнопки вибору. Плюс додав, що б при введенні (або вставці) суми, можна було прямо на клавіатурі натиснути знак долара або євро (знак рубля на клавіатурі і в шрифті поки відсутній) з відповідним перемиканням валюти.
 
 
 
Live Tiles
Оскільки додаток під Windows Phone 8.0, про WNS мріяти не доводиться, тому на телефоні використовуємо старий, добрий Periodic Background Agent. Код нічим особливим не відрізняється , потрібно тільки не забути запитати користувача при першому старті, чи хоче він отримувати оновлення в фоні. Побічним позитивним ефектом від використання свого агента, стало те що, коли користувач, наприклад, в дорозі і без інтернету, у нас вже є не сильно старі дані для відображення (на Windows 8.1 цю проблему вирішуємо німого по-іншому, див. нижче). При підготовці зображень для тайлів, пам'ятаємо, що в Windows Phone логотип розташовується строго по центру (з підписом або без), а в Windows 8 — трохи зміщується вгору.
 
 
 
Графіки
Окремо варто згадати перенесення графіка флуктуацій. За звичкою на Windows графік був реалізований за допомогою Controls.DataVisualization (додаток робилося з прицілом на Universal Apps, а Телеріковскіе контроли ще не вийшли (на момент написання)). І як виявилося, саме під Windows Phone 8.0 нормального порту DataVisualization немає. Під WinRT (Windws 8 і Windows Phone 8.1) є WinRTXamlToolkit , навіть на Windows Phone 7 можна було використовувати лібу від звичайного Silverlight. А от під WP8.0 ніяк (в WPToolkit при перенесенні на WP8 DataVisualization загубився). Взагалі ще з WP7, WPToolkit не перестає "радувати".
Загалом, витративши півгодини на пошуки, кинув Телеріковий контрол на телефоні (можливо і на Windows 8 на нього перейду при порте на Universal Apps), підігнав зовнішній вигляд і забув як страшний сон.
 
 
 
Кеширование
У Windows 8.1 (в WP8.1 відсутній) з'явилася чудова фіча: можна підказати ОС які урли кешувати. Тобто якщо зірки зійдуться, (а точніше, якщо ваш додаток регулярно використовується) Windows сама закешірует потрібні вам дані ще до запуску програми. Називається ця штука Content Prefetcher . Можна додати конкретні урли або xml файл-список з сервера (якщо дані динамічні, наприклад новини). Простий приклад:
 
public static void RegisterPrefetchUrls()
        {
                if (!ContentPrefetcher.ContentUris.Any(u => u.AbsoluteUri == LIVE_DATA_URL))
                {
                    ContentPrefetcher.ContentUris.Clear();
                    ContentPrefetcher.ContentUris.Add(new Uri(LIVE_DATA_URL));
                }
        }

Відповідно вантажимо дані як зазвичай через HttpClient. Але працює тільки з Windows.Web.Http.HttpClient (той, який працює через WinInet, не плутати з іншими реалізаціями HttpClient). Якщо потрібно завантажити дані тільки з кешу, якщо вони є, застосовуємо фільтр Filters.HttpCacheReadBehavior.OnlyFromCache .
До слова в VS Update 2 з'явилася корисна менюшка що б змусити ОС закеширувати дані в цілях налагодження, раніше було тільки з консолі. Але потрібно не забути спочатку запустити сам додаток, що б вказати урли, подробней тут .
 
 
 

Статистика використання

Цього разу було вирішено підключити Google Analytics, а не Flurry. Аж надто Flurry показує неправильні цифри в порівнянні з власною статистикою (не тільки у мене ). Плюс бідна підтримка платформ відмінних від iOS / Android і моторошно тупящееся веб-панель (80 + http запитів при кожному перезавантаженні). Як на мене, є сенс використовувати Flurry тільки для крос-платформних ігор т.к. є дуже корисні інструменти відстеження установок, in-app і т.п.
 
Для Windows / Windows Phone є готова Ліба «Google Analytics SDK for Windows 8 and Windows Phone ». Інтеграція проста, тільки я б порадив викликати SendView () не з OnNavigatedTo () як у прикладі, а з Page.Loaded що б зарахувати покази коли користувач повертається кнопкою тому. Також я додав платформу в поле версії в analytics.xml:
 
<appVersion>1.0.0.0 (WP)</appVersion>

Залежно від специфіки додатку є сенс додати кастомниє події (SendEvent ()). Як приклад, мені цікаво скільки користувачів дійсно буде використовувати конвертер валют, або який відсоток відключив Live Tile.
 
 

Фінальні штрихи

Робимо сторіночку додатки, не забуваємо додати msApplication-ID в мета теги, що б користувач міг поставити програму з меню IE. Якщо є бажання, реєструємо додаток в Bing, що б користувач міг поставити додаток прямо зі сторінки результатів веб пошуку (на Windows 8.1 / Windows Phone 8.0/8.1).
Єдине що засмучує це відсутність російськомовної кнопки «скачати» для Windows Phone Store (може представники MS прокоментують?). Якщо слідувати правилам використання торгових марок Microsoft, потрібно використовувати тільки вже готові зображення, є навіть цілий гайд де і як їх використовувати. У підсумку виходить, що для Windows Phone Store немає кнопки російською, а для Windows Store немає без слова «Download». Доводиться використовувати англійські версії кнопок:
 
 
 

Висновок

Вийшло якось довго і сумбурно, хоч я і намагався вибрати тільки цікаві моменти. Якщо що упустив, запитуйте в коментарях.
    
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.