KDP(電子出版)のメモ 急急如律令

Amazon Kindleダイレクト・パブリッシングでの電子出版や電子書籍の作成販売について、文章やイラストの作成や編集方法について書いています。

preferenceが落ちる原因の特定

何故か、preferenceを追加するとアプリがクラッシュした。booleanにstringを入れようとしたと見るが、よくわからなかった。最終的にはkey値が同じものを複数指定していたのが原因だった。

あなた:

java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Boolean at android.app.SharedPreferencesImpl.getBoolean(SharedPreferencesImpl.java:345)

ChatGPT:

このエラー:

java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Boolean

は、SharedPreferences#getBoolean() で boolean型として読み込もうとしたキーに、実際には文字列(<string>)として保存されていた値が入っていたことが原因です。

たとえば以下のような preferences.xml の値:

<string name="RemoveEmptyLine">0</string>

これに対して Java 側で:

boolean removeEmptyLine = prefs.getBoolean("RemoveEmptyLine", false);

と書くと、SharedPreferences は "0"(String)を boolean にキャストしようとしてクラッシュします。

<boolean name="RemoveEmptyLine" value="false" />

そして Java 側はこのままでOK:

boolean removeEmptyLine = prefs.getBoolean("RemoveEmptyLine", false);
String val = prefs.getString("RemoveEmptyLine", "0");
boolean removeEmptyLine = "1".equals(val) || "true".equalsIgnoreCase(val);

先ほど提供してくれた XML の中だと、以下が <string> なのに boolean として読まれてそうなキー です:

キー名 現在の型 期待される型
RemoveEmptyLine = "0" = false
Cover = "0" boolean または int にするべき
MaxEmptyLine = "-" int か boolean(使用箇所による)
  1. preferences.xml を開いて、booleanとして使っているキーを確認
  2. 対象の に修正
  3. アプリを再インストール or SharedPreferences.clear() して古い値を消す(※以前保存された値が残るため)

preferences.xml を開いて、booleanとして使っているキーを確認

対象の <string><boolean> に修正

アプリを再インストール or SharedPreferences.clear() して古い値を消す(※以前保存された値が残るため)

prefs.edit().clear().apply(); // 開発中だけ使う
  • getBoolean() を使うときは SharedPreferences に保存された値が 本当に boolean かどうか確認
  • XML や PreferenceFragmentCompat 側で に修正
  • 古いデータが残っているとクラッシュするので初期化も忘れずに!

getBoolean() を使うときは SharedPreferences に保存された値が 本当に boolean かどうか確認

XML や PreferenceFragmentCompat 側で <string><boolean> に修正

古いデータが残っているとクラッシュするので初期化も忘れずに!

該当箇所のコードを貼ってもらえれば、型の修正提案もできますよ。必要なら教えてください!

あなた:

FATAL EXCEPTION: main Process: io.github.kyukyunyorituryo.aozoraepub3, PID: 32254 java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Boolean at android.app.SharedPreferencesImpl.getBoolean(SharedPreferencesImpl.java:345) at androidx.preference.Preference.getPersistedBoolean(Preference.java:1975) at androidx.preference.TwoStatePreference.onSetInitialValue(TwoStatePreference.java:207) at androidx.preference.Preference.onSetInitialValue(Preference.java:1634) at androidx.preference.Preference.dispatchSetInitialValue(Preference.java:1607) at androidx.preference.Preference.onAttachedToHierarchy(Preference.java:1330) at androidx.preference.Preference.onAttachedToHierarchy(Preference.java:1345) at androidx.preference.PreferenceGroup.addPreference(PreferenceGroup.java:253) at androidx.preference.PreferenceGroup.addItemFromInflater(PreferenceGroup.java:173) at androidx.preference.PreferenceInflater.rInflate(PreferenceInflater.java:350) at androidx.preference.PreferenceInflater.rInflate(PreferenceInflater.java:351) at androidx.preference.PreferenceInflater.inflate(PreferenceInflater.java:161) at androidx.preference.PreferenceInflater.inflate(PreferenceInflater.java:112) at androidx.preference.PreferenceManager.inflateFromResource(PreferenceManager.java:217) at androidx.preference.PreferenceFragmentCompat.setPreferencesFromResource(PreferenceFragmentCompat.java:380) at io.github.kyukyunyorituryo.aozoraepub3.SettingsFragment.onCreatePreferences(SettingsFragment.java:15) at androidx.preference.PreferenceFragmentCompat.onCreate(PreferenceFragmentCompat.java:161) at androidx.fragment.app.Fragment.performCreate(Fragment.java:3090) at androidx.fragment.app.FragmentStateManager.create(FragmentStateManager.java:475) at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:257) at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:1899) at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:1817) at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:1760) at androidx.fragment.app.FragmentManager.dispatchStateChange(FragmentManager.java:2985) at androidx.fragment.app.FragmentManager.dispatchActivityCreated(FragmentManager.java:2895) at androidx.fragment.app.FragmentController.dispatchActivityCreated(FragmentController.java:263) at androidx.fragment.app.FragmentActivity.onStart(FragmentActivity.java:351) at androidx.appcompat.app.AppCompatActivity.onStart(AppCompatActivity.java:251) at android.app.Instrumentation.callActivityOnStart(Instrumentation.java:1564) at android.app.Activity.performStart(Activity.java:8555) at android.app.ActivityThread.handleStartActivity(ActivityThread.java:3859) at android.app.servertransaction.TransactionExecutor.performLifecycleSequence(TransactionExecutor.java:221) at android.app.servertransaction.TransactionExecutor.cycleToPath(TransactionExecutor.java:201) at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:173) at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2394) at android.os.Handler.dispatchMessage(Handler.java:106) at android.os.Looper.loopOnce(Looper.java:211) at android.os.Looper.loop(Looper.java:300) at android.app.ActivityThread.main(ActivityThread.java:8296) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:559) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:954)

