Xamarin逆引きTips
MvvmCrossでAndroidの画面の再生成に対応するには?
Androidアプリでは別アプリ移動時に画面が破棄され、アプリ再表示時に画面が復元される場合がある。この画面の再生成を、MvxViewModelのライフサイクルメソッドにより行う方法を説明する。
Androidでは、実行しているアプリを切り替えた場合、それまで実行中であったアプリの画面やアプリ自体のプロセスを破棄してメモリを確保することがある。破棄されたアプリ画面は、別のアプリからタスクスイッチやバックキーなどで戻ってきたときに再生成される仕組みなので、画面を離れる際に元の状態を保存しておき、画面を復元できるようにしておくことが必要となる。
MvvmCrossを使ったAndroidアプリ開発では、プラットフォームごとに発生する画面の再生成に対して*1、MvxViewModel
クラスに用意された機能で対応できるようになっている。今回は、そのMvxViewModel
クラスに用意されたライフサイクルメソッドを使用して、画面の再生成に備える方法について解説する*2。
- *1 MvvmCrossが対応しているプラットフォームの中では、Windows Phoneでも同様の実装が必要になる。なお、iOSでは別のアプリに移動してメモリが不足した場合、バックグラウンドのアプリは終了し、再度、アプリが通常起動する。
- *2 なお、本TipsはMac OS X(10.10.3)、Xamarin Studio(5.8.3)、MvvmCross(3.5.0)で動作を確認している(※編集部注: Windows上のVisual Studioでも同様の手順で、本稿の内容が実現できることは確認している)。
MvxViewModelのライフサイクル
MvxViewModel
は通常、MvxViewController
(iOS)やMvxActivity
(Android)といったViewの生成時に、自動的にインスタンス化されるが、その際、図1の流れで処理が呼ばれていく。
まずは、通常の起動処理を見ていこう。
画面が生成されてViewModelが生成される時には、通常のクラスと同じようにコンストラクターが呼ばれる(1)。ここでは主にIoCコンテナー(DIコンテナー)に入っているプラグインやServiceのオブジェクトを取得する操作を記述する。
次に、Init
メソッド(もしくはInitFromBundle
メソッド)が呼ばれる(2)。これは「Tips:MvvmCrossで画面遷移するには?」でも紹介した通り、画面遷移時に渡されたパラメーターを受け取ることができるメソッドだ。また、全般的な初期化もこのタイミングで行える。
その後、Start
メソッドが呼び出され(3)、表示に必要なデータがそろった状態で画面表示の準備処理を行うことができる。
Start
メソッドを抜けた後は、実際の画面表示に向けて、各プラットフォームのライフサイクルイベントが発生していき、実際に画面が表示される。各プラットフォームのライフサイクルイベントに対応する処理はMvxViewModel
クラスには実装がないが、コマンドバインディングでイベントをキャッチすることが可能だ。この点については「Tips:MvvmCrossでコマンドバインディングするには?」の「【コラム】UIViewController(iOS)やActivity(Android)のライフサイクルを監視する」を参照してほしい。
その後、別の画面に遷移する場合など、状態の保存が必要になったタイミングでSaveState
メソッド(もしくはSaveStateToBundle
メソッド)が呼び出される(4)。ここでは画面に持っている状態をオブジェクトに保存して返す。すると、MvvmCrossは各プラットフォーム固有の情報保存処理に対してオブジェクトのデータを保存する。
別のアプリに移ったりメモリが不足したりするなどして画面が破棄された後に画面に戻ってきた場合は、再度、画面生成が行われ、ViewModelも「コンストラクター」「Init
メソッド」と順番に呼び出されていくが、Init
メソッドとStart
メソッドの間でReloadState
メソッド(もしくはReloadStateFromBundle
メソッド)が呼び出される(5)。このメソッドにSaveState
メソッドにより保存された値(=オブジェクトのデータ)が入っているので、この値を使ってViewModelの状態を復元する。
サンプルで動きを確認する
では、実際にサンプルを使って動きを確認してみよう。
プロジェクトの作成
「Tips:MvvmCrossのプロジェクトをセットアップするには?」の手順に従い、MvvmCrossプロジェクトを作成する。ソリューション名は「CrossStateSample」と設定する。なお、今回はAndroidでの動きを見るのが主目的のため、iOS用のTouchプロジェクトは作成しなかった。
その後、「Tips:MvvmCrossで画面遷移するには?」の「画面追加と画面遷移の実装」を参考にSecondViewModel
とSecondView
を作成し、FirstViewMode
とFirstView
を変更していく。その手順は以下の通りだ。
まず、CrossStateSample.CoreプロジェクトのViewModelフォルダーにSecondViewModel.csを追加し、次のように編集する。
using System;
using Cirrious.MvvmCross.ViewModels;
namespace CrossStateSample.Core.ViewModels
{
public class SecondViewModel : MvxViewModel
{
public IMvxCommand BackCommand {
get {
return _backCommand ?? (_backCommand =
new MvxCommand(() => Close(this)));
}
}
IMvxCommand _backCommand;
}
}
|
FirstViewModelはデフォルトのHello
プロパティを残した状態でGoToSecondCommand
プロパティを追加する。具体的には次のように編集する。
using Cirrious.MvvmCross.ViewModels;
namespace CrossStateSample.Core.ViewModels
{
public class FirstViewModel : MvxViewModel
{
private string _hello = "Hello MvvmCross";
public string Hello
{
get { return _hello; }
set { _hello = value; RaisePropertyChanged(() => Hello); }
}
// -- ▼▼ ここから追加 ▼▼ --
public IMvxCommand GoToSecondCommand {
get {
return _goToSecondCommand ?? (_goToSecondCommand =
new MvxCommand(() => ShowViewModel<SecondViewModel>()));
}
}
private IMvxCommand _goToSecondCommand;
// -- ▲▲ ここまで追加 ▲▲ --
}
}
|
次に、CrossStateSample.DroidプロジェクトのResources
-layout
フォルダーにあるFirstView.axmlファイルを開き、次のように編集する。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:local="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<EditText
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textSize="40dp"
local:MvxBind="Text Hello" />
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textSize="40dp"
local:MvxBind="Text Hello" />
<!-- ▼▼ ここから追加 ▼▼ -->
<Button
android:text="Go to SecondView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
local:MvxBind="Click GoToSecondCommand" />
<!-- ▲▲ ここまで追加 ▲▲ -->
</LinearLayout>
|
デフォルトのものからButtonを追加している。
さらに、SecondView.axmlファイルを追加し、次のように編集する。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:local="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:text="Back to FirstView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
local:MvxBind="Click BackCommand" />
</LinearLayout>
|
FirstViewに戻るボタンを配置する。
最後にSecondView.axmlファイルに対応するViewクラスを作成する。Views
フォルダーにSecondView.csファイルを追加し、次のように編集する。
using Android.App;
using Android.OS;
using Cirrious.MvvmCross.Droid.Views;
namespace CrossStateSample.Droid.Views
{
[Activity(Label = "SecondView")]
public class SecondView : MvxActivity
{
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
SetContentView(Resource.Layout.SecondView);
}
}
}
|
この状態で実行すると、テンプレートのHello
変数に対してバインディングされたテキストと、SecondViewへの遷移ボタンがある状態で起動する。表示されているテキストを編集し、[Go to SecondView]ボタンをタップしてSecondViewに移動した後、FirstViewに戻った際も表示されているテキストは保持されていることが分かる。また、ホーム画面に戻ったりしても保持されている(図2)。具体的な対応はなされていないものの、まだアプリはメモリ内にあり、Viewやプロセスが破棄されていないため、状態が保持されていることとなる。
状態保存のテストのためにAndroidの設定を変更する
アクティビティ(View)の破棄やプロセスの停止は、アプリをバックグラウンドにした状態で他のアプリで大量のメモリを使用したり、アクティビティの遷移階層を深くしたりすることで発生する。しかし現在流通している端末はスペックが上がっており、アクティビティ破棄に至るまでには動画再生やゲームなど重いタスクのアプリを複数本起動することを繰り返す必要がある。デバッグのためにこれらの操作をするのはかなりの手間となってしまう。
デバッグの効率化を目的として、Androidの開発者オプションにはアクティビティ破棄やプロセス停止を任意で発生させることができる設定が用意されている。端末の設定アプリで[開発者向けオプション](Developer options)を開き、[アクティビティを保持しない](Don't keep activities)にチェックを入れたり、[バックグラウンドプロセスの上限](Background process limit)を[バックグラウンドプロセスを使用しない](No background processes)などに切り替えたりする(図3)。
[アクティビティを保持しない]や[バックグラウンドプロセスの上限]の値を変更すると、画面破棄のテストが簡単にできる(この画面は「Xperia Z3 Compact SO-02G」のもの)。
[アクティビティを保持しない]のオプションを使用すると、ホーム画面に戻るなどユーザーがアプリから離れた際、表示されていなかったアクティビティが破棄される。
さらに[バックグラウンドプロセスの上限]を[バックグラウンドプロセスを使用しない]などにすると、ユーザーがアプリから離れた瞬間にプロセスが停止する。
両者の差は、アクティビティ以外のオブジェクトに影響があるかどうかである。アクティビティの破棄はViewとViewModelにのみ影響し、それ以外の状態はメモリに保持される。しかしプロセスの停止はMvvmCrossの内部クラスやプラグイン、ServiceクラスなどのIoCコンテナーに入っているオブジェクトなども含めて初期化される。テストの際はどちらのパターンでも正常に復帰できることを確認することが重要となる。
実際に[アクティビティを保持しない]の設定を行った端末で先ほどのサンプルを確認する(図4)。FirstViewに表示されたテキストを編集し、[Go to SecondView]をタップしてSecondViewを開いた状態で端末のホーム画面に戻る。そしてタスクスイッチからアプリを戻り、[Back to FirstView]をタップしてFirstViewに戻ると、アクティビティが破棄されるためテキストがデフォルトの「Hello MvvmCross」に戻っていることが分かる。
なお、[アクティビティを保持しない]や[バックグラウンドプロセスの上限]の設定が有効な場合、アプリによっては復帰に時間がかかる場合や一部情報が失われるなど、実際の利用には不向きなため、確認が終わったら元の設定に戻すことを強く推奨する。
状態を保存し、保存した状態から画面を復元する
状態を保存するためには、保存する値を保持するためのクラスを用意し、状態を保存したそのクラスのインスタンスを返すSaveState
メソッドを実装し、状態を復元するためのReloadState
メソッドを実装する。
FirstViewModel.csファイルを開き、次のように編集する。
using Cirrious.MvvmCross.ViewModels;
namespace CrossStateSample.Core.ViewModels
{
public class FirstViewModel : MvxViewModel
{
private string _hello = "Hello MvvmCross";
public string Hello
{
get { return _hello; }
set { _hello = value; RaisePropertyChanged(() => Hello); }
}
public IMvxCommand GoToSecondCommand {
get {
var b = new MvxBundle();
b.Write("hoge");
return _goToSecondCommand ?? (_goToSecondCommand =
new MvxCommand(() => ShowViewModel<SecondViewModel>()));
}
}
private IMvxCommand _goToSecondCommand;
// -- ▼▼ ここから追加 ▼▼ --
// 1
public class SavedState
{
public string Hello { get; set; }
}
// 2
public SavedState SaveState()
{
return new SavedState { Hello = Hello };
}
// 3
public void ReloadState(SavedState state)
{
Hello = state.Hello;
}
// -- ▲▲ ここまで追加 ▲▲ --
}
}
|
まず、保存する値を保持するSavedState
クラスを用意する(1)。状態保存が必要になったタイミングでSaveState
メソッドが呼ばれるので、SavedState
クラスのインスタンスを作成して保存する値を格納する(2)。Viewが再生成されると、ReloadState
メソッドが呼ばれるので(3)、ここで引数に渡されたSavedState
オブジェクトから必要なデータを復元する。
この状態でアプリを実行すると、先ほど行ったView破棄の操作でも入力された内容が保持されていることが分かる。
なお、状態保存に使用するクラスに使用できる型だが、「Tips:MvvmCrossで画面遷移するには?」の「パラメーターの型について」で記載したものと同じ制限を受ける。
【コラム】InitFromBundle/SaveStateToBundle/ReloadStateFromBundle メソッドを使用する
「Tips:MvvmCrossで画面遷移するには?」で紹介したInit
メソッドと共に、SaveState
メソッド、ReloadState
メソッドは継承関係ではなく独自の定義を行っている。これはMvvmCrossがリフレクションでこれらのメソッドを検索しているためだ。どのメソッドも実際には値をMvxBundle
クラスのオブジェクトから値を読み込んで引数として与えたり、戻り値をMvxBundle
オブジェクトに書き込んだりしている(利用できる型の制限が共通なのは、共通のMvxBundle
クラスを使用しているからである)。
独自にメソッドを定義する以外に、MvxViewModel
クラスには、InitFromBundle
/SaveStateToBundle
/ReloadStateFromBundle
メソッドなどがすでに用意されていて、これらをオーバーライドして用いることができる。
MvxBundle
クラスは、クラス内のプロパティをリフレクションで取得して格納するWrite
/Read
メソッドの他に、Data
というDictionary<string, string>
型のプロパティを持っており、これを直接取り扱うことも可能だ。例えば今回のサンプルで、SavedState
クラスやSaveState
メソッド、ReloadState
メソッドを用いずに、次のように書くこともできる。
protected override void SaveStateToBundle(IMvxBundle bundle)
{
bundle.Data["Hello"] = Hello;
}
protected override void ReloadFromBundle(IMvxBundle state)
{
Hello = state.Data["Hello"];
}
|
MvxBundle
を直接操作するので処理の記述は手間となるが、複雑なデータ型を保存する場合や独自にシリアライズ機能を持っている型を扱う場合などは選択するとよいだろう。また、メソッドはIDEの入力補完で入力できるので、その点を魅力に感じる人もいるだろう。
※以下では、本稿の前後を合わせて5回分(第47回~第51回)のみ表示しています。
連載の全タイトルを参照するには、[この記事の連載 INDEX]を参照してください。
48. Xamarin.Formsでプラットフォームごとの微調整を行うには?
カスタムレンダラーやDependencyServiceの仕組みを使わず、Deviceクラスを利用してプラットフォーム間で異なる部分を微調整する方法を説明する。
49. 【現在、表示中】≫ MvvmCrossでAndroidの画面の再生成に対応するには?
Androidアプリでは別アプリ移動時に画面が破棄され、アプリ再表示時に画面が復元される場合がある。この画面の再生成を、MvxViewModelのライフサイクルメソッドにより行う方法を説明する。
50. Xamarin.Formsでローカルデータベースを使用するには?
アプリを終了して再起動したときに、ユーザーデータを復活させたい場合、ローカルやクラウドにデータを保存することになる。その一つの方法として、SQLite.Netを使ってローカルDBに保存する方法を説明する。
51. MvvmCrossでカスタムコンバーターを作成するには?
MvvmCrossでのiOS/Androidアプリ開発において、バインディングする値を変換できるカスタムコンバーターの使い方を説明する。