Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor: Preferences Datastore 적용 #15

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,6 @@ dependencies {

// jetpack navi
implementation(libs.bundles.jetpack.navi)
// sharedPreference crypto
implementation(libs.security.crypto)
// json
implementation(libs.kotlinx.serialization.json)
// google
Expand All @@ -63,4 +61,8 @@ dependencies {
kapt(libs.dagger.hilt.compiler)
// 기초 androidx 라이브러리 ("core-ktx", "constraintlayout", "appcompat", "activity")
implementation(libs.bundles.androidx)

// preferences Datastore
implementation(libs.datastore.preferences)
implementation(libs.datastore.core)
}
18 changes: 7 additions & 11 deletions app/src/main/java/com/sopt/now/di/SharedPreferenceModule.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package com.sopt.now.di

import android.content.Context
import android.content.SharedPreferences
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import androidx.datastore.preferences.preferencesDataStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
Expand All @@ -14,16 +12,14 @@ import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object SharedPreferenceModule {
private const val PREFERENCE_NAME = "chanu_dataStore"
private val Context.datastore by preferencesDataStore(
name = PREFERENCE_NAME
)

@Provides
@Singleton
fun providesLocalPreferences(
@ApplicationContext context: Context
): SharedPreferences =
EncryptedSharedPreferences.create(
context,
context.packageName,
MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build(),
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
) = context.datastore
}
Comment on lines 12 to 25
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거 모듈 data모듈로 이동해 주세요!
지금은 app모듈, data모듈 둘다 라이브러리에 의존해야하는데 data모듈에서 필요할때만 사용해도 될것 같아요

5 changes: 3 additions & 2 deletions data/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,14 @@ android {
dependencies {
implementation(project(":domain"))

// sharedPreference
implementation(libs.core.ktx)
// json
implementation(libs.kotlinx.serialization.json)
// dagger hilt
implementation(libs.dagger.hilt)
kapt(libs.dagger.hilt.compiler)

implementation(libs.retrofit2)

// preferences Datastore
implementation(libs.datastore.preferences)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import com.sopt.now.data.dto.UserDto

interface SharedPreferenceDataSource {
var checkLogin: Boolean
fun saveUserInfo(userDto: UserDto?)
fun getUserInfo(): UserDto

fun clear()
suspend fun saveUserInfo(userDto: UserDto?)
suspend fun getUserInfo(): UserDto
suspend fun clear()
Comment on lines +7 to +9
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suspend를 사용해도 되고 타입을 Flow타입으로 감싸주어도 될것 같습니다!
전 후자로 진행했어요

}
Original file line number Diff line number Diff line change
@@ -1,35 +1,75 @@
package com.sopt.now.data.datasourceimpl

import android.content.SharedPreferences
import androidx.core.content.edit
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.core.stringPreferencesKey
import com.sopt.now.data.datasource.SharedPreferenceDataSource
import com.sopt.now.data.dto.UserDto
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.io.IOException
import javax.inject.Inject

class SharedPreferenceDataSourceImpl @Inject constructor(
private val sharedPreferences: SharedPreferences
private val dataStore: DataStore<Preferences>
) : SharedPreferenceDataSource {
override var checkLogin: Boolean
get() = sharedPreferences.getBoolean(CHECK_LOGIN, false)
set(value) = sharedPreferences.edit { putBoolean(CHECK_LOGIN, value) }
get() = runBlocking {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

runBlocking을 사용할때 유의해야할 점은 뭘까요 ?

dataStore.data
.catch { exception ->
if (exception is IOException) {
emit(emptyPreferences())
} else {
throw exception
}
}
.map { preference ->
preference[stringPreferencesKey(CHECK_LOGIN)]?.toBoolean() ?: false
}.first()
}
set(value) {
runBlocking {
dataStore.edit { preferences ->
preferences[stringPreferencesKey(CHECK_LOGIN)] =
value.toString()
}
}
}

override fun saveUserInfo(userDto: UserDto?) {
override suspend fun saveUserInfo(userDto: UserDto?) {
val json = Json.encodeToString(userDto)
sharedPreferences.edit { putString(USER_INFO, json) }
dataStore.edit { preferences ->
preferences[stringPreferencesKey(USER_INFO)] = json
}
}

override fun getUserInfo(): UserDto {
val json = sharedPreferences.getString(USER_INFO, "")
if (json.isNullOrBlank()) return UserDto("", "", "", "")
override suspend fun getUserInfo(): UserDto {
val json = dataStore.data
.catch { exception ->
if (exception is IOException) {
emit(emptyPreferences())
} else {
throw exception
}
}
.map { preference ->
preference[stringPreferencesKey(USER_INFO)] ?: ""
}.first()

if (json.isBlank()) return UserDto("", "", "", "")
return Json.decodeFromString(json)
}

override fun clear() {
sharedPreferences.edit {
remove(USER_INFO)
remove(CHECK_LOGIN)
override suspend fun clear() {
dataStore.edit { preferences ->
preferences.remove(stringPreferencesKey(USER_INFO))
preferences.remove(stringPreferencesKey(CHECK_LOGIN))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import javax.inject.Inject
class UserInfoRepositoryImpl @Inject constructor(
private val sharedPreferenceDataSource: SharedPreferenceDataSource
) : UserInfoRepository {
override fun saveUserInfo(user: UserEntity) {
override suspend fun saveUserInfo(user: UserEntity) {
sharedPreferenceDataSource.saveUserInfo(
userDto = UserDto(
id = user.id,
Expand All @@ -24,15 +24,15 @@ class UserInfoRepositoryImpl @Inject constructor(
sharedPreferenceDataSource.checkLogin = isChecked
}

override fun getUserInfo(): UserEntity {
override suspend fun getUserInfo(): UserEntity {
return sharedPreferenceDataSource.getUserInfo().toUserEntity()
}

override fun getCheckLogin(): Boolean {
return sharedPreferenceDataSource.checkLogin
}

override fun clear() {
override suspend fun clear() {
sharedPreferenceDataSource.clear()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ package com.sopt.now.domain.repository
import com.sopt.now.domain.entity.UserEntity

interface UserInfoRepository {
fun saveUserInfo(user: UserEntity)
suspend fun saveUserInfo(user: UserEntity)
fun saveCheckLogin(isChecked: Boolean)
fun getUserInfo(): UserEntity
suspend fun getUserInfo(): UserEntity
fun getCheckLogin(): Boolean
fun clear()
suspend fun clear()
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import javax.inject.Inject
class ClearUserInfoUseCase @Inject constructor(
private val userInfoRepository: UserInfoRepository
) {
operator fun invoke() {
suspend operator fun invoke() {
userInfoRepository.clear()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import javax.inject.Inject
class GetUserInfoUseCase @Inject constructor(
private val userInfoRepository: UserInfoRepository
) {
operator fun invoke(): UserEntity {
suspend operator fun invoke(): UserEntity {
return userInfoRepository.getUserInfo()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import javax.inject.Inject
class SaveUserInfoUseCase @Inject constructor(
private val userInfoRepository: UserInfoRepository
) {
operator fun invoke(user: UserEntity) {
suspend operator fun invoke(user: UserEntity) {
userInfoRepository.saveUserInfo(user)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class SignUpViewModel @Inject constructor(
checkValidateUser()
}

fun saveUserInfoSharedPreference(input: UserEntity) {
suspend fun saveUserInfoSharedPreference(input: UserEntity) {
saveUserInfoUseCase.invoke(input)
}

Expand Down
26 changes: 16 additions & 10 deletions feature/src/main/java/com/sopt/now/feature/mypage/MyPageFragment.kt
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
package com.sopt.now.feature.mypage

import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import com.sopt.now.core.base.BindingFragment
import com.sopt.now.core.util.fragment.toast
import com.sopt.now.core.util.intent.navigateTo
import com.sopt.now.feature.R
import com.sopt.now.feature.auth.LoginActivity
import com.sopt.now.feature.databinding.FragmentMyPageBinding
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch

@AndroidEntryPoint
class MyPageFragment : BindingFragment<FragmentMyPageBinding>(R.layout.fragment_my_page) {
private val viewModel by viewModels<MyPageViewModel>()
override fun initView() {
initBtnClickListener()
initUpdateUserDataUI()
lifecycleScope.launch {
initBtnClickListener()
initUpdateUserDataUI()
}
}

private fun initBtnClickListener() {
Expand All @@ -32,18 +36,20 @@ class MyPageFragment : BindingFragment<FragmentMyPageBinding>(R.layout.fragment_

private fun initClearInfoBtnClickListener() {
binding.tvMainClearInfo.setOnClickListener {
viewModel.clearSharedPrefUserInfo()
toast(
getString(
R.string.login_completed,
getString(R.string.main_clear_user_under_bar)
lifecycleScope.launch {
viewModel.clearSharedPrefUserInfo()
toast(
getString(
R.string.login_completed,
getString(R.string.main_clear_user_under_bar)
)
)
)
navigateTo<LoginActivity>(requireContext())
navigateTo<LoginActivity>(requireContext())
}
}
}

private fun initUpdateUserDataUI() = with(binding) {
private suspend fun initUpdateUserDataUI() = with(binding) {
viewModel.getSharedPrefUserInfo().apply {
tvMainIdData.text = id
tvMainPwdData.text = password
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ class MyPageViewModel @Inject constructor(
private val clearUserInfoUseCase: ClearUserInfoUseCase
) : ViewModel() {

fun getSharedPrefUserInfo(): UserEntity = getUserInfoUseCase.invoke()
suspend fun getSharedPrefUserInfo(): UserEntity = getUserInfoUseCase.invoke()
fun updateCheckLoginState(isAutoLogin: Boolean) {
saveCheckLoginUseCase.invoke(isAutoLogin)
}

fun clearSharedPrefUserInfo() = clearUserInfoUseCase.invoke()
suspend fun clearSharedPrefUserInfo() = clearUserInfoUseCase.invoke()
}
3 changes: 3 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ fragment-ktx = "1.6.2"
security-crypto = "1.1.0-alpha06"
jetpack-navi = "2.7.7"
paging = "3.1.1"
datastore = "1.0.0-alpha07"

# kotlin
kotlin = "1.9.0"
Expand Down Expand Up @@ -55,6 +56,8 @@ navigation-ui-ktx = { group = "androidx.navigation", name = "navigation-ui-ktx",
paging = { module = "androidx.paging:paging-runtime-ktx", version.ref = "paging" }
paging-domain = { module = "androidx.paging:paging-common", version.ref = "paging" }
kotlin-coroutines = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }
datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore"}
datastore-core = {group = "androidx.datastore", name = "datastore-core", version.ref = "datastore"}

# Google
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
Expand Down