ChatGPT:

この FATAL EXCEPTION は、Androidの SharedPreferences に保存された値の型と、Preference(特に SwitchPreferenceCompat や CheckBoxPreference など)で期待している型が一致していないために発生しています。

以下のようなコードが原因で:

prefs.getBoolean("SomeKey", false);

でも、preferences.xml には:

<string name="SomeKey">false</string>

<string> として保存されていた場合、アプリはクラッシュします。

at androidx.preference.Preference.getPersistedBoolean(Preference.java:1975)

これは、<SwitchPreferenceCompat><CheckBoxPreference> などの TwoStatePreference の defaultValue に boolean を期待していたのに、実際は SharedPreferences に String があった、ということです。

提供されたXMLでは、以下のように string になってるものが対象です:

<string name="RemoveEmptyLine">0</string> ← これはNG

↓以下のように直す:

<boolean name="RemoveEmptyLine" value="false" />

アプリ内で以前保存された不適切な型(例:String)を削除します。

SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
SharedPreferences.Editor editor = prefs.edit();
editor.remove("RemoveEmptyLine"); // クラッシュしてるキー名
editor.apply();

または開発中なら全削除でもOK:

prefs.edit().clear().apply(); // 全消し(慎重に)

エラーを引き起こしているのは boolean型の設定項目にstring型の値が保存されていたキー です。

たとえば:

<string name="RemoveEmptyLine">0</string> ← boolean expected
<string name="Cover">0</string> ← boolean expected?
<string name="MaxEmptyLine">-</string> ← int expected?
対策 内容
XMLを修正 に変更
キャッシュクリア prefs.edit().clear().apply() または .remove("キー") を使う
型の一貫性を保つ 保存時も読み込み時も常に同じ型を使う

必要であれば、preferences.xml を私の方で自動チェックして、どのキーが boolean にすべきか一覧で出すこともできます。希望しますか?

あなた:

preferenceのUIを開いただけで落ちる

ChatGPT:

了解です、「PreferenceのUIを開いただけで落ちる」という状況は、かなりの確率で preferences.xml 内にある boolean を期待している項目に、string 型の値が保存されていることが原因 です。

これは SharedPreferences に保存された値の型が UI コンポーネント(SwitchPreferenceCompatなど)と一致していないために、UI初期化時 (onCreatePreferences) にクラッシュします。

以下の項目が問題を引き起こしている可能性が非常に高いです:

キー名 内容 想定される正しい型 修正例
RemoveEmptyLine 0 boolean
Cover 0 boolean(または string) ※UI側の型と要相談
MaxEmptyLine - int? UI側がEditTextPreferenceかSwitchかで変わる
ChukiRuby 0 int or boolean どちらとして使っているか確認
TitleType 0 int OK(ただし使用箇所で型キャスト注意)

アプリ起動時(MainActivity.onCreate() など)に 強制的に不正な型を消す:

SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
prefs.edit()
    .remove("RemoveEmptyLine")
    .remove("Cover")
    .remove("MaxEmptyLine")
    .remove("ChukiRuby")
    .apply();

またはすべて初期化:

prefs.edit().clear().apply(); // 注意:全ての設定が初期化されます

RemoveEmptyLine のように boolean にすべき項目は、次のように修正してください:

<!-- 修正前 -->
<string name="RemoveEmptyLine">0</string>

<!-- 修正後 -->
<boolean name="RemoveEmptyLine" value="false" />
  • Androidの SharedPreferences は、キーごとに型を固定して保存します。
  • Preference のUIは getPersistedBoolean() など、型を決め打ちで呼び出すため、保存されている型が違うと ClassCastException が出ます。

Androidの SharedPreferences は、キーごとに型を固定して保存します。

Preference のUIは getPersistedBoolean() など、型を決め打ちで呼び出すため、保存されている型が違うと ClassCastException が出ます。

  • preferences.xml に で boolean 値を入れていないか?
  • SharedPreferences に古い値が残っていないか?
  • Preference UIの該当キーの型と保存時の型が一致しているか?

