Deep Insider の Tutor コーナー
>>  Deep Insider は本サイトからスピンオフした姉妹サイトです。よろしく! 
Xamarin逆引きTips

Xamarin逆引きTips

MvvmCrossでAndroidの画面の再生成に対応するには?

2015年5月11日

Androidアプリでは別アプリ移動時に画面が破棄され、アプリ再表示時に画面が復元される場合がある。この画面の再生成を、MvxViewModelのライフサイクルメソッドにより行う方法を説明する。

  • このエントリーをはてなブックマークに追加

 Androidでは、実行しているアプリを切り替えた場合、それまで実行中であったアプリの画面やアプリ自体のプロセスを破棄してメモリを確保することがある。破棄されたアプリ画面は、別のアプリからタスクスイッチやバックキーなどで戻ってきたときに再生成される仕組みなので、画面を離れる際に元の状態を保存しておき、画面を復元できるようにしておくことが必要となる。

 MvvmCrossを使ったAndroidアプリ開発では、プラットフォームごとに発生する画面の再生成に対して*1MvxViewModelクラスに用意された機能で対応できるようになっている。今回は、その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の流れで処理が呼ばれていく。

図1 MvxViewModelのライフサイクル

 まずは、通常の起動処理を見ていこう。

 画面が生成されて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で画面遷移するには?」の「画面追加と画面遷移の実装」を参考にSecondViewModelSecondViewを作成し、FirstViewModeFirstViewを変更していく。その手順は以下の通りだ。

 まず、CrossStateSample.CoreプロジェクトのViewModelフォルダーにSecondViewModel.csを追加し、次のように編集する。

C#
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;
  }
}
BackCommandを実装する(SecondViewModel.cs)

 FirstViewModelはデフォルトのHelloプロパティを残した状態でGoToSecondCommandプロパティを追加する。具体的には次のように編集する。

C#
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;

    // -- ▲▲ ここまで追加 ▲▲ --
  }
}
GoToSecondCommandを実装する(FirstViewModel.cs)

 次に、CrossStateSample.DroidプロジェクトのResourceslayoutフォルダーにあるFirstView.axmlファイルを開き、次のように編集する。

XML
<?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>
レイアウトの編集(SecondView.axml)

デフォルトのものからButtonを追加している。

 さらに、SecondView.axmlファイルを追加し、次のように編集する。

XML
<?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>
レイアウトの編集(SecondView.axml)

FirstViewに戻るボタンを配置する。

 最後にSecondView.axmlファイルに対応するViewクラスを作成する。ViewsフォルダーにSecondView.csファイルを追加し、次のように編集する。

C#
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);
    }
  }
}
Viewクラスの実装(SecondView.cs)

 この状態で実行すると、テンプレートのHello変数に対してバインディングされたテキストと、SecondViewへの遷移ボタンがある状態で起動する。表示されているテキストを編集し、[Go to SecondView]ボタンをタップしてSecondViewに移動した後、FirstViewに戻った際も表示されているテキストは保持されていることが分かる。また、ホーム画面に戻ったりしても保持されている(図2)。具体的な対応はなされていないものの、まだアプリはメモリ内にあり、Viewやプロセスが破棄されていないため、状態が保持されていることとなる。

図2 サンプルの画面遷移

メモリが十分にあれば未対策でもViewModelの値は保持される。

状態保存のテストのためにAndroidの設定を変更する

 アクティビティ(View)の破棄やプロセスの停止は、アプリをバックグラウンドにした状態で他のアプリで大量のメモリを使用したり、アクティビティの遷移階層を深くしたりすることで発生する。しかし現在流通している端末はスペックが上がっており、アクティビティ破棄に至るまでには動画再生やゲームなど重いタスクのアプリを複数本起動することを繰り返す必要がある。デバッグのためにこれらの操作をするのはかなりの手間となってしまう。

 デバッグの効率化を目的として、Androidの開発者オプションにはアクティビティ破棄やプロセス停止を任意で発生させることができる設定が用意されている。端末の設定アプリで[開発者向けオプション](Developer options)を開き、[アクティビティを保持しない](Don't keep activities)にチェックを入れたり、[バックグラウンドプロセスの上限](Background process limit)を[バックグラウンドプロセスを使用しない](No background processes)などに切り替えたりする(図3)。

図3 開発者向けオプション

[アクティビティを保持しない]や[バックグラウンドプロセスの上限]の値を変更すると、画面破棄のテストが簡単にできる(この画面は「Xperia Z3 Compact SO-02G」のもの)。

 [アクティビティを保持しない]のオプションを使用すると、ホーム画面に戻るなどユーザーがアプリから離れた際、表示されていなかったアクティビティが破棄される。

 さらに[バックグラウンドプロセスの上限]を[バックグラウンドプロセスを使用しない]などにすると、ユーザーがアプリから離れた瞬間にプロセスが停止する。

 両者の差は、アクティビティ以外のオブジェクトに影響があるかどうかである。アクティビティの破棄はViewとViewModelにのみ影響し、それ以外の状態はメモリに保持される。しかしプロセスの停止はMvvmCrossの内部クラスやプラグイン、ServiceクラスなどのIoCコンテナーに入っているオブジェクトなども含めて初期化される。テストの際はどちらのパターンでも正常に復帰できることを確認することが重要となる。

 実際に[アクティビティを保持しない]の設定を行った端末で先ほどのサンプルを確認する(図4)。FirstViewに表示されたテキストを編集し、[Go to SecondView]をタップしてSecondViewを開いた状態で端末のホーム画面に戻る。そしてタスクスイッチからアプリを戻り、[Back to FirstView]をタップしてFirstViewに戻ると、アクティビティが破棄されるためテキストがデフォルトの「Hello MvvmCross」に戻っていることが分かる。

図4 先ほどのサンプルを、[アクティビティを保持しない]設定で動かすと、入力されたテキストが初期化されてしまっていることが分かる

 なお、[アクティビティを保持しない]や[バックグラウンドプロセスの上限]の設定が有効な場合、アプリによっては復帰に時間がかかる場合や一部情報が失われるなど、実際の利用には不向きなため、確認が終わったら元の設定に戻すことを強く推奨する。

状態を保存し、保存した状態から画面を復元する

 状態を保存するためには、保存する値を保持するためのクラスを用意し、状態を保存したそのクラスのインスタンスを返すSaveStateメソッドを実装し、状態を復元するためのReloadStateメソッドを実装する。

 FirstViewModel.csファイルを開き、次のように編集する。

C#
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;
    }
    // -- ▲▲ ここまで追加 ▲▲ --
  }
}
画面状態の保存・復元処理を追加する(FirstViewModel.cs)

 まず、保存する値を保持するSavedStateクラスを用意する(1)。状態保存が必要になったタイミングでSaveStateメソッドが呼ばれるので、SavedStateクラスのインスタンスを作成して保存する値を格納する(2)。Viewが再生成されると、ReloadStateメソッドが呼ばれるので(3)、ここで引数に渡されたSavedStateオブジェクトから必要なデータを復元する。

 この状態でアプリを実行すると、先ほど行ったView破棄の操作でも入力された内容が保持されていることが分かる。

