年 9 月 2015

ボリューム 30 番号 9

クラウド接続型モバイル アプリ - 認証とオフライン サポートを備えた Xamarin アプリのビルド

Kraig Brockschmidt

8 月号に掲載した 2 部構成の連載の第 1 部「Azure Web Apps と WebJobs を使用した Web サービスの作成」(msdn.microsoft.com/magazine/mt185572) で説明したように、今日では、多くのモバイル アプリが、価値のある興味深いデータを提供する 1 つ以上の Web サービスに接続されています。これらのサービスを REST API から直接呼び出し、その応答をクライアント内で処理するのが簡単な方法ですが、そのような方法はバッテリ、帯域幅を消費し、さまざまな Web サービスの制限事項を調整することになるため、コストがかかります。ローエンドのハードウェアでは、パフォーマンスが低下することもあります。そのため、カスタム バックエンドに作業をオフロードすることに意味があることを、"Altostratus" プロジェクトを使ってデモしました。

第 1 部で説明した Altostratus バックエンドは、StackOverflow と Twitter (2 つの異なるデータ ソース) から定期的に「会話」を収集して正規化し、Microsoft Azure データベースに格納します。つまり、クライアントが必要とする正確なデータをバックエンドから直接提供するため、最初にデータを提供したプロバイダーの制限事項を調整することなく、クライアント数に応じて Azure 内でバックエンドを拡張できます。また、クライアントのニーズに合わせてバックエンドでデータを正規化することで、モバイル デバイスのリソースを大幅に節約し、Web API 経由でのデータ交換を最適化しました。

今回は、クライアント アプリについて詳しく説明します (図 1 参照)。まず、全体の状況を把握するためにアプリのアーキテクチャを説明し、その後 Xamarin と Xamarin.Forms の使用、バックエンドとの認証、オフライン キャッシュの構築、Team Foundation Server (TFS) と Visual Studio Online での Xamarin を使ったビルドを取り上げます。

Android タブレット (左)、Windows Phone (中央)、および iPhone (右) 上で動作する Xamarin モバイル アプリ
図 1 Android タブレット (左)、Windows Phone (中央)、および iPhone (右) 上で動作する Xamarin モバイル アプリ

クライアント アプリのアーキテクチャ

クライアント アプリには、3 つの主要ページ (ビュー) として Configuration (構成)、Home (ホーム)、および Item (アイテム) があり、それぞれ同名のクラスがあります (図 2 参照)。二次的なログイン (Login) ページには UI がなく、単に OAuth プロバイダーの Web ページのホストとして機能します。基本的なモデル - ビュー - ビューモデル (MVVM: Model-View-ViewModel) 構造を使用するため、ログインを除く各メイン ページには、ビューとデータ モデルの各クラスとのバインド関係を処理する関連ビュー モデル クラスがあります。データ モデルの各クラスは、それぞれアイテム、カテゴリ、構成設定 (認証プロバイダーのリストなど) を表します。

関係する主要クラスの名前を示す Altostratus クライアント アプリのアーキテクチャ (特に指定のない限り、プロジェクト内のファイルとそのクラス名は同じ)
図 2 関係する主要クラスの名前を示す Altostratus クライアント アプリのアーキテクチャ (特に指定のない限り、プロジェクト内のファイルとそのクラス名は同じ)

 (注: 開発者の多くは、ビュー モデルやその他のクラスとは独立したポータブル クラス ライブラリ (PCL) に XAML ビューを分離し、デザイナーが個別に Blend などのツールを使用してビューを操作できるようにすることを好みます。今回はプロジェクトの構造を単純にするためと、現時点では Blend が Xamarin.Forms のコントロールを操作できないため、こうした分離を行いませんでした。)

データ モデルには、常に、ローカル SQLite データベースからオブジェクトを設定します。このデータベースは、アプリの初回実行時にデータを事前に設定します。データ モデルで行われるバックエンドとの同期は単純なプロセスで、新しいデータを (ネットワーク トラフィックを最小限に抑えて) 取得し、取得したデータでデータベースを更新して、古いデータを消去し、オブジェクトを最新状態に更新するようデータ モデルに指示します。このプロセスは、ビュー モデルとのデータ バインドによって UI の更新をトリガーします。

後ほど取り上げますが、同期のきっかけとなるイベントは多種多様です。たとえば、UI の更新ボタン、構成の変更、(保存済みの構成を取得する) バックエンドとの認証、30 分以上経過後のアプリの再開などがあります。同期は、当然ながら Web API を利用したバックエンドとの主要通信になりますが、バックエンドには、ユーザーの登録、認証済みユーザーの設定の取得、認証済みユーザーの構成変更時の設定更新などを目的とした API も提供します。

クロス プラットフォーム クライアント用 Xamarin.Forms

