[Android] SharedPreferences

leeeha·2022년 10월 30일
1
post-thumbnail

Concepts of Persistence

데이터 저장

  • File Read/Write (보통 이미지 파일)
  • SharedPreference, Database (보통 문자열, 숫자, Boolean)

File

  • 자바 API 사용
  • File : 파일 및 디렉토리를 지칭하는 클래스
  • FileInputStream / FileOutputStream : 파일에서 바이트 스트림으로 데이터를 읽거나 쓰는 클래스
  • FileReader / FileWriter : 파일에서 문자열 스트림으로 데이터를 읽거나 쓰는 클래스
  • 파일이 저장되는 곳을 내장 메모리 공간과 외장 메모리 공간으로 구분
  • 외장 메모리 공간은 앱별 저장 공간과 공용 저장 공간으로 구분
  • 공용 저장 공간은 여러 앱이 공용으로 사용하는 저장 공간을 의미함.

내장 메모리 공간

  • 앱이 설치되면 시스템에서 자동으로 할당하는 메모리 공간
  • 앱의 패키지명으로 폴더가 생성됨. (패키지명은 어플리케이션의 식별자)
  • 내장 메모리 공간에 저장되는 파일을 위해서는 File 객체를 만들 때 Context 객체의 filesDir 프로퍼티를 사용함.
val file = File(filesDir, "test.txt") // 내장 메모리 공간에 파일 객체 할당  
val writeStream: OutputStreamWriter = file.writer() 
writeStream.write("hello world") 
writeStream.flush() 

SharedPreferences

기본 형태

  • 데이터를 key-value 형태로 저장
  • 내부적으로 내장 메모리의 앱 폴더XML 파일로 데이터가 저장됨. (문자열, 숫자, Boolean 값을 저장하기 편리함.)
  • Activity.getPreferences(int mode)
  • 하나의 액티비티를 위한 데이터 저장을 목적으로 하는 경우
  • 이 함수를 이용한 액티비티 클래스명으로 XML 파일명이 만들어짐.
  • Context.getSharedPreferences(String name, int mode)
  • 앱 전체의 데이터를 key-value 형태로 저장
val sharedPref = getSharedPreferences("my_prefs", Context.MODE_PRIVATE)

데이터 저장 (put data)

  • 데이터 저장을 위해서는 SharedPreferences.Editor 클래스의 함수 이용
  • SharedPreferences.Editor 객체는 SharedPreferences의 edit() 함수로 획득
  • commit() 하는 순간에 데이터를 저장함.
putBoolean(String key, boolean value)
putInt(String key, int value)
putFloat(String key, float value)
putLong(String key, long value)
putString(String key, String value) 

데이터 획득 (get data)

  • 저장된 데이터를 획득할 때는 SharedPreferences의 getter 함수 이용
getBoolean(String key, boolean defValue)
getInt(String key, int defValue)
getFloat(String key, float defValue)
getLong(String key, long defValue)
getString(String key, String defValue) 

실습 예제

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:orientation="vertical"
    android:gravity="center">

    <EditText
        android:id="@+id/editView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        />
    <CheckBox
        android:id="@+id/checkView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="check"/>

    <Button
        android:id="@+id/saveButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="SAVE"/>
    <Button
        android:id="@+id/getButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Get Data"/>
    <TextView
        android:id="@+id/resultView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>


</LinearLayout>
package com.tutorial.c55

import android.content.Context
import android.os.Bundle
import android.widget.Button
import android.widget.CheckBox
import android.widget.EditText
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val editView = findViewById<EditText>(R.id.editView)
        val checkView = findViewById<CheckBox>(R.id.checkView)
        val saveButton = findViewById<Button>(R.id.saveButton)
        val getButton = findViewById<Button>(R.id.getButton)
        val resultView = findViewById<TextView>(R.id.resultView)

        // 앱 전체의 데이터 저장을 위해 SharedPreferences 객체 생성  
        val sharedPref = getSharedPreferences("my_prefs",
            Context.MODE_PRIVATE)

        // sharedPref에 데이터 저장 
        saveButton.setOnClickListener {
            sharedPref.edit().run {
                putString("data1", editView.text.toString())
                putBoolean("data2", checkView.isChecked)
                commit()
            }
        }

		// sharedPref에 저장된 데이터 획득 
        getButton.setOnClickListener {
            val data1 = sharedPref.getString("data1", "none")
            val data2 = sharedPref.getBoolean("data2", false)
            resultView.text = "data1: $data1, data2: $data2"
        }
    }
}

