Android SharedPreferences Observing

이동현·2020년 10월 11일
1

안녕하세요. 이동현입니다.

어디서나 접근 가능한 간단한 지속성 데이터를 저장하는 방식으로 SharedPreferences를 사용 하곤 합니다.

최근 프로젝트를 진행하면서,
저장된 SharedPreference값이 바뀌는 것을 Observe하고, 그 즉시 데이터의 변경에 따른 View 단에서의 처리를 해야 할 일이 있었습니다.

LiveData를 통한 처리가 될 것 같았지만, 생각보다 쉽지 않았습니다.
구글링을 통해 방법을 알아냈고, 정리 + 기억 보존겸 포스팅을 하려고 합니다.

본 방법은 RxJava / RxAndroid를 사용합니다.
저는 이 부분을 학습해본적이 없어서 코드에 대한 분석보다는, 간단한 예제 정도만 보여주고 끝날 것 같습니다.

0. 샘플 앱에 대한 설명

EditText에 입력된 문자열을 '저장하기' Button을 눌러 SharedPreference의 Value로 저장합니다.
이 때, SharedPreference의 Value가 변경됨을 감지하여, 변경된 문자열을 별도의 TextView에 출력해서 사용자에게 보여주는 샘프 앱을 만듭니다.

이 때, Button의 onClick() 메서드 내에, TextView를 조작하는 과정이 없다는 것을 확인하면서 진행합니다.

activity_main.xml 의 코드는 아래와 같습니다.

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

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="64dp"
        android:text="SharedPreference 저장값"
        android:textSize="24sp" />

    <TextView
        android:id="@+id/tv_name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:textSize="48sp" />

    <EditText
        android:id="@+id/input_name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="64dp"
        android:hint="SharedPreference에\n저장할 값을 입력하세요"
        android:textSize="20sp" />

    <Button
        android:id="@+id/action_save"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:text="저장하기"
        android:textSize="16sp"/>

</LinearLayout>

1. RxJava/Android dependency

implementation 'io.reactivex.rxjava2:rxjava:2.1.9'
implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'

위의 dependency들을 gradle(app)에 넣어줍니다.

2. 사용할 SharedPreference 준비

1) SharedPreference

class SharedPreference(context: Context) {
    private val sharedPreference = 
   	context.getSharedPreferences("observe_test", MODE_PRIVATE)

    var name: String?
        get() = sharedPreference.getString("TEST_NAME", "")
        set(value) = sharedPreference.edit().putString("TEST_NAME", value).apply()
}

위와 같이 기본적인 SharedPreference를 만들어둡니다.

EditText에 입력된 값은 TEST_NAME을 Key로 가지도록 구성했습니다.
저장되는 파일의 이름은 observe_test입니다.

2) Application의 상속 클래스 작성

또한, SharedPreference를 어디에서나 사용하기 위해 Application을 상속받은 클래스에서 아래와 같은 작업을 해줍니다.

class App: Application() {
    companion object {
        lateinit var sharedPreference: SharedPreference
    }

    override fun onCreate() {
        super.onCreate()
        sharedPreference = SharedPreference(applicationContext)
    }
}

이제, Application.sharedPrefernce 의 방식으로 SharedPreference에 접근 가능합니다.

3) Manifests.xml 등록

마지막으로, manifests 파일에 만들어진 App Class를 등록합니다.

<application
    android:name="App"

3. 관찰 작업을 위한 LiveData Class의 확장

LiveData의 상속을 통해 Observer 사용을 Custom 할 수 있습니다.

아래와 같은 코드를 사용합니다.

class LivePreference<T> constructor(
    private val updates: Observable<String>,
    private val preferences: SharedPreferences,
    private val key: String,
    private val defaultValue: T
) : MutableLiveData<T>() {

    private var disposable: Disposable? = null

    override fun onActive() {
        super.onActive()

        value = (preferences.all[key] as T) ?: defaultValue

        disposable = updates
            .filter { t -> t == key }
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribeWith(object: DisposableObserver<String>() {
                override fun onComplete() {
                }

                override fun onNext(t: String) {
                    postValue((preferences.all[t] as T) ?: defaultValue)
                }

                override fun onError(e: Throwable) {
                }
            })
    }

    override fun onInactive() {
        super.onInactive()
        disposable?.dispose()
    }

}

