読者です 読者をやめる 読者になる 読者になる

ぴよぴよエンジニアの日記

ぴよぴよエンジニアの日記です 技術系のことや日常のことをつぶやきます

Xamarin.Forms におけるマルチウィンドウの落とし穴

Xamarin C# Android

本エントリーは [初心者さん・学生さん大歓迎!] Xamarin その2 Advent Calendar 2016 - Qiita の11日目になります.

ゆるふわ枠ということでお手柔らかにお願い致します.



Android N からディスプレイを2つに分割してアプリを表示できるマルチウィンドウという機能が加わりました.

developer.android.com

タブレットなどの大画面ディスプレイには非常に嬉しい機能ですね.

しかしこのマルチウィンドウ、実はけっこう難物だったりします.


qiita.com

  1. 他のアプリからstartActivityForResultで起動する可能性がある場合、マルチウインドウに対応する必要がある
  1. onStopでUIの更新を止めるようにする

Xamarin.Forms に関係しそうなのはこの2つでしょうか.


1つ目に関しては、

android:resizeableActivity="false"にしようが、画面回転に対応させないようにしようが、マルチウインドウ中のActivityからstartActivityForResultで起動されたActivityはマルチウインドウで表示(画面分割された状態で表示)されます。

と、あるので 暗黙的インテントで起動する場合はどうしようもないですね.
大人しくマルチウィンドウ対応をするしかなさそうです.


2つ目が本エントリーの主題です.

今回はマルチウィンドウ時のライフサイクルを確認した上で、どんな対策があるかを考えます.





マルチウィンドウに対応・非対応させる

[Activity(ResizeableActivity = true/false)]

上記アノテーションを加えることでマルチウィンドウへの対応・非対応を記述できます.


f:id:Santea:20161207160425j:plain

結果はこのようになります.

false を指定すればマルチウィンドウでアプリを開くことはできません.



ライフサイクル

まずはライフサイクルを確認するために、Acitivity, Application, ContentPage にデバッグ出力を実装します.


MainActivity.cs

using System;

using Android.App;
using Android.Content.PM;
using Android.OS;

namespace MultiWindowSample.Droid
{
    [Activity(Label = "MultiWindowSample", 
        Icon = "@drawable/icon", 
        Theme = "@style/MainTheme",
        MainLauncher = true, 
        ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation,
        ResizeableActivity = true)]
    public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
    {
        protected override void OnCreate(Bundle bundle)
        {
            TabLayoutResource = Resource.Layout.Tabbar;
            ToolbarResource = Resource.Layout.Toolbar;

            base.OnCreate(bundle);
            Console.WriteLine("Activity.OnCreate");

            global::Xamarin.Forms.Forms.Init(this, bundle);
            LoadApplication(new App());
        }

        protected override void OnStart()
        {
            base.OnStart();
            Console.WriteLine("Activity.OnStart");
        }

        protected override void OnResume()
        {
            base.OnResume();
            Console.WriteLine("Activity.OnResume");
        }

        protected override void OnPause()
        {
            base.OnPause();
            Console.WriteLine("Activity.OnStart");
        }

        protected override void OnStop()
        {
            base.OnStop();
            Console.WriteLine("Activity.OnStop");
        }

        protected override void OnDestroy()
        {
            base.OnDestroy();
            Console.WriteLine("Activity.OnDestroy");
        }
    }
}


App.xaml.cs

using System.Diagnostics;

using Xamarin.Forms;

namespace MultiWindowSample
{
    public partial class App : Application
    {
        public App()
        {
            InitializeComponent();
            MainPage = new NavigationPage(new FirstPage());
        }

        protected override void OnStart()
        {
            // Handle when your app starts
            Debug.WriteLine("Application.OnStart");
        }

        protected override void OnSleep()
        {
            // Handle when your app sleeps
            Debug.WriteLine("Application.OnSleep");
        }

        protected override void OnResume()
        {
            // Handle when your app resumes
            Debug.WriteLine("Application.OnResume");
        }
    }
}


FirstPage.xaml.cs

using System;
using System.Diagnostics;
using Xamarin.Forms;

