1
0

More than 3 years have passed since last update.

Android 10 搭載の一部端末で ListView の項目を選択できない障害について調べたら……

Last updated at Posted at 2021-02-17

障害発生

 
業務で Android アプリの保守をしていたら、Android 10 搭載の一部端末で ListView をタップしてもしばらく反応しないという障害が発生しました。

障害が発生し始めたのは、TabActivity を(ようやく) ViewPager2 に置換してから。当該画面は Activity から Fragment に作り替えられました。

以前との違いを調べれば原因が分かるはず、とリーダーから指示されて調べたのですが……

これは選択できなくて当然なのでは

調べて、原因が判明するまでに、Android Studio のデバッガで Android システムの Java クラス内部まで見る必要がありました。

原因はこうです。

Activity から Fragment に作り替えたことにより、ListView を含む Fragment が ViewPager2により Windows にアタッチしたりデタッチしたりされるようになりました。

ListView の親クラス AbsListView がWindow からデタッチされたときに呼び出される onDetachedFromWindow() で、Adapter への参照を持つインスタンス変数 mAdapter が mAdapter != null の場合に次の処理が実行されます。

if (mAdapter != null && mDataSetObserver != null) {
    mAdapter.unregisterDataSetObserver(mDataSetObserver);
    mDataSetObserver = null;
}

これにより mDataSetObserver == null が保証され、Window からデタッチされている間はデータの変更が反映されなくなります。

その後で AbsListView が Window にアタッチされたときに呼び出される onAttachedToWindow() で、Adapter への参照を持つインスタンス変数 mAdapter が mAdapter != null && mDataSetObserver == null の場合に次の処理が実行されます。

if (mAdapter != null && mDataSetObserver == null) {
    mDataSetObserver = new AdapterDataSetObserver();
    mAdapter.registerDataSetObserver(mDataSetObserver);

    // Data may have changed while we were detached. Refresh.
    mDataChanged = true;
    mOldItemCount = mItemCount;
    mItemCount = mAdapter.getCount();
}

データの変更が View に反映されていないことを示すインスタンス変数 mDataChanged が mDataChanged == true になります。この変数が true の間は、リストの項目が古いので、タップが無視されます。

ところが、障害が起きている端末では、いくら待っても、この変数が false に変わらないのです。そのためタップを繰り返しても無視されてしまいます。

インスタンス変数 mDataChanged が mDataChanged == false になる契機の一つは、View に Layout が実行されることです。しかし Window にアタッチされたのに Layout は実行されません……

どうしてそんなことになるのか、頭を抱えました。

なぜ多くの端末で項目を選択できるのか

障害が発生していない端末で挙動を調べるようリーダーに指示されて、調べた結果が下記のスクリーンショットです。本当に商用のアプリのデバッグ中に記録したスクリーンショットで、アプリの詳細や開発環境が露見する箇所はぼかしています。

screenshot_for_qiita_20210217.png

このスクリーンショットは Pixel 4a で ListView を1回タップした後の処理の途中です。

タップなので、ポインティングデバイスには DOWN と UP のイベントが発生します。しかし、その間に、人間にとってはタップのつもりが、端末内部では MOVE のイベントが発生しています。

MOVE のイベントが発生すると ListView はスクロールさせる必要が生じます。そこに、次のようなコメントがあります。

// Re-sync everything if data has been changed
// since the scroll operation can query the adapter.

データが変更されていれば同期させると言います。そして View の子を Layout しています。

ListView がスクロールされる際、その事前条件を満たすための処理が実行されることにより、データが ListView に反映されるため、以降は項目を選択できるようになるのです。それまでデータが ListView に反映されていないことはインスタンス変数 mDataChanged == true によって証拠が残っています。

ListView の項目を選択できることが、タップ中に MOVE イベントが発生することに依存しているのです。

障害が発生していた端末では、タップ中に MOVE イベントが発生しないことがありました。頻度はまちまちです。MOVE イベントが発生しなかったタップは、ListView の状態が整わず、無視されていました。

僕には、これがバグとは断定できません。Google が端末メーカに、タップ中に MOVE イベントを発生させるよう要求していれば、この仕様で動作します。端末メーカがどのような指示を受けているのか知らない僕は、是非を断ずることができません。

ただ、障害を起こした端末が存在します。また、UI パーツの初期化完了がポインティングデバイスのイベントに依存している仕様は、すぐには受け入れがたいものがあります。

直近ではどうしたか

まずはアプリのバグを直さなければいけません。

対処として、問題の ListView を含む Fragment の onResume() で ListView に requestLayout() を呼び出すようにしました。Layout は瞬時に終わっているようで、利用者から見て問題なく ListView の項目を選択できるようになっています。

これからの話は……

上記の挙動が、Android 9 以前と、Android 11 でどう変化しているのか、調べていません。僕は言い出しっぺですが、余力がありません。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0