MSDN Magazine で何回も取り上げてきたように、Xamarin では C# と Microsoft .NET Framework を使用して、Android、iOS、および Windows 向けのアプリをビルドでき、その多くのコードをプラットフォーム間で共有できます (開発構成の概要については、後半の「TFS と Visual Studio Online での Xamarin を使用したビルド」を参照してください)。Xamarin.Forms フレームワークは、XAML と C# 向けに共通の UI ソリューションを提供することで、さらに多くのコードを共有できるようにします。Xamarin.Forms により、Altostratus プロジェクトでは 1 つの PCL で 95 パーセント以上のコードを共有できるようになります。プロジェクトのプラットフォーム固有のコードは、実際にはそれほど多くなく、プロジェクト テンプレートからの少量のスタートアップ コード、ログイン ページ用に Web ブラウザー コントロールを処理するレンダリング コード、事前にデータが設定された SQLite データベースの読み取りと書き込みを適切に行えるようにローカル ストレージ上にコピーする数行のコードだけです。

Xamarin プロジェクトを構成して、PCL 以外の共有プロジェクトを使用することもできます。ただし、Xamarin では PCL を使用することが推奨されます。Altostratus の主なメリットは、Win32 コンソール アプリケーションでも同じ PCL を使用して、事前にデータを設定したデータベースを作成している点です。つまり、事前にデータを設定するだけのためにデータベース コードが重複することがなく、初期化プログラムはアプリの他の部分と常に同期された状態になります。

共有コードを使用しても、各ターゲット プラットフォーム上のアプリを徹底的にテストするのに必要な作業は減らないことを忘れないでください。プロセスのテストにかける時間は、各アプリをネイティブに作成した場合と変わりません。また、Xamarin.Forms は非常に新しいため、コードでの対処が必要なプラットフォーム固有のバグやその他の動作が見つかる場合があります。Altostratus の作成時に見つかったバグやその他の動作の詳細については、bit.ly/1g5EF4j (英語) のブログ記事を参照してください。

よくわからない問題を見つけたら、まずは Xamarin バグ データベース (bugzilla.xamarin.com、英語) を確認します。同じ問題が見つからなければ、Xamarin Forums (forums.xamarin.com、英語) で質問するか、問題を投稿して、Xamarin スタッフの迅速な対応に期待します。

各プラットフォームの UI 層の詳細を理解するよりも、こうした個別の問題に対応する方がはるかに簡単です。Xamarin.Forms は比較的新しいものなので、このように問題点を見つけることで、フレームワークが堅牢になっていきます。

プラットフォーム固有の調整

Xamarin.Forms 内でのレイアウト調整など、プラットフォームごとに調整が必要になることがあります (さまざまな例については、Charles Petzold のすばらしい書籍『Programming Mobile Apps with Xamarin.Forms』 (bit.ly/1H8b2q6) を参照してください)。また、webview 要素で最初の Navigating イベントが発生したときなど、いくつか動作の不整合への対処が必要になることもあります (これについては、bit.ly/1g5EF4j (英語) のブログ記事を参考にしてください)。

そのため、Xamarin.Forms には API Device.OnPlatform<T>(iOS_value, Android_value, Windows_value) と、これに対応する XAML 要素があります。OnPlatform は現在のランタイムに応じて異なる値を返します。たとえば、次の XAML コードは、Windows Phone では [Configuration] (構成) ページのログイン制御を非表示にします。これは、Xamarin.Auth コンポーネントが Windows Phone をまだサポートしていないためです。したがって、Windows Phone では常に未認証の状態で実行されます (configuration.xaml)。

<StackLayout Orientation="Vertical">
  <StackLayout.IsVisible>
    <OnPlatform x:TypeArguments="x:Boolean" Android="true" iOS="true"
      WinPhone="false" />
  </StackLayout.IsVisible>
  <Label Text="{ Binding AuthenticationMessage }" FontSize="Medium" />
  <Picker x:Name="providerPicker" Title="{ Binding ProviderListLabel }"
    IsVisible="{ Binding ProviderListVisible }" />
  <Button Text="{ Binding LoginButtonLabel}" Clicked="LoginTapped" />
</StackLayout>

コンポーネントに関しては、Xamarin 自体がネイティブ プラットフォームの共通機能を抽象化するコンポーネントを主体に構築されており、その多くは、UI 用の Xamarin.Forms が登場する前から存在しています。一部のコンポーネントは Xamarin に組み込まれていますが、その他のコンポーネントはコミュニティへの投稿を含め components.xamarin.com (英語) から取得します。Altostratus では、Xamarin.Auth 以外に、Connectivity Plugin (tinyurl.com/xconplugin、英語) を使用して、インジケーターを表示し、デバイスがオフラインのときは最新の情報に更新するためのボタンを無効にしています。

このプラグインでは、デバイスの接続状態が変化する (プラグインの IsConnected プロパティに反映される) タイミングと、プラグインがイベントを発生させるタイミングにずれがあることがわかりました。つまり、デバイスがオフラインなってから、[Refresh] (最新情報への更新) ボタンが無効状態に変化するまで数秒の時間差があります。これに対処するため、今回は Refresh コマンド イベントを使用して、プラグインの IsConnected 状態をチェックすることにしました。オフライン状態であれば、すぐにボタンを無効にしますが、オンラインに戻るときは、自動的に同期を開始するよう ConnectivityChanged ハンドラーに指示するフラグを設定します。