preferences.xml に <string> で boolean 値を入れていないか?

SharedPreferences に古い値が残っていないか?

Preference UIの該当キーの型と保存時の型が一致しているか?

必要であれば、問題のある <string> をすべて <boolean><int> に修正したバージョンの XML を出力しますよ。欲しいですか?

あなた:

数字をスライダーから選択したい

ChatGPT:

数字をキーボードで入力する EditTextPreference を、スライダー(シークバー)で選択できるようにしたい場合、androidx.preference では SeekBarPreference を使うのがベストです!

<SeekBarPreference
    android:key="WebBeforeChapter"
    android:title="出力話数"
    android:summary="最新話からの出力数"
    android:defaultValue="1"
    android:min="1"
    android:max="100" />
  • defaultValue="1":初期値を 1 にします。
  • min="1":最小値(必要に応じて調整可能)。
  • max="100":最大値(必要に応じて調整可能)。

defaultValue="1":初期値を 1 にします。

min="1":最小値(必要に応じて調整可能)。

max="100":最大値(必要に応じて調整可能)。

スライダーはそのままでも使えますが、細かく調整したい場合は SeekBarPreference に app:showSeekBarValue="true" をつけると値も表示できます。

xmlns:app="http://schemas.android.com/apk/res-auto"

をルートXMLに追加して:

<SeekBarPreference
    android:key="WebBeforeChapter"
    android:title="出力話数"
    android:summary="最新話からの出力数"
    android:defaultValue="1"
    android:min="1"
    android:max="100"
    app:showSeekBarValue="true" />

必要なら、現在の EditTextPreference の値が正しく int になっているかのチェックと変換コードも出せますよ!また、範囲を変更したい(例:最大200にしたいなど)場合も教えてください。

あなた:

次のpreferenceを追加すると設定の呼び出し時に止まる

<PreferenceCategory android:title="変換対象">
    <CheckBoxPreference
        android:key="WebBeforeChapter"
        android:title="最新"
        android:summary="最新話から指定話数のみ出力します"
        android:defaultValue="false"/>

    <EditTextPreference
        android:key="WebBeforeChapter"
        android:title="出力話数"
        android:summary="最新話からの出力数"
        android:defaultValue="1"
        android:inputType="number" />

    <CheckBoxPreference
        android:key="WebModifiedOnly"
        android:title="更新分"
        android:summary="追加更新のあった話のみ変換します"
        android:defaultValue="false"/>

    <CheckBoxPreference
        android:key="WebModifiedTail"
        android:title="連続"
        android:summary="最新話から連続した更新分のみ変換します"
        android:defaultValue="false"/>
</PreferenceCategory>

ChatGPT:

設定画面でクラッシュする原因は、以下のように 同じ key を複数の Preference に使っている ためです。

<CheckBoxPreference
    android:key="WebBeforeChapter" ←①
    ... />

<EditTextPreference
    android:key="WebBeforeChapter" ←②
    ... />

このように WebBeforeChapter というキーが 2 回使われており、片方は boolean(CheckBox)、もう片方は String(EditText)として保存されます。

これにより、Androidがbooleanを期待しているところでStringが保存されていて型が合わず、ClassCastException が発生しています:

java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Boolean

各 Preference の key は 重複させず、ユニークにしてください。

例:

<PreferenceCategory android:title="変換対象">
    <CheckBoxPreference
        android:key="WebBeforeChapterCheck"
        android:title="最新"
        android:summary="最新話から指定話数のみ出力します"
        android:defaultValue="false"/>

    <SeekBarPreference
        android:key="WebBeforeChapter"
        android:title="出力話数"
        android:summary="最新話からの出力数"
        android:defaultValue="1"
        android:min="1"
        android:max="100"
        app:showSeekBarValue="true" />

    <CheckBoxPreference
        android:key="WebModifiedOnly"
        android:title="更新分"
        android:summary="追加更新のあった話のみ変換します"
        android:defaultValue="false"/>

    <CheckBoxPreference
        android:key="WebModifiedTail"
        android:title="連続"
        android:summary="最新話から連続した更新分のみ変換します"
        android:defaultValue="false"/>
</PreferenceCategory>
  • 元のEditTextPreferenceをSeekBarPreferenceに変更しているのは、前のご要望を踏まえてです。元に戻す場合は教えてください。
  • WebBeforeChapterCheck(チェックボックス)と WebBeforeChapter(数値)を分離したことで、"使う/使わない" フラグと "使う場合の数値" が別々に管理できます。

元のEditTextPreferenceをSeekBarPreferenceに変更しているのは、前のご要望を踏まえてです。元に戻す場合は教えてください。

WebBeforeChapterCheck(チェックボックス)と WebBeforeChapter(数値)を分離したことで、"使う/使わない" フラグと "使う場合の数値" が別々に管理できます。

何かの設定がグレーアウトされてほしい(例:チェックされてないとスライダーを無効化)みたいな挙動が欲しければ、カスタムで処理できますのでそれもお知らせください!