Settings XML

  • 앱의 환경설정 자동화를 위한 XML 파일

  • API Level 29 이전 버전에서는 PreferenceFragment 이용
  • API Level 29 버전부터는 AndroidX Preference 사용을 권장

implementation 'androidx.preference:preference-ktx:${version}'

settings.xml 생성

  • res/xml 폴더에 설정과 관련된 XML 파일 작성

  • 루트 태그가 <PreferenceScreen>
  • <SwitchPreferenceCompat>, <Preference> 등의 태그를 이용해 각각의 설정 항목을 준비
<PreferenceScreen xmins:app="http://schemas.android.com/apk/res-auto">
  <SwitchPreferenceCompat
    app:key="notifications"
    app:title="Enable message notifications"/> 
</PreferenceScreen> 

settings.xml 적용

  • PreferenceFragmentCompat를 상속받은 Fragment로 settings.xml 파일 적용
class SettingsFragment: PreferenceFragmentCompat() {
	override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
    setPreferencesFromResource(R.xml.settings, rootKey) 
}

이렇게 settings.xml 파일만 적용해주면, 설정 화면의 UI와 이벤트 처리, 그리고 데이터 저장까지 모두 자동화 된다.

설정 태그의 종류

실습 예제

build.gradle (모듈 단위)

implementation 'androidx.preference:preference-ktx:1.2.0'

cf) 위의 dependency를 추가했더니
Duplicate class androidx.lifecycle.ViewModelLazy found in modules jetified-lifecycle-viewmodel-ktx-2.3.1-runtime (androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1) and lifecycle-viewmodel-2.5.1-runtime (androidx.lifecycle:lifecycle-viewmodel:2.5.1)
이런 에러가 발생했다.
👉 이 링크를 참고하여 implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1' 이 코드를 추가했더니 에러가 사라졌다. preference와 viewmodel이 관련있는 걸까..?! 정확한 이유를 모르겠다.....

setting.xml

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
    <PreferenceCategory
        android:key="a_category"
        android:title="A Setting">
        <SwitchPreferenceCompat
            android:key="a1"
            android:title="A - 1 Setting"/>
        <SwitchPreferenceCompat
            android:key="a2"
            android:title="A - 2 Setting"/>
    </PreferenceCategory>

    <PreferenceCategory
        android:key="b_category"
        android:title="B Setting">
        <SwitchPreferenceCompat
            android:key="b1"
            android:title="B - 1 Setting"/>
        <SwitchPreferenceCompat
            android:key="b2"
            android:title="B - 2 Setting"/>
    </PreferenceCategory>
</PreferenceScreen>

SettingsFragment.kt

package com.tutorial.c56

import android.os.Bundle
import androidx.preference.PreferenceFragmentCompat

class SettingsFragment : PreferenceFragmentCompat() {
    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
        setPreferencesFromResource(R.xml.settings, rootKey)
    }
}

activitiy_main.xml

<?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:name="com.tutorial.c56.SettingsFragment" />

앱을 종료시켰다가 다시 실행해도 설정 값이 그대로 유지되는 것을 확인할 수 있다!


PreferenceFragmentCompat

  • 설정을 위한 XML 파일만 만들어서 프래그먼트에 적용시켜주면, 설정 화면을 띄울 수 있고 유저가 설정한 값은 앱을 종료시켜도 유지됨.
  • 그런데, 어떤 경우에는 settings.xml 파일의 내용을 개발자 코드 상에서 제어할 필요가 있을 때가 있음.
    ex) 설정 항목을 클릭하는 순간의 이벤트를 처리하고 싶거나, 코드가 실행되어 발생한 결과를 설정 항목의 title이나 summary로 지정하고 싶은 경우

