Edited at

Toast に関するよくある勘違い。

More than 1 year has passed since last update.

きょうび Toast の話なんて需要あるのー?って感じがするんですけど、たまに間違った情報を見かけるので。


TL;DR



  • Toast とはシステムサービスへの RPC。


    • 実体は NotificationManagerService にあるので、ローカル通知を無効にしようとすると Toast も出なくなる。



  • 呼び出すための ContextActivity でも Application でも Service でもなんでもいい。



    • getApplicationContext() による呼び出しは、リファレンスにもある通り正当。


    • Toast#setView() を使う場合も、別に内部で View を生成するわけではないので、テーマ適用の問題は起きない。



  • メインスレッド(UI スレッド)から呼び出す必要はない。



    • Looper さえ回っていれば、自分で作ったスレッドでも問題ない。



  • Context という API の存在は Android SDK を設計した人間の過ちだと思う。


最初に

Toast をフィードバックとして多用するのはやめましょう


  • 開発に必要なデバッグログ表示には Log クラスを使いましょう。


    • 直接 Log クラスを使うよりライブラリを使った方が良いです。



  • ユーザーのインタラクションが必要な場合は、SnackBar を使う方がより好ましいです。

Material Design の Snackbars & toasts には、


Toasts (Android only) are primarily used for system messaging. They also display at the bottom of the screen, but may not be swiped off-screen.


としか書いてありません。この「システムメッセージ」とは何を指すのか不明瞭で、どういう使い分けをするのかという部分がいまいち分かりづらいところです。


Snackbar と Toast の違い

SnackBar Vs Toast - Programmer Guru より翻訳して引用。

Toast
Snackbar

API Level 1 から使える。
API Level 23 で追加された(※)。

基本的に Activity を必要としない。(ホーム画面や他のアプリ上にも表示できる)
自身のアプリケーションの Activity 内部にのみ表示できる。

ユーザー入力に応じたアクションを実行することができない。
アクションを実行できる。

スワイプによって非表示することができない。
スワイプで非表示にすることができる。

クリックやスワイプといったユーザー入力を制御できない。
ユーザー入力を制御できる。

ユーザーに情報メッセージを表示するのに向く。
ユーザーの注意が必要な警告・情報メッセージを表示するのに向く。

Snackbar はサポートライブラリにしか存在しないので、この記述は間違っていますがそのままにしてあります。


一番大きな違い

Snackbar はアプリケーション内の Window に表示されるものですが、Toast はアプリケーションを跨いでも表示される=アプリケーション外の Window に表示される、ということです。

このことは、アプリケーション外でも表示されていることから自明だとは思います。

アプリケーション
ホームへ移動


Toast を表示している状態で、Layout Inspector を起動するなど確認する方法はいくつかあります。

実装を読むのが一番早いでしょう。Toast.java を見ると、内部に TN というクラスがあり、恐ろしくざっくりと説明すると以下のようなことをしています。

// WindowManager のパラメータを設定

final WindowManager.LayoutParams params = mParams;
// Window Type として Toast を指定
params.type = WindowManager.LayoutParams.TYPE_TOAST;

// WindowManager を取得する
WindowManager mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
// WindowManager に対して View を表示
mWM.addView(mView, mParams);

WindowManager はアプリケーションのものを含めて、複数の Window を管理しており、それらは表示の優先度に応じた層を形成しています。

WindowManager.LayoutParams.TYPE_TOAST はアプリケーションの Window よりも高い優先度を持つ Window のため、常にアプリケーションより前面に表示されます。

一般のアプリでも、SYSTEM_ALERT_WINDOW のパーミッションがあれば「特殊な Window への View の表示」が実現できていました。しかし、Android O 以降では大きく制限されるようになったのでご注意ください。


Toast の表示に至るまで

大体 Toast がうまく動かない原因は、Toast 表示を行おうとしたスレッドの、Looper(実行ループ)が回っていないことです。これについて、コードを追って説明してみます。

Toast.java より、TN という内部クラスにて、Handler を作成しているのが分かると思います。この HandlerToast を作成したスレッドのものになります。

final Handler mHandler = new Handler() {

@Override
public void handleMessage(Message msg) {
IBinder token = (IBinder) msg.obj;
handleShow(token);
}
};

Handler って何?という人は、その手の資料をまず読んでみましょう。

さて、Toast の表示は以下のように行われます。

// NotificationManagerService を取得する

INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;

try {
// NotificationManagerService に Toast の表示をキューイングする
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}

既に説明した通り、Toast はアプリ単位の Window ではなく、システムサービスの WindowManager が管理する、Window Type が TYPE_TOAST のレイヤに表示されるものです。

複数のアプリケーションからの Toast 表示要求を捌けるように、NotificationManagerService へキューイングし、ひとつひとつ順番に表示されます。

すると、TN クラスとは「どのような Toast を表示するのか」という情報を内包したオブジェクトであり、キューから取り出されたときにコールバックを受け取り、handleShow() メソッドにて Toast を表示するのだと分かると思います。

この仕組みのため、Toast を生成した ThreadLooper が回っていないと、コールバックされても Handler が処理することができず、例外となります。


メインスレッド以外からの Toast 表示

メインスレッドは必ず Looper が回っているので、そこから呼び出すのが一番なのですが、以下のようなコードでどこからでも Toast が表示できると思います。たぶん。

private static class ToastThread extends Thread {

final private Context context;
final private CharSequence message;

private ToastThread(Context context, CharSequence message){
this.context = context.getApplicationContext();
this.message = message;
}

@Override
public void run() {
Looper.prepare();
Toast.makeText(context, message, Toast.LENGTH_LONG).show();
Looper.loop();
}
}

使用する側のコードはこんな感じです。

new ToastThread(this, "hello, toast.").start();

ちなみにいいコードではないと思いますので真似しない方がいいです。Toast 自体、どうしても必要という場面以外では避けるべきでしょう。


なぜ Toast の表示に Context が要求されるのか?

以下のふたつの理由になるかと思います。


  1. システムサービスの取得に Context が必要なため



    • NotificationManagerServiceWindowManager、あとは Toast の View を生成するための LayoutInflater を取得しています。



  2. パッケージ名を取得するため



    • NotificationManagerService 内で、「通知」が無効されている場合に Toast を出さないように制御するためです。



ですので、Activity Context である必要性はないですし、ContextgetWindow() の返り値を持つかも関係ないです。