Androidで設定画面を作ってみる

# Andoroidでシステム設定のような設定画面を作ってみる
Androidのアプリで設定画面を持っているものが有ります。AndroidX Preference Libraryを使って、Andoroid OSのシステム設定のような設定画面をアプリ内に作ってみましょう。

参考URL
本家Android develpers [設定](https://developer.android.com/guide/topics/ui/settings?hl=ja)
# どんな設定画面を作るか？
とりあえず、こんな設定画面を作ってみましょう。
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/933408/e5569687-768a-0bc0-c829-f0c0a9e7af8d.png">
設定値として色々な型が可能ですが、
* 文字列(何でも入力可)
* 文字列(数値のみ)
* リスト形式
* チェックボックス形式
* スィッチ形式(ON/OFF、YES/NO、True/False)

を作って見ましょう。
「文字列(何でも入力可)」と「文字列(数値のみ)」AndroidX Preference Libraryの型としてはEditTextPreferenceで一緒ですが、入力の時のキーパットが違います。
「文字列(何でも入力可)」は何でも入力できるキーパッドですが、
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/933408/a2a54604-dc88-3f5b-8471-b222f8d8718b.png">
「文字列(数値のみ)」の場合は数値しか入力できないキーパッドになります。ここで数値しか入らないことを保証しています。
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/933408/bb84eb21-9edb-20e1-040e-83b9c081bea7.png">
リスト形式の場合は、どれかひとつを選ぶダイアログが表示されます
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/933408/1ac8c0cf-d5e1-ee74-5faf-a65860a8a92b.png">
チェックボックス形式は上の図の通りですね。複数選択可能です。
スィッチ形式はONかOFFかのどちらかが選択可能です。
# build.gradleに依存関係の追加
build.gradleに依存性を追加します。
```gradle:build.gradle
dependencies {
    （中略）
    implementation 'androidx.preference:preference-ktx:1.2.0'
    implementation 'androidx.preference:preference:1.2.0'
    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
}
```

ここで、注意しないといけないのは、androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1を追加しないと、ビルド時に意味不明のエラーになります。
```
Duplicate class androidx.lifecycle.ViewModelLazy found in modules lifecycle-viewmodel-2.5.1-runtime (androidx.lifecycle:lifecycle-viewmodel:2.5.1) and lifecycle-viewmodel-ktx-2.3.1-runtime (androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1)
Duplicate class androidx.lifecycle.ViewTreeViewModelKt found in modules lifecycle-viewmodel-2.5.1-runtime (androidx.lifecycle:lifecycle-viewmodel:2.5.1) and lifecycle-viewmodel-ktx-2.3.1-runtime (androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1)

Go to the documentation to learn how to Fix dependency resolution errors.
```
色々ネットを検索して、上記の解決方法を見つけたのですが、正直、なんでこのビルドエラーになって、なんでこれで解消するのか、自分の力量では説明できません。（lifecycle関係なんか使ってないのに・・・androidx.preference:preferenceから芋づる式で入ってしまうんだろうか？）
# 設定画面のFragmentウィザードで雛形を一気に作る
AndroidStudioに設定画面のウィザートが用意されていてある程度の雛形がすぐ作成できます。
javaのフォルダを右クリック → 新規 → Fragment → Setting Fragment
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/933408/e68accb5-0ee2-1d4a-e6bd-6854535ca055.png">
設定画面のFragment作成のダイアログが表示されます。ここはデフォルトのままFragment名はSettingFragmentのままでいきます。
![Screenshot_20221126_205832.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/933408/644f0a7d-7944-7dca-f4b1-b53562036b1b.png)
完了をクリックすると、こんな感じで雛形ができあがっています。
![Screenshot_20221126_212351.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/933408/c4667a57-a21c-7e5d-4eae-cd0eb2cc750f.png)
* SettingFragment(フラグメント)
* arrays.xml(リスト形式で使う)
* root_preferences.xml(設定画面のデザインレイアウト)

がウィザードで作成されれているのがわかります。
root_preferences.xmlを開くとデザイナのviewが上がってきます。こんな感じで雛形ができています。
![Screenshot_20221126_210247.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/933408/fc9db56e-49e1-700b-1226-3a0111161ca0.png)
左上の「Palette」に設定画面に使用可能なパーツが出ています。ここから、その下の「Component Tree」にドラッグ＆ドロップして設定画面にパーツを追加していきます。パーツとして何が使用可能か、一通り見てみましょう。

「Commons」の下
![Screenshot_20221126_210313.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/933408/aeccd925-84dd-4a79-70f7-d0ffa36f79c5.png)
「Preferences」の下
![Screenshot_20221126_210329.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/933408/16e79031-2c8c-f4cc-7d11-27d1c6aaa1f2.png)
「Groups」の下
![Screenshot_20221126_210340.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/933408/6534acc4-62a4-13bc-e8e8-c352332e492b.png)
# 設定画面のデザイン
「Component Tree」の構造を見ると
* PreferenceScreen
  * PreferenceCategory
    * 設定画面のパーツ
    * ・・・
  * PreferenceCategory
    * 設定画面のパーツ
    * ・・・

といった階層構造になっているのがわかると思います。今回、上の設定画面を作るのに必要な設定画面のパーツは
| 設定画面のパーツ | class |
|:-:|:-:|
| 文字列(何でも入力可)  |  EditTextPreference |
| 文字列(数値のみ)  |  EditTextPreference |
| リスト形式  | ListPreference  |
| チェックボックス形式  | CheckBoxPreference  |
| スィッチ形式  |  SwitchPreference |

となります。では実際に雛形を参考にしながら設定画面のパーツを配置していきます。「ComponetTree」はこのようになります。
![Screenshot_20221127_113704.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/933408/bbcda44f-f8ee-bfb6-957d-6864e321a421.png)
実は、チェックボックス形式(CheckBoxPreference)は設定値としては1個毎にバラバラになるので、上の例のように3つの中から任意の個数選択可能だと3つ配置することになります。
リスト形式のリストの中身ですが、雛形で作られたarrys.xmlを参照しています。
![Screenshot_20221127_114324.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/933408/7177b841-87a0-c8ff-71bd-33ce4afda2a1.png)
arrays.xmlはこうなっています。
```xml:arrays.xml
<resources>
    <!-- Reply Preference -->
    <string-array name="list_entries">
        <item>設定1</item>
        <item>設定2</item>
        <item>設定3</item>
    </string-array>
    <string-array name="list_values">
        <item>1</item>
        <item>2</item>
        <item>3</item>
    </string-array>
</resources>
```
ListPreferenceのプロパティの「entries」が実際に設定画面で表示されるリストの内容、「entryValues」が選択された時に採用される値です。それがarrays.xmlとリンクしているのがわかるでしょうか。HTMLのSELECTタグみたいな仕組みになっています。

できあがったroot_preferences.xmlはこのようになります。
```xml:root_preferences.xml
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <PreferenceCategory app:title="@string/textCatTitle">
        <EditTextPreference
            app:key="textPreference"
            app:title="@string/textPrefTitle"
            app:useSimpleSummaryProvider="true" />
    </PreferenceCategory>
    <PreferenceCategory android:title="@string/numberCatTitle">
        <EditTextPreference
            android:defaultValue="0"
            android:key="numberPreference"
            android:selectAllOnFocus="true"
            android:singleLine="true"
            android:title="@string/numberPrefTitle"
            app:useSimpleSummaryProvider="true" />
    </PreferenceCategory>
    <PreferenceCategory app:title="@string/listCatTitle">
        <ListPreference
            app:entries="@array/list_entries"
            app:entryValues="@array/list_values"
            app:key="listPreference"
            app:title="@string/listPrefTitle"
            app:useSimpleSummaryProvider="true" />
    </PreferenceCategory>
    <PreferenceCategory android:title="@string/checkCatTitle">
        <CheckBoxPreference
            android:defaultValue="false"
            android:key="checkboxPreference1"
            android:title="@string/checkPrefTtile1" />
        <CheckBoxPreference
            android:defaultValue="false"
            android:key="checkboxPreference2"
            android:title="@string/checkPrefTtile2" />
        <CheckBoxPreference
            android:defaultValue="false"
            android:key="checkboxPreference3"
            android:title="@string/checkPrefTtile3" />
    </PreferenceCategory>
    <PreferenceCategory android:title="@string/switchCatTitle">
        <SwitchPreferenceCompat
            app:defaultValue="false"
            app:key="switchPreference"
            app:summaryOff="@string/summaryOff"
            app:summaryOn="@string/summaryOn"
            app:title="@string/switchPrefTitle" />
    </PreferenceCategory>
</PreferenceScreen>
```
# SettingFragment.ktの修正
上でも説明しましたが、「文字列(何でも入力可)」、「文字列(数値のみ) 」は設定パーツ的にはEditTextPreferenceで同じです。ここで、「文字列(数値のみ) 」は数値のみ入力可能なキーパッドが表示されるように細工をします。
```kotlin:SettingFragment.kt
    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
        setPreferencesFromResource(R.xml.root_preferences, rootKey)
        //  入力を数値のみに限定する
        val numberPreference = findPreference<EditTextPreference>("numberPreference")
        numberPreference?.setOnBindEditTextListener { editText ->
            editText.inputType = InputType.TYPE_CLASS_NUMBER
        }
    }

```
onCreatePreferencesメソッドはウィザードの雛形ですでにできあがっているので、そこに追加します。
# 設定を表示する画面も作ってみる
設定を書き込んだだけじゃ、画面で確認する方法がないので、設定を表示する画面も作ってみます。少々、雑ですが、こんな画面を作ります。
<img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/933408/cf3fdf3e-8f90-0146-6bf7-cb76f66e8c85.png">
アクションバーにアイコンを2つ配置して、スパナのアイコンをタップするとSettingFragmentの画面へ、Uターンのアイコンをタップするとこの表示画面へ、お互いに行き来できるようにしてみました。
```kotlin:DisplayFragment.kt
class DisplayFragment : Fragment() {
   private lateinit var binding: FragmentDisplayBinding
   override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        binding = FragmentDisplayBinding.inflate(inflater, container, false)
        return binding.root
    }
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val sharedPreference = activity?.let { PreferenceManager.getDefaultSharedPreferences(it) }
        binding.textPref.text = sharedPreference?.getString("textPreference", "") ?: ""
        binding.numberPref.text = sharedPreference?.getString("numberPreference", "") ?: ""
        binding.listPref.text = sharedPreference?.getString("listPreference", "") ?: ""
        binding.checkPref1.text = sharedPreference?.getBoolean("checkboxPreference1", false) .toString()
        binding.checkPref2.text = sharedPreference?.getBoolean("checkboxPreference2", false) .toString()
        binding.checkPref3.text = sharedPreference?.getBoolean("checkboxPreference3", false) .toString()
        binding.switchPref.text = sharedPreference?.getBoolean("switchPreference", false) .toString()
    }
}
```
onViewCreatedメソッドにSharedPreferenceから値を取得して表示する処理を入れています。
# MainActivity
MainActivityは最初にDisplayFragmentを表示して、アクションバーのアイコンタップによってどっちのFragmentに遷移するかの処理をいれます。
```kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
   override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater).apply {
            setContentView(this.root)
        }
        supportFragmentManager
            .beginTransaction()
            .replace(R.id.fragmentContainerView, DisplayFragment())
            .commit()
    }
    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        menuInflater.inflate(R.menu.menu, menu)
        return true
    }
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        when (item.itemId) {
            R.id.setting -> {
                supportFragmentManager
                    .beginTransaction()
                    .replace(R.id.fragmentContainerView, SettingsFragment())
                    .commit()
            }
            R.id.disp -> {
                supportFragmentManager
                    .beginTransaction()
                    .replace(R.id.fragmentContainerView, DisplayFragment())
                    .commit()
            }
            else -> {}
        }
        return true
    }
}
```

入力した設定値はAndroid端末内のSharedPreferenceとしてxmlで保存されますので、アプリを終了しても、端末を再起動しても残っています。AndroidStudioのDeviceFileExploerで見ると
**/data/data/{パッケージ名}/shared_prefs**
の下にxmlでこんな風に保存されます。
```xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="listPreference">2</string>
    <boolean name="checkboxPreference1" value="false" />
    <boolean name="switchPreference" value="true" />
    <string name="numberPreference">123</string>
    <boolean name="checkboxPreference3" value="true" />
    <string name="textPreference">ああああ</string>
    <boolean name="checkboxPreference2" value="false" />
</map>
```
# 最後に
以上で、一通り説明しました。完成形は[gitHub](https://github.com/cozyk100/AndroidStudioProjectsPublic/tree/main/SettingKotlin)に置きました。