findPreference()

설정을 위해 사용한 태그에 해당되는 객체를 findPreference() 함수로 획득

<EditTextPreference
   android:key="id" 
   android:title="ID 설정" 
   android:isPreferenceVisible="false"/>
val idPref: EditTextPreference? = findPreference("id")
idPref?.isVisible = true 

SimpleSummaryProvider

  • <EditTextPreference>, <ListPreference>에 의한 설정값을 화면에 출력하려면, SimpleSummaryProvider 이용
    ex) 글을 입력하세요. → Summary에 사용자의 입력값을 보여줌.
soundPreference!!.setSummaryProvider(ListPreference.SimpleSummaryProvider.getInstance()) 
  • SummaryProvider의 하위 클래스를 만들어서 개발자가 원하는대로 summary를 지정할 수 있음.

이벤트 처리 (클릭, 변경)

  • 설정 항목을 클릭한 이벤트에 대한 처리는 setOnPreferenceClickListener() 이용
idPref?.setOnPreferenceClickListener {
  true 
}
  • 유저가 설정을 변경한 순간을 감지하여, 바뀐 값을 사용해야 하는 경우
    • Preference.OnPreferenceChangeListener : 각 Preference 객체에 적용
    • SharedPreferences.OnSharedPreferenceChangeListener : 모든 설정 객체의 변경을 하나의 이벤트 핸들러에서 감지

실습 예제

앱을 나갔다가 다시 들어와도 설정값이 그대로 유지되어 있음을 확인할 수 있다!

build.gradle (모듈 단위)

implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
implementation 'androidx.preference:preference-ktx:1.2.0'

res/xml/settings.xml

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
    <SwitchPreferenceCompat
        android:key="server_flag"
        android:summaryOff="서버 연동 비활성화 상태"
        android:summaryOn="서버 연동 활성화 상태"
        android:title="서버 연동"/>
    <PreferenceCategory
        android:dependency="server_flag"
        android:title="서버 연동 정보">
        <EditTextPreference
            android:key="server_id"
            android:title="ID"/>
        <ListPreference
            android:key="sound_list"
            android:title="서버 메시지 알림음"
            android:entries="@array/array_voice"
            android:entryValues="@array/array_voice"/>
    </PreferenceCategory>
</PreferenceScreen>

res/values/arrays.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name="array_voice">
        <item>카톡</item>
        <item>카톡왔숑</item>
        <item>카카오톡</item>
    </string-array>
</resources>

SettingsFragment.kt

package com.tutorial.c57

import android.os.Bundle
import android.text.TextUtils
import android.widget.Toast
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat

class SettingsFragment : PreferenceFragmentCompat() {
    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
        setPreferencesFromResource(R.xml.settings, rootKey)

        val idPref = findPreference<EditTextPreference>("server_id")
        val soundPref = findPreference<ListPreference>("sound_list")

        // id 값이 변경되는 경우
        idPref?.setOnPreferenceChangeListener { preference, newValue ->
            Toast.makeText(activity, "새로운 값: $newValue", Toast.LENGTH_SHORT).show()
            true
        }

        // soundPref는 summary 그대로 출력
        soundPref!!.setSummaryProvider(ListPreference.SimpleSummaryProvider.getInstance())

        // idPref는 summary를 커스텀하여 출력
        idPref!!.summaryProvider =
            Preference.SummaryProvider<EditTextPreference> { preference ->
                val text = preference.text
                if(TextUtils.isEmpty(text)){
                    "id 값이 설정되지 않았습니다."
                }else{
                    "설정된 값은 $text 입니다."
                }
            }
    }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
    android:name="com.tutorial.c57.SettingsFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>
profile
습관이 될 때까지 📝

0개의 댓글