namespace MultiWindowSample
{
    public partial class FirstPage : ContentPage
    {
        public FirstPage()
        {
            InitializeComponent();
        }

        protected override void OnAppearing()
        {
            base.OnAppearing();
            Debug.WriteLine("Page.OnAppearing");
        }

        protected override void OnDisappearing()
        {
            base.OnDisappearing();
            Debug.WriteLine("Page.OnDisappearing");
        }

        private void Button_OnClicked(object sender, EventArgs e)
        {
            Navigation.PushAsync(new SecondPage());
        }
    }
}



フルスクリーンからマルチウィンドウ

まずは、フルスクリーン表示からマルチウィンドウ表示への遷移です.

Page.OnDisappearing
Activity.OnPause
Application.OnSleep
Activity.OnStop
Activity.OnDestroy
Activity.OnCreate
Page.OnAppearing
Application.OnStart
Activity.OnStart
Activity.OnResume
ContentPage.OnDisappearing
Activity.OnPause

Activity.OnDestroy がコールされ、一度 Activity が破棄されています!

その後 Activity.OnCreate で再度 Activity が生成され、一度 Activity.OnResume で前面に表示されます(このとき Page.OnAppearing も同時にコールされています).

最後にもう一つのアプリにフォーカスが移るため Activity.OnPause がコールされています(このときも Page.OnDisappearing が同時にコールされています).


問題点としては、

  1. OnDestroy → OnCreate の流れでアプリが初期状態になる
  2. Page.OnDisappearing がコールされるが、実際には画面が表示されている


1 については、具体的に以下のような現象が発生します.

f:id:Santea:20161207171456g:plain:w360

OnCreate でナビゲーションスタックが初期状態に戻されてしまうので、フルスクリーン時の画面遷移を保持できません

ここは画面回転時とは大きく違う点です.


2 については、Page.OnDisappearing で例えば動画の再生を一時停止する処理を書いていると、画面は表示されているのに動画が再生されていないという状況になります.

動画の再生などユーザが気づき得る明示的な状況ならば大して問題にならないかもしれませんが、GPSのロギングを一時停止するなどの暗黙的な状況だと支障が出るかもしれません.



マルチウィンドウ時のフォーカスオン・オフ

続いてマルチウィンドウ時のフォーカスオン・オフの切り替えです.

f:id:Santea:20161207173219p:plain:w360

このように2つのアプリが並んでいるときにそれぞれをタップしてフォーカスが切り替わったときの挙動を調べます.


フォーカスオン

Activity.OnResume
Page.OnAppearing

フォーカスオフ

Page.OnDisappearing
Activity.OnPause


こちらはわりと期待通りのイベントがコールされていますね.

Page.OnAppearing, Page.OnDisappearing で画面が表示されていない前提で処理を書かないほうがいいことは、前述したものと同様です.



マルチウィンドウからフルスクリーン

最後にマルチウィンドウからフルスクリーンへの切り替えです.

Page.OnDisappearing
Activity.OnPause
Application.OnSleep
Activity.OnStop
Activity.OnDestroy
Activity.OnCreate
Page.OnAppearing
Application.OnStart
Activity.OnStart
Activity.OnResume

基本的にフルスクリーンからマルチウィンドウへの挙動と同様です.


f:id:Santea:20161207180019g:plain:w360

ナビゲーションスタックが初期化される現象も同様に発生します.



まとめと対策

Xamarin.Forms におけるマルチウィンドウの問題点は大きく分けて2つです.

  1. ナビゲーションスタックが初期化されてしまう
  2. Page.OnDisappearing で表示・非表示をハンドルすることは十分でない


1 については、ナビゲーションスタックを管理するモデルを用意するのが良いかと考えています.

その際、画面遷移時に必要な各パラメーターも同時に管理する必要があります.

こちらに関してはモデルに統合することもできますし、SQLite で保存しておくという方法もあるかと思います.


2 については、Application.OnResume/OnSleep でハンドルするという選択肢でしょうか.

www.nuits.jp

こちらの記事が参考になりそうです.


僕自身どうするのが良いのかいまいちはっきりしていませんが、設計に詳しいこの方が何かアドバイスをくだされば嬉しいな|ω・`)チラ


以上です.




参考