また、Altostratus は Xamarin.Auth (tinyurl.com/xamauth、英語) を使用して、OAuth による認証の詳細も処理します。これについては、後ほど説明します。現時点では、このコンポーネントがサポートするのが iOS と Android のみで、Windows Phone をサポートしていないことに注意が必要です。今回のプロジェクトの目的から、この問題には対処していません。さいわい、クライアント アプリは未認証の状態でも適切に実行されます。ただし、ユーザーの設定がクラウドで保持されず、バックエンドとのデータ交換の最適化が完全には行われません。コンポーネントがアップデートされ、Windows をサポートするようになったら、login コントロールが表示されるように、前述の XAML の OnPlatform タグを削除するだけです。

バックエンドとの認証

Altostratus アプリケーションでは全体として、HTTP 要求の処理時にバックエンドがユーザー固有の基本設定を自動的に適用するように、ユーザー固有の基本設定をバックエンドに格納するメカニズムをデモしようとしています。今回のアプリケーションであれば、もちろん、要求と URI パラメーターだけを使用して同じ結果を実現できますが、そのような例ではシナリオが複雑になったときに土台として機能しません。また、サーバーに基本設定を格納しておけば、ユーザーが所有するすべてのデバイスでその基本設定をローミングできます。

ユーザー固有のデータを操作する場合は、どのような種類のデータであってもユーザーを一意に認証する必要があります。念のため、「認証」と「承認」は意味が異なります。「認証」とは、ユーザーを特定して、そのユーザーが本人かどうかを検証する方法です。一方、「承認」は、特定のユーザーが所有する権限 (管理者、一般ユーザー、ゲストなど) に関係します。

今回の認証は、独自の資格情報システムを実装するのではなく、Google や Facebook など、サード パーティによるソーシャル ログインを使用します (バックエンドには、クライアント アプリが [Configuration] (構成) ページの UI からプロバイダーのリストを入手するための API を用意します)。ソーシャル ログインを行う主なメリットは、資格情報と、それに付随するセキュリティやプライバシーに関する問題に対処する必要がまったくないことです。バックエンドは、ユーザー名として電子メール アドレスのみを保存し、クライアントは実行時にアクセス トークンのみを管理します。そうしなければ、電子メールの検証やパスワードの取得など、あらゆる面倒な処理をプロバイダーが担当することになります。

もちろん、すべてのユーザーがソーシャル ログイン プロバイダーのアカウントを持っているわけではありません。プライバシーを理由にソーシャル メディア アカウントを使用しないユーザーもいます。また、ソーシャル ログインは基幹業務アプリには適していない場合もあります。そのような場合は、Azure Active Directory がお勧めです。ただし、今回の目的からは、個々のユーザーを認証する何らかの手段が必要なので、ソーシャル ログインが論理的な選択肢と言えます。

認証を受けたユーザーは、バックエンドに基本設定を保存することが承認されます。他のレベルの承認 (他のユーザーの基本設定を変更できるなど) を実装する場合は、バックエンドにアクセス許可データベースを用意してユーザー名をチェックすることになります。

OAuth2 を使用した ASP.NET Web API でのソーシャル ログイン: OAuth2 (bit.ly/1SxC1AM、英語) は、ユーザーの資格情報を共有することなく、リソースへのアクセスをそのユーザーに許可する認証フレームワークです。OAuth2 では、さまざまなエンティティ間で資格情報が渡されるしくみを指定する「資格情報フロー」を複数定義します。ASP.NET Web API では、いわゆる「Implicit Grant Flow」を使用します。この場合、モバイル アプリは資格情報を収集する必要も、シークレットを格納する必要もありません。この処理は OAuth2 プロバイダーと ASP.NET Identity ライブラリ (asp.net/identity、英語) でそれぞれ行われます。

ソーシャル ログインを有効にするには、開発者ポータルでアプリケーションをそれぞれのログイン プロバイダーに登録しなければなりません (この場合の "アプリケーション" は、モバイルや Web など、すべてのクライアント エクスペリエンスを含み、特にモバイル アプリには限定されません)。登録が完了すると、プロバイダーからクライアントの一意 ID とシークレットが提供されます。サンプルについては、bit.ly/1BniZ89 (英語) を参照してください。

それらの値を使用して、ASP.NET Identity ミドルウェアを初期化します (図 3 参照)。

図 3 ASP.NET Identity ミドルウェアの初期化

var fbOpts = new FacebookAuthenticationOptions
{
  AppId = ConfigurationManager.AppSettings["FB_AppId"],
  AppSecret = ConfigurationManager.AppSettings["FB_AppSecret"]
};
fbOpts.Scope.Add("email");
app.UseFacebookAuthentication(fbOpts);
var googleOptions = new GoogleOAuth2AuthenticationOptions()
{
  ClientId = ConfigurationManager.AppSettings["GoogleClientID"],
  ClientSecret = ConfigurationManager.AppSettings["GoogleClientSecret"]
};
app.UseGoogleAuthentication(googleOptions);

"FB_AppID" のような文字列は、実際の ID とシークレットの格納先となる Web アプリの環境と構成設定を示すキーとなります。これにより、リビルドや再デプロイを行わずにアプリを更新できます。

