Xamarin.Forms の Shell で Android の戻るボタンでトップページに一旦戻るようにする

この記事は [Xamarin Advent Calendar 2020](https://qiita.com/advent-calendar/2020/xamarin) の 10 日目の記事です。あいてるみたいなので飛び込んでみました。
9 日目は @muak_x さんの[スタートアップトレースを使ってXamarin.Androidの起動速度を向上させる](https://zenn.dev/muak/articles/d364cb4ee7a890)でした。

## Xamarin.Forms の Shell

Xamarin.Forms の Shell というコントロールを使うとモバイル アプリケーションでありがちな画面レイアウトをさくっと作ることが出来ます。例えば以下のようなハンバーガーメニューでページを切り替えるようなアプリが出来ます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/e108bb96-95a0-4c01-8005-cdcbf01795ca.png)

そのほかにも以下のようにタブでページ切り替えをするようなアプリや、ハンバーガーメニューとタブを組み合わせた画面遷移を行うようなアプリケーションが Shell コントロールを使うと作成できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/5b7217ef-007e-e2b4-2bb9-0a633bc3ad2e.png)

GitHub の Xamarin.Forms リポジトリで Shell 関係のタグで絞るとこれを書いている 2020/12/09 時点では 231 件の Open な Issues があるので、結構まだ使い込むと不具合に当たる可能性が高いのかな？という気もしないでもないですが、出てきた当初のように少し触っただけで不具合を踏む可能性は下がったなという印象はあります。

この記事は Xamarin.Forms v4.8.0.1687 を使って書いています。

## ちょっとした不満

Android には戻るボタンがあって、それを押すと 1 ページ前に戻るという動作が基本的には期待される動作になります。ただ、Shell コントロールのハンバーガーメニューの部分をタップして遷移したものはナビゲーションのスタックに積まれないみたいなので、以下のようにアプリがサクッと終了してしまいます。

![shellnavissue.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/30652bcc-d982-c74e-c9e5-e37b95a48ed7.gif)

いくつかの画面下部にタブがあったりハンバーガーメニューがあるアプリを触って確認したところ、タブ移動やハンバーガーメニューによる画面遷移をした後に戻るボタンを押すと一旦トップページに戻るという動作をしていました。トップページで改めて戻るボタンを押すことでアプリが終了します。

以下は Google アプリの動作です。このアプリは厳密にはちょっと動画が異なる（別アプリに遷移している or Xamarin.Forms でいうナビゲーションスタックにプッシュしている）ように見えますが、とりあえずアプリが閉じてしまう前に一旦最初のページに戻るという動作のイメージとしては以下のような感じです。

![googleapp.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/febcbac9-1663-b6be-38a6-427107ab66b1.gif)


今回は Shell でこの動きを実現したいと思います。

## 戻るボタンのハンドリング

Xamarin.Forms で戻るボタンのハンドリングを行うには OnBackButtonPressed メソッドをオーバーライドします。OnBackButtonPressed メソッドで true を返すとアプリで戻るボタンをハンドリングしたと言うことにできます。false を返すとデフォルトの戻るボタンの動作になります。

なので、Shell の OnBackButtonPressed をオーバーライドして、以下のような処理を書くことでトップページ（Shell のプロジェクトテンプレートの場合は AboutPage) 以外のときと、トップページのときで処理を変えてあげれば思った通りの動きになります。

```csharp
using ShellSampleApp.Views;
using System;
using Xamarin.Forms;

namespace ShellSampleApp
{
    public partial class AppShell : Xamarin.Forms.Shell
    {
        public AppShell()
        {
            InitializeComponent();
            Routing.RegisterRoute(nameof(ItemDetailPage), typeof(ItemDetailPage));
            Routing.RegisterRoute(nameof(NewItemPage), typeof(NewItemPage));
        }

        // 追加
        protected override bool OnBackButtonPressed()
        {
            if (CurrentState.Location.OriginalString == "//AboutPage")
            {
                // トップページならそのまま終わる
                return base.OnBackButtonPressed();
            }
            else
            {
                // トップページ以外の場合はトップページに戻る
                _ = GoToAsync("//AboutPage");
                return true;
            }
        }

        private async void OnMenuItemClicked(object sender, EventArgs e)
        {
            await Shell.Current.GoToAsync("//LoginPage");
        }
    }
}
```

動きは以下のようになります。

![shellresolve.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70288/442c3006-ab5c-e023-e2ff-26aba835f122.gif)


## 気になる点

OnBackButtonPressed メソッドが非同期じゃないので、画面遷移の GoToAsync メソッドを呼ぶだけ呼んで無視しています。なので GoToAsync の中でもし例外が起きてしまったら例外が虚空の彼方に消えてしまいます。
なので、ちゃんとするなら以下のようにした方がいいと思いますが、GoToAsync で正しいパスを与えた状態でエラーを起こす方法がぱっと思いつかなかったので未検証です。

```csharp
protected override bool OnBackButtonPressed()
{
    if (CurrentState.Location.OriginalString == "//AboutPage")
    {
        // トップページならそのまま終わる
        return base.OnBackButtonPressed();
    }
    else
    {
        // トップページ以外の場合はトップページに戻る
        _ = GoToAsync("//AboutPage").ContinueWith(async (result) =>
        {
            if (result.Exception != null)
            {
                // エラーが起きてたら何か処理
                await Device.InvokeOnMainThreadAsync(async () =>
                {
                    await DisplayAlert("Error", result.Exception.Message, "Close");
                });
            }
        });
        return true;
    }
}
```

## まとめ

ということで、Shell を使ったときの戻るボタンの動作をカスタマイズしてみました。昔は Shell では OnBackButtonPressed が実行されないという問題もあったのですが、それが修正されていたので割と簡単に対処できました。今回は新規作成したプロジェクトテンプレートを元に動作確認をしたので、もしかしたら Shell の画面の定義によっては期待した通りに動かない可能性もありますが、その場合も今回使った OnBackButtonPressed の中で頑張ればなんとかなるのかなと期待しています。

ということで、この記事は以上になります。ソースコードは GitHub にアップしています。

https://github.com/runceel/Xamarin.Forms-Shell-Android-BackButtonBehavior