SettingsActivity再入門 ～PreferenceFragmentCompatの読み書き対象を変更する～


[SettingsActivity再入門 ～PreferenceFragmentCompatで作る設定画面～](https://qiita.com/ryo_mm2d/items/5e6d91bcdb8122ab3ef6) の続きです。

PreferenceFragmentCompatはデフォルトの挙動から、DefaultSharedPreferencesに対して読み書きにするためにしか使えないと勘違いされがちですが、**デフォルト以外のSharedPreferencesやSharedPreferences以外への読み書きも可能です。**

ここでは、PreferenceFragmentCompatの保存先をDefaultSharedPrefrencesから変更する方法を説明します。

# 読み書き先を変更する

PreferenceFragmentCompatはDefaultSharedPreferencesに対して読み書きを行いますが、この挙動は変更することができます。読み書き処理がどのように実装されているのかを確認してみます。

これはPreferenceクラスに全部の型の読み書きが実装されています。Booleanの処理を見てみましょう。

```java:Preference.java
protected boolean persistBoolean(boolean value) {
    if (!shouldPersist()) {
        return false;
    }

    if (value == getPersistedBoolean(!value)) {
        return true;
    }

    PreferenceDataStore dataStore = getPreferenceDataStore();
    if (dataStore != null) {
        dataStore.putBoolean(mKey, value);
    } else {
        SharedPreferences.Editor editor = mPreferenceManager.getEditor();
        editor.putBoolean(mKey, value);
        tryCommit(editor);
    }
    return true;
}

protected boolean getPersistedBoolean(boolean defaultReturnValue) {
    if (!shouldPersist()) {
        return defaultReturnValue;
    }

    PreferenceDataStore dataStore = getPreferenceDataStore();
    if (dataStore != null) {
        return dataStore.getBoolean(mKey, defaultReturnValue);
    }

    return mPreferenceManager.getSharedPreferences().getBoolean(mKey, defaultReturnValue);
}

@Nullable
public PreferenceDataStore getPreferenceDataStore() {
    if (mPreferenceDataStore != null) {
        return mPreferenceDataStore;
    } else if (mPreferenceManager != null) {
        return mPreferenceManager.getPreferenceDataStore();
    }
    return null;
}
```

`persistBoolean`が書き込みで、`getPersistedBoolean`が読み出し処理ですね。
見ての通り、PreferenceDataStoreがあればそれを、なければPreferenceManagerのSharedPreferencesを利用するようになっていますね。
また、PreferenceDataStoreはPreferenceごとに設定されているものがあればそれを、なければPreferenceManagerのPreferenceDataStoreを利用しています。このことから書き込み先の変更方法は3種類ありそうですね。

ついでに、PreferenceManagerのインスタンスはどのように作られているかですが、PreferenceFragmentCompatのonCreateで作られています。

```java:PreferenceFragmentCompat.java
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    final TypedValue tv = new TypedValue();
    getActivity().getTheme().resolveAttribute(R.attr.preferenceTheme, tv, true);
    int theme = tv.resourceId;
    if (theme == 0) {
        // Fallback to default theme.
        theme = R.style.PreferenceThemeOverlay;
    }
    getActivity().getTheme().applyStyle(theme, false);

    mPreferenceManager = new PreferenceManager(getContext());
    mPreferenceManager.setOnNavigateToScreenListener(this);
    final Bundle args = getArguments();
    final String rootKey;
    if (args != null) {
        rootKey = getArguments().getString(ARG_PREFERENCE_ROOT);
    } else {
        rootKey = null;
    }
    onCreatePreferences(savedInstanceState, rootKey);
}
```

`onCreatePreferences`はPreferenceManagerが作られた直後にコールされるものなので、PreferenceManagerに対して何か操作をする場合は、`onCreatePreferences`の中で行うのが良さそうです。

## SharedPreferencesファイルの変更

まずは、標準で使われるSharedPreferencesがどのように作られているのを見てみましょう。`PreferenceManager#getSharedPreferences()`の実装は以下のようになっています。


```java:PreferenceManager.java
public SharedPreferences getSharedPreferences() {
    if (getPreferenceDataStore() != null) {
        return null;
    }

    if (mSharedPreferences == null) {
        final Context storageContext;
        switch (mStorage) {
            case STORAGE_DEVICE_PROTECTED:
                storageContext = ContextCompat.createDeviceProtectedStorageContext(mContext);
                break;
            default:
                storageContext = mContext;
                break;
        }

        mSharedPreferences = storageContext.getSharedPreferences(mSharedPreferencesName,
                mSharedPreferencesMode);
    }

    return mSharedPreferences;
}
```

PreferenceDataStoreがある場合はnullを返します。SharedPreferencesよりPreferenceDataStoreの方が優先されていることが分かります。PreferenceDataStoreがない場合は、SharedPreferencesのインスタンスを提供しますが、インスタンスが作られていない場合は、ここでmSharedPreferencesNameを使ってSharedPreferencesが作成されます。

mSharedPreferencesNameはコンストラクタで`<packagename>_preferences`という、DefaultSharedPreferencesの名前が設定されています。

```java:PreferenceManager.java
public PreferenceManager(Context context) {
    mContext = context;
    setSharedPreferencesName(getDefaultSharedPreferencesName(context));
}

private static String getDefaultSharedPreferencesName(Context context) {
    return context.getPackageName() + "_preferences";
}
```

参考までにgetDefaultSharedPreferencesの実装は以下のようになっており、同じ名前になっていることが分かりますね。

```java:PreferenceManager.java
public static SharedPreferences getDefaultSharedPreferences(Context context) {
    return context.getSharedPreferences(getDefaultSharedPreferencesName(context),
            getDefaultSharedPreferencesMode());
}
```

コンストラクタでもコールしていますが、mSharedPreferencesNameはsetSharedPreferencesNameで変更できます。setSharedPreferencesNameをコールするとmSharedPreferences のインスタンスが廃棄されるので、次回のgetSharedPreferencesで作り直されますね。

```java:PreferenceManager.java
public void setSharedPreferencesName(String sharedPreferencesName) {
    mSharedPreferencesName = sharedPreferencesName;
    mSharedPreferences = null;
}
```

先に示したように、`onCreatePreferences`の先頭であればPreferenceManagerが作られたばかりなので、SharedPreferencesのインスタンスはまだ作られていないので、ここで変更すれば読み書き対象のSharedPrferencesを完全に置き換えることができます。

```kotlin
class SettingsFragment : PreferenceFragmentCompat() {
    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
        preferenceManager.sharedPreferencesName = BuildConfig.APPLICATION_ID + "_main"
        setPreferencesFromResource(R.xml.root_preferences, rootKey)
    }
}
```

## PreferenceManagerのPreferenceDataStoreを指定する

SharedPreferencesは使いたくない、他の永続化システムを使いたいんだ、という場合はPreferenceDataStoreを指定します。
先に説明したように、SharedPreferencesよりもPreferenceDataStoreが優先して使われるようになっていますので、読み書きの処理のラッパーを提供すれば任意の永続化システムを使うことができます。

設定する箇所は、`onCreatePreferences`の先頭が良いでしょう。

```kotlin
class SettingsFragment : PreferenceFragmentCompat() {
    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
        preferenceManager.preferenceDataStore = MyPreferenceDataStore()
        setPreferencesFromResource(R.xml.root_preferences, rootKey)
    }
}
```

これで、各Preferenceの読み書きの対象がMyPreferenceDataStoreに置き換わります。

PreferenceDataStoreは以下のように各型ごとに読み書きの処理を実装するabstractクラスになっています。デフォルトの実装は書き込みがUnsupportedOperationExceptionで、読み出しが引数のデフォルト値をそのまま返すというものです。
絶対に使わないことが分かっている型があればその型の実装は省略することができます。

```java
public abstract class PreferenceDataStore {
    public void putString(String key, @Nullable String value) {
        throw new UnsupportedOperationException("Not implemented on this data store");
    }

    public void putStringSet(String key, @Nullable Set<String> values) {
        throw new UnsupportedOperationException("Not implemented on this data store");
    }

    public void putInt(String key, int value) {
        throw new UnsupportedOperationException("Not implemented on this data store");
    }

    public void putLong(String key, long value) {
        throw new UnsupportedOperationException("Not implemented on this data store");
    }

    public void putFloat(String key, float value) {
        throw new UnsupportedOperationException("Not implemented on this data store");
    }

    public void putBoolean(String key, boolean value) {
        throw new UnsupportedOperationException("Not implemented on this data store");
    }

    @Nullable
    public String getString(String key, @Nullable String defValue) {
        return defValue;
    }

    @Nullable
    public Set<String> getStringSet(String key, @Nullable Set<String> defValues) {
        return defValues;
    }

    public int getInt(String key, int defValue) {
        return defValue;
    }

    public long getLong(String key, long defValue) {
        return defValue;
    }

    public float getFloat(String key, float defValue) {
        return defValue;
    }

    public boolean getBoolean(String key, boolean defValue) {
        return defValue;
    }
}
```

## 特定のPreferenceの書き込み先を変更する

前項までの方法ではPreferenceFragmentCompatのPreferenceManagerに対して設定を行うことで全体の挙動を変更していましたが、Preference単位で変更することもできます。

```kotlin
findPreference<SwitchPreferenceCompat>("sync")?.preferenceDataStore = MyPreferenceDataStore()
```

これで特定のPreferenceだけ読み書き先を変更することができます。Preferenceごとに読み書きするデータ型は決まっているため、PreferenceDataStoreの実装もその一つの読み書きだけ実装するでも良さそうです。

----

以上、PreferenceFragmentCompatの読み書き先の変更方法でした。
先に紹介したようにXMLを記述するだけで簡単に設定画面を作ることができ、その読み書き先も最小限の実装で差し替えることができるので、柔軟な実装ができそうですね。