Xamarin.Auth を使用した資格情報フローの処理: ソーシャル認証のプロセスでは全体的に、アプリ、バックエンド、プロバイダー間のさまざまなハンドシェイクが必要です。さいわい、Xamarin.Auth コンポーネント (Xamarin コンポーネント ストアにあります) は、アプリ向けにこのハンドシェイクの大半を処理します。これには、ブラウザー コントロールの操作やコールバック フックの提供が含まれているため、アプリは承認の完了時に応答するだけです。

Xamarin.Auth は既定で、クライアント アプリに承認動作の一部を実行させます。つまり、クライアント ID とシークレットを格納するのはアプリの役割です。これは ASP.NET のフローとは若干異なりますが、Xamarin.Auth には適切にファクタリングされたクラス階層があります。Xamarin.Auth.OAuth2Authenticator クラスは、必要な基本機能を提供する WebRedirectAuthenticator から派生し、少量のコードを追加で記述する必要があるだけです。このクラスは Android プロジェクトと iOS プロジェクトの LoginPageRenderer.cs ファイルに含めています (Xamarin.Auth は、まだ Windows をサポートしません)。ここで行う処理の詳細については、ブログ記事 (tinyurl.com/kboathalto、英語) を参照してください。

クライアント アプリには、Xamarin.Forms の基本 ContentPage から派生する LoginPage クラスがあり、ユーザーをそのページにナビゲートします。このクラスは 2 つのメソッド CompleteLoginAsync と CancelAsync を公開します。これらのメソッドは、プロバイダーの Web インターフェイスでユーザーが行う操作に応じて、LoginPageRenderer コードから呼び出されます。

認証済み要求の送信: ログインに成功したら、クライアント アプリにはアクセス トークンが提供されます。認証済み要求を作成するには、次のように Authorization ヘッダーにトークンを含めるだけです。

