2013/11/21
MultiSelectListPreference にバグがあり、勝手に選択解除されることがある
以下のような手順で操作をすると、勝手に選択が解除されます。
場合によっては Preference の内容も書き換えてしまいます。
この問題は少なくとも 4.1.1_r1 で直っています。
再現手順
以下のような MultiSelectListPreference を用意しますres/xml/pref.xml
< MultiSelectListPreference android:key = "data_list" android:entries = "@array/pref_example_list_titles" android:entryValues = "@array/pref_example_list_values" android:negativeButtonText = "@android:string/cancel" android:positiveButtonText = "@android:string/ok" android:title = "@string/pref_title_messages" /> </ PreferenceScreen > |
これを表示する PreferenceActivity を用意し、起動。
ダイアログを表示。
「A」を選択して、OKをタップ。
PreferenceActivity に戻る。
もう一度 Preference をタップしてダイアログを表示。
この時は、まだ「A」が選択されたまま。
なにもせず、OK をタップ。
PreferenceActivity に戻る。
もう一度ダイアログ表示。
すると、「A」の選択が外れている。
この問題、3回もダイアログを表示しないと見えてこないので、あまり問題にならなそうですが、実は2回目にダイアログを表示した時点で、内部の不整合が起こっています。
そのため、むしろユーザが認識できない形でバグが入り込んでしまうので、結構危険な気がします。
原因
この現象の原因は、MultiSelectListPreference 内のmValues
と mNewValues
の扱いに問題があることです。mValues
は現在選択されている項目、mNewValues
は新しく選択した項目で、OK を押した場合には値が更新され、Cancel を押した場合にはmNewValues
が破棄されるという構造になっています。コードを追っていきましょう。
まず、「A」を選択して OK を押すと、
onDialogClosed()
が呼ばれます。そして、その中で
setValues()
が呼ばれます。MultiSelectListPreference (Android 4.0.1_r1)
195 196 197 198 199 200 201 202 203 204 205 206 | @Override protected void onDialogClosed( boolean positiveResult) { super .onDialogClosed(positiveResult); if (positiveResult && mPreferenceChanged) { final Set<string> values = mNewValues; if (callChangeListener(values)) { setValues(values); } } mPreferenceChanged = false ; } |
setValues()
の中では引数values
が mValues
に無条件で代入されています。呼び出し元をもう一度見てみましょう。引数には
mNewValues
が入っています。つまり、この時点で、
mValues
と mNewValues
は同じインスタンスを示すことになります。127 128 129 130 131 | public void setValues(Set<string> values) { mValues = values; persistStringSet(values); } |
この後、もう一度ダイアログを表示すると
onPrepareDialogBuilder()
が呼ばれます。表示自体は
getSelectedItems()
で取得した 「A」 が選択された状態で表示されますが、mNewValues.clear()
によって、mValues
も clear されてしまいます。当然、
mNewValues.addAll(mValues)
をしても mNewValues
は空のまま。
表示と状態が合っていない状態が出来上がります。157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 | @Override protected void onPrepareDialogBuilder(Builder builder) { super .onPrepareDialogBuilder(builder); if (mEntries == null || mEntryValues == null ) { throw new IllegalStateException( "MultiSelectListPreference requires an entries array and " + "an entryValues array." ); } boolean [] checkedItems = getSelectedItems(); builder.setMultiChoiceItems(mEntries, checkedItems, new DialogInterface.OnMultiChoiceClickListener() { public void onClick(DialogInterface dialog, int which, boolean isChecked) { if (isChecked) { mPreferenceChanged |= mNewValues.add(mEntryValues[which].toString()); } else { mPreferenceChanged |= mNewValues.remove(mEntryValues[which].toString()); } } }); mNewValues.clear(); mNewValues.addAll(mValues); } |
この後、OK を押すと、空の
mNewValues
を mValues
にセットしに行くので、次に表示する時には選択が解除された状態になるというわけです。解決策
この問題、少なくとも Android 4.1.1_r1 以降では以下のように修正されています。MultiSelectListPreference (Android 4.1.1_r1)
127 128 129 130 131 132 | public void setValues(Set<string> values) { mValues.clear(); mValues.addAll(values); persistStringSet(values); } |
入力をそのまま代入するのではなく、
addAll()
を使うことで参照が同じになってしまうのを防いでいるわけです。ターゲットが Android 4.1 以降であるならば、この問題をケアする必要はありません。
しかし、まだターゲットが Android 4.1 以降のみというケースは少ないはず。
そういう場合にどうしたら良いか考えてみました。
まず最初に、
OnPreferenceChangeListener
や OnPreferenceClickListener
を使って回避する方法を思いついたのですが、どちらもタイミングが悪く、mNewValues
と mValues
の一致を防ぐことは出来ません。で、結局、新たなクラスを作るしかないという結論にたどり着きました。
まず、以下のようなクラスを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public class WrapperMultiSelectListPreference extends MultiSelectListPreference { public WrapperMultiSelectListPreference(Context context) { super (context); } public WrapperMultiSelectListPreference(Context context, AttributeSet attrs) { super (context, attrs); } @Override public void setValues(Set<String> values) { super .setValues( new HashSet<String>(values)); } } |
これを、以下のように使用すればOKです。
1 2 3 4 5 6 7 8 9 | < com.kokufu.android.lib.preference.WrapperMultiSelectListPreference android:key = "data_list" android:entries = "@array/pref_example_list_titles" android:entryValues = "@array/pref_example_list_values" android:negativeButtonText = "@android:string/cancel" android:positiveButtonText = "@android:string/ok" android:title = "@string/pref_title_messages" /> < PreferenceScreen /> |
マイナーなバグに対応するために、新たなクラスを定義するのはちょっと気に入りませんが、世の中に出てしまったバグなので致し方ありません。
ちなみに、バージョン間のコードの差分を確認するには GrepCode を利用すると便利です。
0 件のコメント:
コメントを投稿