図5 SaveState/ReloadStateメソッドを実装することで、[アクティビティを保持しない]設定でも値が保持されていることが分かる

 なお、状態保存に使用するクラスに使用できる型だが、「Tips:MvvmCrossで画面遷移するには?」の「パラメーターの型について」で記載したものと同じ制限を受ける。

【コラム】InitFromBundle/SaveStateToBundle/ReloadStateFromBundle メソッドを使用する

 「Tips:MvvmCrossで画面遷移するには?」で紹介したInitメソッドと共に、SaveStateメソッド、ReloadStateメソッドは継承関係ではなく独自の定義を行っている。これはMvvmCrossがリフレクションでこれらのメソッドを検索しているためだ。どのメソッドも実際には値をMvxBundleクラスのオブジェクトから値を読み込んで引数として与えたり、戻り値をMvxBundleオブジェクトに書き込んだりしている(利用できる型の制限が共通なのは、共通のMvxBundleクラスを使用しているからである)。

 独自にメソッドを定義する以外に、MvxViewModelクラスには、InitFromBundleSaveStateToBundleReloadStateFromBundleメソッドなどがすでに用意されていて、これらをオーバーライドして用いることができる。

 MvxBundleクラスは、クラス内のプロパティをリフレクションで取得して格納するWriteReadメソッドの他に、DataというDictionary<string, string>型のプロパティを持っており、これを直接取り扱うことも可能だ。例えば今回のサンプルで、SavedStateクラスやSaveStateメソッド、ReloadStateメソッドを用いずに、次のように書くこともできる。

C#
protected override void SaveStateToBundle(IMvxBundle bundle)
{
  bundle.Data["Hello"] = Hello;
}

protected override void ReloadFromBundle(IMvxBundle state)
{
  Hello = state.Data["Hello"];
}
状態を保持するクラス(今回の例ではSavedStateクラス)を使う代わりに、MvxBundleクラスのDataプロパティを直接使用する場合のコード例

 MvxBundleを直接操作するので処理の記述は手間となるが、複雑なデータ型を保存する場合や独自にシリアライズ機能を持っている型を扱う場合などは選択するとよいだろう。また、メソッドはIDEの入力補完で入力できるので、その点を魅力に感じる人もいるだろう。

※以下では、本稿の前後を合わせて5回分(第47回~第51回)のみ表示しています。
 連載の全タイトルを参照するには、[この記事の連載 INDEX]を参照してください。

Xamarin逆引きTips
47. MvvmCrossで画面遷移するには?

MvvmCrossでiOS/Androidアプリの画面遷移をするための基本的な実装方法を説明する。

Xamarin逆引きTips
48. Xamarin.Formsでプラットフォームごとの微調整を行うには?

カスタムレンダラーやDependencyServiceの仕組みを使わず、Deviceクラスを利用してプラットフォーム間で異なる部分を微調整する方法を説明する。

Xamarin逆引きTips
49. 【現在、表示中】≫ MvvmCrossでAndroidの画面の再生成に対応するには?

Androidアプリでは別アプリ移動時に画面が破棄され、アプリ再表示時に画面が復元される場合がある。この画面の再生成を、MvxViewModelのライフサイクルメソッドにより行う方法を説明する。

Xamarin逆引きTips
50. Xamarin.Formsでローカルデータベースを使用するには?

アプリを終了して再起動したときに、ユーザーデータを復活させたい場合、ローカルやクラウドにデータを保存することになる。その一つの方法として、SQLite.Netを使ってローカルDBに保存する方法を説明する。

Xamarin逆引きTips
51. MvvmCrossでカスタムコンバーターを作成するには?

MvvmCrossでのiOS/Androidアプリ開発において、バインディングする値を変換できるカスタムコンバーターの使い方を説明する。

サイトからのお知らせ

Twitterでつぶやこう!