GET http://hostname/api/UserPreferences HTTP/1.1
Authorization: Bearer I6zW8Dk...
Accept: */*
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; rv:11.0) like Gecko

ここで、"Bearer" は長い不明瞭なトークン文字列の後にベアラー トークンを使用する承認を示します。

各要求に認証ヘッダーを追加するには、カスタム メッセージ ハンドラーを使って、すべての REST 要求に System.Net.Http.HttpClient ライブラリを使用します。メッセージ ハンドラーは、HTTP 要求/応答メッセージを調査して変更できるプラグイン コンポーネントです。詳細については、bit.ly/1MyMMB8 (英語) を参照してください。

メッセージ ハンドラーは、AuthenticationMessageHandler クラス (webapi.cs) で実装し、HttpClient インスタンスの作成時にインストールします。

_httpClient = HttpClientFactory.Create(
  handler, new AuthenticationMessageHandler(provider));

ITokenProvider インターフェイスは、単に、ハンドラーがアプリからアクセス トークンを取得する方法です (これは model.cs の UserPreferences クラスで実装しています)。SendAsync メソッドは HTTP 要求ごとに呼び出されます。トークン プロバイダーが使用できる Authorization ヘッダーがある場合は、それを追加します (図 4 参照)。

図 4 Authorization ヘッダーの追加

public interface ITokenProvider
{
  string AccessToken { get; }
}
class AuthenticationMessageHandler : DelegatingHandler
{
  ITokenProvider _provider;
  public AuthenticationMessageHandler(ITokenProvider provider)
  {
     _provider = provider;
  }
  protected override Task<HttpResponseMessage>
    SendAsync(HttpRequestMessage request,
    System.Threading.CancellationToken cancellationToken)
  {
    var token = _provider.AccessToken;
    if (!String.IsNullOrEmpty(token))
    {
      request.Headers.Authorization =
        new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
    }
    return base.SendAsync(request, cancellationToken);
  }
}

第 1 部で説明したように、バックエンドでは、要求と一緒にトークンを受け取った場合、そのトークンを使用してユーザー基本設定を取得し、残りの要求に自動的に適用します。たとえば、ユーザーが会話制限数を既定値の 100 ではなく 25 に設定した場合、要求から返されるのは最大 25 アイテムになるため、ネットワーク帯域幅が削減されます。

バックエンド データのオフライン キャッシュの構築

モバイル Web を上回るモバイル アプリの大きなメリットは、オフラインでの使用をサポートする柔軟性です。モバイル アプリはその仕様上、常にユーザーのデバイスに存在し、ブラウザーベースのメカニズムを使わずに、SQLite などのさまざまなデータ ストレージ オプションを使用してオフライン データ キャッシュを保持できます。

実際、Altostratus モバイル クライアントの UI は、ローカル SQLite データベースに保持されているデータを操作するため、オフラインでも完全に機能します。オンラインになったら、バックグラウンド プロセスがバックエンドから現在のデータを取得し、データベースを更新します。これにより、データベースの上位に位置するデータ モデル オブジェクトが更新され、その後、データ バインドによって UI の更新がトリガーされます (前述の 図 2 参照)。これは、第 1 部で説明したバックエンドと非常によく似たアーキテクチャです。第 1 部のバックエンドでは、進行中の Web ジョブがデータを収集し、正規化して、SQL Server データベースに格納するため、Web API はそのデータベースから直接要求を処理できます。

Altostratus のオフライン サポートには、次の 3 つの個別のタスクが関与します。

  • 事前にデータを設定したデータベース ファイルをアプリ パッケージに直接配置して、オンラインになる必要なく、初回実行時に作業データを即座に提供するタスク。
  • UI に表示されるデータ モデルのパーツ (会話(アイテム)、カテゴリ、認証プロバイダー、ユーザーの基本設定) ごとに一方向の同期プロセスを実装するタスク。
  • UI の [Refresh] (最新情報への更新) ボタン以外で、適切なトリガーとの同期をフックするタスク。

ここからは、この 3 つのタスクを順番に見ていきます。

事前にデータを設定したデータベースの作成: ユーザーは、オンライン ストアからアプリをインストールできますが、デバイスがオフラインの場合は実行されません。デバイスがオンラインにならないと、何も実行できないのは問題です。デバイスがオフラインでも、それなりに実行することが望まれます。

この方法をデモするため、Altostratus クライアントの各プラットフォームのアプリ パッケージには、事前にデータを設定した SQLite データベースを含めます (このデータベースを、各プラットフォーム プロジェクトの resources/raw フォルダーに格納します)。初回実行時、クライアントはこのデータベース ファイルを読み取り/書き込みが可能なデバイス上の場所にコピーし、その後はこのコピーだけを使用します。ファイル コピー プロセスは各プラットフォームの一意な処理になるため、Xamarin.Forms.DependencyService 機能を使用して、実行時に、ISQLite という定義済みのインターフェイスの特定の実装に解決します。これは、DataAccessLayer コンストラクター (DataAccess.cs) で行い、ISQLite.GetDatabasePath を呼び出して、データベース ファイルのコピー先の読み取り/書き込みが可能なプラットフォーム固有の場所を取得します (図 5 参照)。

図 5 データベース ファイルをコピーしたプラットフォーム固有の場所の取得

public DataAccessLayer(SQLiteAsyncConnection db = null)
{
  if (db == null)
  {
    String path = DependencyService.Get<ISQLite>().GetDatabasePath();
    db = new SQLiteAsyncConnection(path);               
    // Alternate use to use the synchronous SQLite API:
    // database = SQLiteConnection(path);               
  }
  database = db;
  _current = this;
}

初期データベースを作成するために、Altostratus ソリューションには DBInitialize という小さな Win32 コンソール アプリケーションを含めています。このアプリケーションはアプリと同じ共有 PCL を使用してデータベースを操作するため、コード ベースが一致しない 2 つ目のコードが存在するという問題は発生しません。ただし、DBInitialize では DependencyService を使用する必要がありません。つまり、ファイルを直接作成し、接続を開くだけです。

string path = "Altostratus.db3";
SQLiteAsyncConnection conn = new SQLiteAsyncConnection(path);
var dbInit = new DataAccessLayer(conn);

この後、DBInitialize は DataAccessLayer.InitAsync を呼び出してテーブル (事前にデータを設定したデータベースをアプリが操作する必要がないもの) を作成し、他の DataAccessLayer メソッドを使用してバックエンドからデータを取得します。非同期呼び出しなので、DBInitialize は .Wait を使用するだけです。DBInitialize はコンソール アプリケーションなので、レスポンシブ UI を考慮する必要がありません。

DataModel model = new DataModel(dbInit);
model.InitAsync().Wait();
model.SyncCategories().Wait();
model.SyncAuthProviders().Wait();
model.SyncItems().Wait();

ユーザーに目に見えるフィードバックを提供するために、タイマーを使用して、0.5 秒ごとに画面上にドットを表示します。

事前にデータを設定したデータベースは、必ず DB Browser for SQLite (bit.ly/1OCkm8Y、英語) のようなツールを使用してチェックしてから、プロジェクトに含めるようにします。1 つ以上の Web 要求が失敗する可能性がありますが、その場合はデータベースは有効になりません。このロジックを DBInitialize に組み込んで、失敗したデータベースを削除し、エラーを表示するようにします。今回は、エラー メッセージを監視して、必要に応じてプログラムを再実行するだけです。

事前にデータベースにデータを設定しておくと、コンテンツは比較的すぐに古くなってしまいます。このような古いデータがアプリの初回実行時に表示されるのは避けたいと思います。アプリを定期的に更新しなければ必然的にデータは古くなります。したがって、アプリを定期的に更新して、データベースにある程度最新 (データの性質によって変わります) のデータが含まれるようにします。

ユーザーがアプリを既にインストールしている場合、更新による影響はありません。パッケージに含めたデータベース ファイルをコピーするコードは、読み取り/書き込みが可能なコピーが既に存在するかどうかをチェックし、存在するものをそのまま使用します。ただし、ファイルの存在チェックだけでは、しばらくアプリを実行していなかったユーザーは、最近更新されたパッケージに含まれているデータよりも古いデータでアプリを起動してしまいます。そこで、キャッシュ データベースのタイムスタンプがパッケージに含まれるデータベースのタイムスタンプよりも古いかどうかをチェックし、古ければ新しいコピーをキャッシュに上書きします。ただし、この機能は Altostratus には実装していません。Altostratus では、既存のデータベースのユーザー基本設定情報を保持する必要があります。

オフライン キャッシュとバックエンドとの同期: 繰り返しになりますが、Altostratus クライアントは常にキャッシュ データベースを操作します。バックエンドへのすべての Web 要求 (新しいユーザー基本設定のアップロードを除く) は、キャッシュを同期するというコンテキストで行われます。このメカニズムは DataModel クラスの一部として sync.cs の 4 つのメソッド、SyncSettings (model.cs の Configuration.ApplyBackendConfiguration にデリゲートします)、SyncCategories、SyncAuthProviders、および SyncItems に実装しています。この中で間違いなく中心となるのは SyncItems ですが、まずはこれらのメソッドをトリガーする要因について説明します。

Altostratus では、バックエンドからローカル キャッシュへの一方向のみにデータを同期します。また、対象となるデータはあまり変更されない (バックエンドの Web ジョブのスケジュールが前提) ため、検討するのは分単位や秒単位の更新ではなく、バックエンド データ ストアとの最終的な一貫性のみです。

各同期プロセスは基本的に同じです。バックエンドから最新データを取得し、ローカル データベースを新しい値に更新して、データベースから古い値を削除し、データ モデル オブジェクトにデータを再設定して UI の更新をトリガーします。

アイテムに関する SyncItems は UI から呼び出されるため、もう少し作業が必要です。つまり、興奮したユーザーがボタンを何度も押せないようにします。プライベート DataModel.syncTask プロパティは、アクティブなアイテムの同期があるかどうかを示します。つまり、SyncItems は、syncTask が Null 以外の場合は繰り返しの要求と見なし無視します。さらに、アイテムの要求は完了まで時間がかかる可能性があり、大規模なデータ セットが関係するため、デバイスがオフラインになったらアイテムの同期をキャンセルできるようにもします。そのため、タスクの System.Threading.CancellationToken を保存します。

プライベート SyncItemsCore メソッドがこのプロセスの心臓部です (図 6 参照)。ここでは、データベースから最終同期時のタイムスタンプを取得して Web 要求に含めます。

図 6 プライベート SyncItemsCore メソッド

private async Task<SyncResult> SyncItemsCore()
{
  SyncResult result = SyncResult.Success;
  HttpResponseMessage response;
  Timestamp t = await DataAccessLayer.Current.GetTimestampAsync();
  String newRequestTimestamp =
    DateTime.UtcNow.ToString(WebAPIConstants.ItemsFeedTimestampFormat);
  response = await WebAPI.GetItems(t, syncToken);
  if (!response.IsSuccessStatusCode)
  {
    return SyncResult.Failed;
  }
  t = new Timestamp() { Stamp = newRequestTimestamp };
  await DataAccessLayer.Current.SetTimestampAsync(t);
  if (response.StatusCode == System.Net.HttpStatusCode.NoContent)
  {
    return SyncResult.NoContent;
  }
  var items = await response.Content.ReadAsAsync<IEnumerable<FeedItem>>();
  await ProcessItems(items);
  // Sync is done, refresh the ListView data source.
  await PopulateGroupedItemsFromDB();
  return result;
}

この処理により、バックエンドは新しいアイテムまたは特定の時刻以降に更新されたアイテムのみを返します。その結果、クライアントは本当に必要なデータのみを取得するようになり、ユーザーが契約するデータ プランの上限を超えることはおそらくありません。同時に、クライアントが各要求のデータを処理する作業が少なくなり、バッテリが節約され、ネットワーク トラフィックが最小限に抑えられます。大したことではないようにも思えますが、たとえば、各要求でカテゴリごとに処理する会話数が 50 件から 5 件になれば、90 パーセントの削減です。これを毎日 20 ~ 30 回同期したら、1 つのアプリで 1 か月間にたやすく数百メガバイトに達します。つまり、間違いなく顧客は、トラフィックを最適化するために行った努力を評価してくれるでしょう。

要求が返されたら、ProcessItems メソッドがそれらすべてのアイテムをデータベースに追加します。その際、タイトルを少しクリーンアップし (引用符の削除など)、メイン リストに表示する説明の本文から先頭 100 文字を抽出します。タイトルのクリーンアップをバックエンドで行えば、クライアントでの処理時間が少し短くなるかもしれません。シナリオによってはプラットフォーム固有の調整が必要になることもあるので、このクリーンアップもクライアントに残しています。また、説明から先頭 100 文字を抽出するのもバックエンドで行えば、クライアントの作業が若干少なくなりますが、ネットワーク トラフィックが増加します。ここでのデータ操作については、おそらく他にもトレードオフがありますが、UI の操作は最終的にはクライアントの役割なので、こうした手順をクライアントで管理しています (この詳細と、UI に関するその他の考慮事項については、tinyurl.com/kboathaltoxtra (英語) のブログ記事を参照してください)。

アイテムをデータベースに追加したら、PopulateGroupedItemsFromDB を使用して、データ モデルのグループを更新します。ここで、データベースには、ユーザーの現在の会話数制限値の設定に必要な数よりも多くのアイテムが含まれている可能性があることを理解しておくことは重要です。PopulateGroupedItemsFromDB では、その制限値をデータベース クエリに直接適用することによってこれを考慮します。

しかし、二度と表示されない大量のアイテムを保持することで、時間の経過と共にデータベースのサイズを増加させたくはありません。このため、SyncItems では DataAccessLayer.ApplyConversationLimit メソッドを呼び出して、アイテム数が指定された制限値に達するまでデータベースから古いアイテムを間引きます。Altostratus データ セット内の個々のアイテム サイズは比較的小さいため、ユーザーの現在設定に関係なく、会話数制限値として 100 を使用します。こうすれば、ユーザーが制限値を上げても、バックエンドから再びデータを要求する必要はありません。ただし、所有しているデータ アイテムのサイズがかなり大きい場合、データベースを積極的に間引いて、必要に応じてアイテムを再び要求する方がよい場合もあります。

同期のトリガー: UI の [Refresh] (最新情報への更新) ボタンは、アイテムの同期を行う主なきっかけになることは当然ですが、他にも同期処理が行われるタイミングがあります。では、アイテムの同期をトリガーする要因にはどのようなものがあるでしょう。

Sync* メソッドへのすべての呼び出しが 1 つの場所 HomeViewModel.Sync メソッドで行われるため、コードからこの疑問の答えを見つけるのはちょっとやっかいです。ただし、このメソッドと、セカンダリ エントリ ポイント HomeViewModel.CheckTimeAndSync は、他のさまざまな場所から呼び出されます。Sync への呼び出しが SyncExtent 列挙体の値でパラメーター化されるタイミング、場所、方法を以下にまとめます。

  • 起動時は、HomeViewModel コンストラクターが Sync(SyncExtent.All) を呼び出しっぱなしにするためめ、同期は完全にバックグラウンドで行われます。つまり、async メソッドからの戻り値をローカル変数に保存し、await を使用しないことに関する警告をコンパイラーが表示しないようにします。
  • Connectivity プラグインの ConnectivityChanged イベントのハンドラー内部では、(要求されたときと同じ範囲を使用して) 前回の呼び出し時にデバイスがオフラインだった場合に Sync を呼び出します。
  • ユーザーが [Configuration] (構成) ページに移動し、アクティブなカテゴリや会話数制限値を変更した場合、またはバックエンドにログインしてバックエンドから設定を適用した場合、そのことが DataModel.Configuration.HasChanged フラグに記憶されます。ユーザーがホームページに戻ると、HomePage.OnAppearing ハンドラーが HomeViewModel.CheckRefresh を呼び出して、HasChanged をチェックし、必要に応じて Sync(SyncExtent.Items) を呼び出します。
  • App.OnResume イベント (app.cs) は CheckTimeAndSync を呼び出し、アプリの中断期間に基づいて同期すべき対象を判断するロジックを適用します。このような条件は、当然、データとバックエンド操作の性質に大きく変わります。
  • 最後に、[Refresh] (最新情報への更新) ボタンがフラグを添えて CheckTimeAndSync を呼び出し、必ず少なくとも 1 回はアイテムの同期を実行します。[Refresh] (最新情報への更新) ボタンが CheckTimeAndSync を使用するのは、ユーザーが 1 時間半以上、あるいは 1 日以上、フォアグラウンドでアプリを実行したままにする可能性があり (めったにありません)、その場合、再開時に [Refresh] (最新情報への更新) ボタンで他の同期も実行する必要があるためです。

すべてを HomeViewModel.Sync に統合するメリットは、適切なタイミングでパブリック HomeViewModel.IsSyncing プロパティを設定できる点にあります。このプロパティは、Home.xaml の Xamarin.Forms.ActivityIndicator の IsVisible プロパティと IsRunning プロパティの両方にデータ バインドします。このフラグを設定するだけで、そのインジケーターの表示/非表示を制御します。

TFS と Visual Studio Online での Xamarin を使用したビルド

Altostratus プロジェクトでは、クロスプラットフォームの作業に比較的一般的な開発環境として、Android と Windows Phone 用にエミュレーターとテザリング デバイスを備えた Windows PC と、iOS シミュレーターとテザリング iOS デバイスを備えたローカル Mac OS X コンピューターを使用しました (図 7 参照)。このようにセットアップすると、すべての開発とデバッグの作業を PC 上の Visual Studio 内で直接行い、リモート iOS のビルドとデバッグには Mac OS X コンピューターを使用できます。ストアへの提出準備が整った iOS アプリを Mac から送信することもできます。

Xamarin プロジェクト用の一般的なクロスプラットフォーム開発環境と、Visual Studio Tools for Apache Cordova などの他のテクノロジを利用する開発環境
図 7 Xamarin プロジェクト用の一般的なクロスプラットフォーム開発環境と、Visual Studio Tools for Apache Cordova などの他のテクノロジを利用する開発環境

今回は、チームの共同作業とソース管理に Visual Studio Online を採用し、バックエンドと Xamarin クライアントの両方に対して継続的統合ビルドを実行するように構成しました。このプロジェクトは開始したばかりで、最新の Visual Studio Online ビルド システムを使用して Xamarin アプリをホスト型ビルド コントローラーで直接ビルドしています。詳細については、ブログ記事 (tinyurl.com/kboauthxamvso、英語) を参照してください。ただし、2015 年の前半は、まだホスト型ビルド コントローラーがこれをサポートしていませんでした。さいわい、Visual Studio Online のビルド コントローラーとして、TFS を実行しているローカル コンピューターを使用するだけで済みます。そのサーバーに、Xamarin と必要な Android と Windows の Platform SDK が含まれた無料の TFS Express エディションをインストールし、ビルド アカウントがアクセスできる c:\android-sdk のような場所に Android SDK を配置しました (そのインストーラーの既定では、ビルド アカウントがアクセス許可を持っていない現在のユーザーの記憶域に SDK を配置します)。これについては、Xamarin ドキュメント「Xamarin に対する Team Foundation Server の構成」(bit.ly/1OhQPSW、英語) で説明されています。

ビルド サーバーを完全に構成したら、次は Visual Studio Online に接続します (https://msdn.microsoft.com/ja-jp/library/ms181712.aspx の「ビルド サーバーの配置および構成」を参照してください)。

  1. TFS 管理コンソールを開きます。
  2. 左側のナビゲーション ウィンドウで、サーバー名を展開し、[ビルド構成] を選択します。
  3. [ビルド サービス] にある [プロパティ] をクリックして、[ビルド サービスのプロパティ] ダイアログ ボックスを開きます。
  4. ダイアログの上部にある [サービスの停止] をクリックします。
  5. [通信] の [プロジェクト コレクションのビルド サービスを指定してください] の下にあるボックスに、Visual Studio Online コレクションの URL を入力します。たとえば、「https://<your account>.visualstudio.com/defaultcollection」と入力します。
  6. ダイアログ ボックス下部にある [開始] ボタンをクリックして、サービスを再起動します。

これだけです。Visual Studio のチーム エクスプローラーでビルド定義を作成すると、使用可能なビルド コントローラーの一覧に、Visual Studio Online に接続されている TFS コンピューターが表示されます。そのオプションを選択すると、Visual Studio からキューに登録したビルド、またはチェックイン時にキューに登録されたビルドが TFS コンピューターにルーティングされます。

まとめ

Altostratus プロジェクトを紹介しましたが、役に立ったでしょうか。クラウド接続型のモバイル アプリに役立つコードは見つかりましたか。このプロジェクトの目標は、クライアントがクライアント上で直接実行する必要がある作業を最適化する代わりに、重要な作業を実行できるカスタム バックエンドを使用するクロスプラットフォーム モバイル アプリのわかりやすい例を提供することでした。常時実行されているバックエンドがすべてのクライアントに代わってデータを収集することで、クライアントとのネットワーク トラフィックが大幅に削減されます (データ プランへの影響も少なくなります)。さまざまなソースからのデータを正規化することで、クライアントの処理が必要なデータの量を最小限に抑えることができ、これにより貴重なバッテリを節約できます。バックエンドでユーザーを認証するようにして、ユーザーの基本設定をバックエンドに格納し、クライアントのバックエンドとの対話、ネットワーク トラフィックの最適化、要件の処理にこの基本設定を自動適用するしくみを示しました。特定の要件では、もっと簡単に同じ効果を得ることができる方法があることはわかていますが、より複雑なシナリオに拡張できる例が必要でした。

このプロジェクトに関するご意見やご感想をぜひお寄せください。

Azure のオフライン同期

独自のオフライン キャッシュを実装する代わりに、Microsoft Azure Mobile Apps のテーブル用オフライン同期を利用できます。これにより、同期コードをまったく記述する必要なく、クライアントからサーバーに変更をプッシュできます。ただし、テーブル ストレージを使用するため、SQLite とは異なりリレーショナル データ モデルは提供されません。


Kraig Brockschmidt は、マイクロソフトのシニア コンテンツ開発者であり、クロス プラットフォーム モバイル アプリを担当しています。『HTML、CSS、JavaScript によるプログラミング Windows ストアアプリ』(日経 BP 社、2013 年) の著者であり、kraigbrockschmidt.com (英語) でブログも執筆しています。

Mike Wasson は、マイクロソフトのコンテンツ開発者です。長年の間、Win32 マルチメディア API のドキュメントを作成してきました。現在は、Microsoft Azure と ASP.NET に関する記事を執筆しています。

Rick Anderson は、マイクロソフトのシニア プログラミング ライターであり、ASP.NET MVC、Microsoft Azure、および Entity Framework を担当しています。Twitter は、twitter.com/RickAndMSFT (英語) からフォローできます。

Erik Reitan は、マイクロソフトのシニア コンテンツ開発者であり、Microsoft Azure と ASP.NET を担当しています。Twitter は、twitter.com/ReitanErik (英語) からフォローできます。

Tom Dykstra は、マイクロソフトのシニア コンテンツ開発者であり、Microsoft Azure と ASP.NET を担当しています。

この記事のレビューに協力してくれた技術スタッフの Michael Collier、Brady Gaster、John de Havilland、Ryan Jones、Vijay Ramakrishnan、および Pranav Rastogi に心より感謝いたします。