이번 영상에서는 DataStore를 이용해 앱 설정화면을 만드는 법을 알아보도록 하겠습니다.
우선은 Preferences DataStore Dependency를 추가합니다.
// DataStore
implementation 'androidx.datastore:datastore-preferences:1.0.0'
다음은 fragment_settings.xml 화면을 구성합니다. 책 검색 API에는 sort 라는 파라미터가 있는데 "accuracy"는 정확도순, "latest"는 발간일순으로 검색해주게 되고 입력이 없을 경우 기본값은 "accuracy"로 설정되어 있습니다. 이 정렬방식을 전환하는 라디오 버튼을 설정화면에 만들어주도록 하겠습니다.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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=".ui.fragment.SettingsFragment">
<TextView
android:id="@+id/tv_title_sort"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="40dp"
android:layout_marginEnd="16dp"
android:text="Sort by"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<RadioGroup
android:id="@+id/rg_sort"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="20dp"
android:layout_marginEnd="16dp"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_title_sort">
<RadioButton
android:id="@+id/rb_accuracy"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Accuracy" />
<RadioButton
android:id="@+id/rb_latest"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Latest" />
</RadioGroup>
</androidx.constraintlayout.widget.ConstraintLayout>
그리고 Sort 파라미터는 enum class로 사용할 수 있게 준비합니다.
enum class Sort(val value: String) {
ACCURACY("accuracy"),
LATEST("latest")
}
다음은 값을 저장하고 불러오는 함수를 리포지토리에 정의합니다. 우선은 BookSearchRepository 인터페이스에 함수명세를 추가합니다.
interface BookSearchRepository {
...
+ // DataStore
+ suspend fun saveSortMode(mode: String)
+ suspend fun getSortMode(): Flow<String>
}
다음은 BookSearchRepositoryImpl에서 이 함수를 구현합니다.
class BookSearchRepositoryImpl(
+ private val dataStore: DataStore<Preferences>
) : BookSearchRepository {
...
+ // Datastore
+ private object PreferencesKeys {
+ val SORT_MODE = stringPreferencesKey("sort_mode")
+ }
+ override suspend fun saveSortMode(mode: String) {
+ dataStore.edit { prefs ->
+ prefs[SORT_MODE] = mode
+ }
+ }
+ override suspend fun getSortMode(): Flow<String> {
+ return dataStore.data
+ .catch { exception ->
+ if (exception is IOException) {
+ exception.printStackTrace()
+ emit(emptyPreferences())
+ } else {
+ throw exception
+ }
+ }
+ .map { prefs ->
+ prefs[SORT_MODE] ?: Sort.ACCURACY.value
+ }
+ }
}
dataStore 객체를 초기값으로 받고 저장 및 불러오기에 사용할 Key를 PreferencesKeys에 지정해 줍니다. 그냥 String을 사용하던 SharedPreferences와 다르게, DataStore는 타입 안전을 위해 Preferences.Key를 사용해야 합니다. 우리가 저장할 데이터 타입은 String이므로 stringPreferencesKey를 사용합니다.
다음은 saveSortMode를 작성합니다. 저장하는 작업은 코루틴 안에서 이루어져야 하므로 함수에 suspend를 붙였고 전달받은 mode 값은 edit 블록 안에서 저장하면 됩니다.
그리고 getSortMode를 정의합니다. 파일에 접근하기 위해서는 data 메소드를 사용하는데요, 파일접근에 실패할 경우를 대비해 catch로 예외처리를 하고 map 블록 안에서 키를 전달하여 Flow를 반환받으면 됩니다. 기본값은 "accuracy"가 반환되도록 하였습니다.
다음은 ViewModel을 설정합니다. 우선 값을 저장하는 saveSortMode를 정의합니다. 리포지토리의 saveSortMode를 viewModelScope 내부에서 실행되도록 합니다.
값을 불러오는 getSortMode는 리포지토리의 getSortMode를 실행합니다. 설정값의 특성상 사용할 때 전체 데이터 스트림을 구독할 필요가 없습니다. 그래서 Flow에서 단일 String 값을 가져오도록 first를 붙여준 뒤, 반드시 값을 반환하고 종료되는 withContext 내부에서 실행되도록 하고, 파일 IO 작업이므로 IO 디스패처에서 실행되도록 정의했습니다.
// DataStore
fun saveSortMode(value: String) = viewModelScope.launch {
bookSearchRepository.saveSortMode(value)
}
suspend fun getSortMode() = withContext(Dispatchers.IO) {
bookSearchRepository.getSortMode().first()
}
그리고 searchBooks가 getSortMode를 이용하도록 변경합니다.
fun searchBooks(query: String) = viewModelScope.launch {
+ val response = bookSearchRepository.searchBooks(query, getSortMode(), 1, 15)
_searchResult.postValue(response.body())
}
이제 SettingsFragment를 작성합니다.
class SettingsFragment : Fragment() {
...
+ private lateinit var bookSearchViewModel: BookSearchViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
+ bookSearchViewModel = (activity as MainActivity).bookSearchViewModel
+ saveSettings()
+ loadSettings()
}
+ private fun saveSettings() {
+ binding.rgSort.setOnCheckedChangeListener { _, checkedId ->
+ val value = when (checkedId) {
+ R.id.rb_accuracy -> Sort.ACCURACY.value
+ R.id.rb_latest -> Sort.LATEST.value
+ else -> return@setOnCheckedChangeListener
+ }
+ bookSearchViewModel.saveSortMode(value)
+ }
+ }
+ private fun loadSettings() {
+ lifecycleScope.launch {
+ val buttonId = when (bookSearchViewModel.getSortMode()) {
+ Sort.ACCURACY.value -> R.id.rb_accuracy
+ Sort.LATEST.value -> R.id.rb_latest
+ else -> return@launch
+ }
+ binding.rgSort.check(buttonId)
+ }
+ }
...
}
MainActivity에서 ViewModel을 전달받고 saveSettings와 loadSettings를 정의합니다. saveSettings에서는 체크된 버튼을 확인한 뒤 해당하는 Sort 값을 저장합니다. loadSettings에서는 불러온 값을 확인한 뒤 라디오버튼에 반영하는 기능을 구현하면 됩니다.
우선 DataStore DB에 사용할 키를 만들어 줍니다.
object Constants {
+ const val DATASTORE_NAME = "preferences_datastore"
}
그리고 MainActivity에서 dataStore 싱글톤 인스턴스를 작성한 뒤 리포지토리에 전달해주면 세팅화면을 사용할 수 있게 됩니다.
class MainActivity : AppCompatActivity() {
...
+ private val Context.dataStore by preferencesDataStore(DATASTORE_NAME)
override fun onCreate(savedInstanceState: Bundle?) {
...
val database: BookSearchDatabase = BookSearchDatabase.getInstance(this)
+ val bookSearchRepository = BookSearchRepositoryImpl(database, dataStore)
val factory = BookSearchViewModelProviderFactory(bookSearchRepository)
bookSearchViewModel = ViewModelProvider(this, factory)[BookSearchViewModel::class.java]
}
}