만들어진 클래스를 보시면 MutableLiveData를 상속받고 있습니다.

관찰 상태에 따른 처리를 override 된 함수를 통해 할 수 있습니다.
관찰자가 존재한다면 onActive()가 호출되도록, 관찰 상태가 해체됨에 따라 onInactive()가 호출되도록 합니다.

특히, onActive()의 setValue()를 통해, 값을 업데이트하고 모든 관찰자에게 이 사실을 알릴 수 있습니다. 이는 기존의 LiveData에서의 observing과 같은 방식입니다.

4. Observable SharedPreference 만들기

실제로 Activity 에서 사용할 LiveSharedPreference를 만들어줍니다.
생성자로, Base가 될 SharedPreference를 받아줍니다.

class LiveSharedPreferences constructor(private val preferences: SharedPreferences) {
    private val publisher = PublishSubject.create<String>()
    private val listener = SharedPreferences
        .OnSharedPreferenceChangeListener { _, key -> publisher.onNext(key) }

    private val updates = publisher.doOnSubscribe {
        preferences.registerOnSharedPreferenceChangeListener(listener)
    }.doOnDispose {
        if (!publisher.hasObservers()) {
            preferences.unregisterOnSharedPreferenceChangeListener(listener)
        }
    }

    fun getString(key: String, defaultValue: String): LivePreference<String> {
        return LivePreference(updates, preferences, key, defaultValue)
    }
}

PublishSubject 이 부분이 RxJava를 사용한 방식입니다.
이해를 못한 부분이기도 합니다.

코드만 남기고, 후에 RxJava 학습이 된다면 부가적인 설명을 하겠습니다.


getString() 함수에 parameter로 key와 defaultValue를 받습니다.
사용된 함수는 앞서 만들었던 LivePreference 클래스를 return하며, observer를 달아줄 시 LiveData에서 동작하던 방식 그대로 값을 관찰합니다.

5. 사용하기

MainActivity에서의 사용입니다. 아래와 같이 사용합니다.

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

        val sharedPreference = 
            getSharedPreferences("observe_test", Context.MODE_PRIVATE)
        val liveSharedPreference = LiveSharedPreferences(sharedPreference)


        // Observer 달아주는 과정
        liveSharedPreference
            .getString("TEST_NAME", "")
            .observe(this, Observer<String> { result ->
                tv_name.text = result
            })


        // Button 클릭 리스너
        action_save.setOnClickListener {
            App.sharedPreference.name = input_name.text.toString()
        }
    }
}

LiveSharedPreferences의 생성자로 SharedPreference를 넣어야 합니다.
따라서, 각각의 프로퍼티를 만들어 초기화 시켜줍니다.

사용하는 방식은 주석으로 달아두었습니다.
만들어준 getString() 메서드를 사용하여 LiveData로 감싸진 String을 가져오며, 이를 observe하여 그에 따른 후속 작업을 처리하도록 설계되었습니다.

여기서 주의깊게 보아야 하는 부분은, Button의 클릭 리스너 등록 부분입니다.
이 부분에서는 오로지 SharedPreference의 문자열 값을 새로 Update 해주는 기능밖에 하지 않습니다.
문자열 값이 업데이트 되면, observer가 이를 감지하여, 변경된 값을 result 변수로 받아줍니다. 이후, TextView에 result 문자열을 띄워주게 됩니다.

실행 화면

정상적으로 작동하는 것을 확인할 수 있습니다.


사용법만 정리해두었고, 확실한 코드 이해가 언젠가 된다면 글을 수정 할 생각입니다.
(곧 SharedPreferences도 버려질 가능성이 크지만..)

profile
영차영차

0개의 댓글