I have a problem with android datastore.
I dont know that I can't write or I can't read but it doesn't work any way
here is my code:
class DataStoreProvider(private val context: Context) {
private val Context.dataStore: DataStore<Preferences> by
preferencesDataStore("settings")
private val phoneNumberKey = stringPreferencesKey("phoneNumberPreferencesKey")
private val passwordKey = stringPreferencesKey("passwordPreferencesKey")
fun readPhoneNumber(): String? {
var phoneNumber: String? = null
context.dataStore.data
.map { preferences ->
phoneNumber = preferences[phoneNumberKey]
}
return phoneNumber
}
suspend fun savePhoneNumber(phoneNumber: String) {
context.dataStore.edit { setting ->
setting[phoneNumberKey] = phoneNumber
}
}
}
I call these functions from viewModelScope.launch function on Dispatchers.IO.
and I use 1.0.0 version of data store
any idea what should I do?
Refactor your readPhoneNumber like this:
suspend fun readPhoneNumber(): String? {
return context.dataStore.data
.map { preferences -> preferences[phoneNumberKey] ?: null }
.first()
}
Related
I am working on app settings. I am doing this in Jetpack Compose. However, when the switch button is clicked and changed to true or false, this is printed out during debug, however, the settings don't appear to change nor be saved. No way to confirm this.
Not sure of LaunchEffect{} shall be used.
AppSettings:
import kotlinx.serialization.Serializable
#Serializable
data class AppSettings(
val enableLocation: Boolean = false
)
AppSettingsSerializer:
object AppSettingsSerializer : Serializer<AppSettings> {
override val defaultValue: AppSettings
get() = AppSettings()
override suspend fun readFrom(input: InputStream): AppSettings {
return try {
Json.decodeFromString(
deserializer = AppSettings.serializer(),
string = input.readBytes().decodeToString())
} catch (e: SerializationException){
e.printStackTrace()
defaultValue
}
}
override suspend fun writeTo(t: AppSettings, output: OutputStream) {
output.write(
Json.encodeToString(
serializer = AppSettings.serializer(),
value = t)
.encodeToByteArray()
)
}
}
SettingsViewModel:
#HiltViewModel
class SettingsViewModel #Inject constructor(
val preferencesRepository: PreferencesRepository
) : ViewModel() {
var preferences by mutableStateOf(AppSettings())
private set
init {
preferencesRepository.data
.onEach { preferences = it }
.launchIn(viewModelScope)
}
inline fun updatePreferences(crossinline body: (AppSettings) -> AppSettings) {
viewModelScope.launch {
val data = body(preferences)
preferencesRepository.updateSettings(data)
}
}
}
PreferenceRepository:
class PreferencesRepository(context: Context){
private val Context.dataStore by dataStore(
fileName = "app-settings.json",
serializer = AppSettingsSerializer
)
private val appDataStore = context.dataStore
val data = appDataStore.data
suspend fun updateSettings(settings: AppSettings) {
appDataStore.updateData { settings }
}
}
Inside settings screen:
item {
SwitchPreference(
title = stringResource(R.string.location),
subtitle = AnnotatedString(stringResource(R.string.location_desc)),
checked = settings.enableLocation,
onCheckedChange = {location ->
viewModel.updatePreferences { it.copy(enableLocation = location) }
}
)
}
This is my UserManager.kt Class which has a function to save and map data but how do create a function which tells me that datastore has user or not?
class UserManager(context: Context) {
private val dataStore = context.createDataStore(name = "user_prefs")
companion object {
val USER_NAME_KEY = preferencesKey<String>("USER_NAME")
val USER_NUMBER_KEY = preferencesKey<Int>("USER_NUMBER")
}
suspend fun storeUser(number: Int, name: String) {
dataStore.edit {
it[USER_NUMBER_KEY] = number
it[USER_NAME_KEY] = name
}
}
val userNumberFlow: Flow<Int> = dataStore.data.map {
it[USER_NUMBER_KEY] ?: 0
}
val userNameFlow: Flow<String> = dataStore.data.map {
it[USER_NAME_KEY] ?: ""
}
}
You can treat with the DataStore as a map, and check if it contains your key or not.
You need to pass your key as a paramter to the below method, and it will return true or false based on the availibility of the key.
fun isKeyStored(key: Preferences.Key<String>): Flow<Boolean> =
context.dataStore.data.map {
preference -> preference.contains(key)
}
I'm trying to learn Kotlin and so far i was managing on my own, but alas i need help with understanding DataStore.
I want to save and load a simple String value. I did that with SharedPreferences with no issues but DataStore just isn't working no matter what i try and which "guide" i use.
What i'm currently stuck with:
private val Context.dataStore by preferencesDataStore("savedData")
class DataStorePrefs (context: Context){
private val dataStore = context.dataStore
companion object {
val USER_ID = stringPreferencesKey("USER_ID")
}
suspend fun saveLogin(userId: String){
dataStore.edit { it[USER_ID] = userId }
}
suspend fun restoreLogin(): String{
val result = dataStore.data.map { it[USER_ID]?: "no id" }
Log.e("result", result.toString())
return result.toString()
}
I`m getting this: E/result: com.evolve.recyclerview.data.DataStorePrefs$restoreLogin$$inlined$map$1#7dde190
So how do i actually get the value from a Flow? I tried to use dataStore.data.collect but it just gets stuck on it never returning anything.
I found out that
dataStore.data.map { it[USER_ID]?: "no id" }.first()
gets me the String.
private val Context.dataStore by preferencesDataStore("savedData")
class DataStorePrefs(context: Context){
companion object {
val USER_ID = stringPreferencesKey("USER_ID")
}
suspend fun saveLogin(userId: String){
dataStore.edit { it[USER_ID] = userId }
}
val myFlow: Flow<String> =
dataStore.data.map { it[USER_ID]?: "no id" }
}
Then somewhere else:
myFlow.collect {
doSomething(it)
}
kotlin 1.2.51
I have the following shared preferences that uses a generic extension function.
class SharedUserPreferencesImp(private val context: Context,
private val sharedPreferenceName: String): SharedUserPreferences {
private val sharedPreferences: SharedPreferences by lazy {
context.getSharedPreferences(sharedPreferenceName, Context.MODE_PRIVATE)
}
override fun <T : Any> T.getValue(key: String): T {
with(sharedPreferences) {
val result: Any = when (this#getValue) {
is String -> getString(key, this#getValue)
is Boolean -> getBoolean(key, this#getValue)
is Int -> getInt(key, this#getValue)
is Long -> getLong(key, this#getValue)
is Float -> getFloat(key, this#getValue)
else -> {
throw UnsupportedOperationException("Cannot find preference casting error")
}
}
#Suppress("unchecked_cast")
return result as T
}
}
}
I am trying to write a unit test for this method. As you can see in my test method the testName.getValue("key") the getValue is not recognized.
class SharedUserPreferencesImpTest {
private lateinit var sharedUserPreferences: SharedUserPreferences
private val context: Context = mock()
#Before
fun setUp() {
sharedUserPreferences = SharedUserPreferencesImp(context, "sharedPreferenceName")
assertThat(sharedUserPreferences).isNotNull
}
#Test
fun `should get a string value from shared preferences`() {
val testName = "this is a test"
testName.getValue("key")
}
}
What is the best way to test a extension function that has a generic type?
Many thanks for any suggestions,
There is a conflict between T.getValue(key: String) being a extension function and SharedUserPreferencesImp member function.
You can make T.getValue(key: String) high-level function and this solves a problem. Here is example code:
fun <T : Any> T.getValue(key: String, sharedPreferences: SharedUserPreferencesImp): T {
with(sharedPreferences.sharedPreferences) {
val result: Any = when (this#getValue) {
is String -> getString(key, this#getValue)
is Boolean -> getBoolean(key, this#getValue)
is Int -> getInt(key, this#getValue)
is Long -> getLong(key, this#getValue)
is Float -> getFloat(key, this#getValue)
else -> {
throw UnsupportedOperationException("Cannot find preference casting error")
}
}
#Suppress("unchecked_cast")
return result as T
}
}
class SharedUserPreferencesImp(private val context: Context,
private val sharedPreferenceName: String): SharedUserPreferences {
val sharedPreferences: SharedPreferences by lazy {
context.getSharedPreferences(sharedPreferenceName, Context.MODE_PRIVATE)
}
}
You can also take a look at this two great libraries:
https://github.com/chibatching/Kotpref
https://github.com/MarcinMoskala/PreferenceHolder
i've facing a problem to test sharedpreference in datastore. in actual datastore i implement three arguments, those include sharedpreference.
in this case i want to store value, and get that value. mocking not help here.
mocking cannot propagate actual value, that will be used by code. in second part.
class FooDataStoreTest : Spek({
given("a foo data store") {
val schedulerRule = TestSchedulerRule()
val service: FooRestService = mock()
val context: Context = mock()
val gson: Gson = mock()
val appFooPreference: SharedPreferences = mock()
var appFooSessionStoreService: AppFooSessionStoreService? = null
var fooStoredLocationService: FooStoredLocationService? = null
beforeEachTest {
appFooSessionStoreService = AppFooSessionStoreService.Builder()
.context(context)
.gson(gson)
.preference(appFooPreference)
.build()
fooStoredLocationService = FooStoredLocationService(appFooSessionStoreService)
}
val longitude = 106.803090
val latitude = -6.244285
on("should get foo service with request longitude $longitude and latitude $latitude") {
it("should return success") {
with(fooStoredLocationService) {
val location = Location()
location.latitude = latitude
location.longitude = longitude
// i want to store location in this
fooStoredLocationService?.saveLastKnownLocation(location)
// and retrieve in below code
val l = fooStoredLocationService?.lastKnownLocation
val dataStore = FooDataStore(service, preference, fooStoredLocationService!!)
service.getFooService(longitude, longitude) willReturnJust
load(FooResponse::class.java, "foo_response.json")
val testObserver = dataStore.getFooService().test()
schedulerRule.testScheduler.advanceTimeBy(2, TimeUnit.SECONDS)
testObserver.assertNoErrors()
testObserver.awaitTerminalEvent()
testObserver.assertComplete()
testObserver.assertValue { actual ->
actual == load(FooResponse::class.java, "foo_response.json")
}
}
}
}
afterEachTest {
appFooSessionStoreService?.clear()
fooStoredLocationService?.clear()
}
}})
and this datastore looks like
open class FooDataStore #Inject constructor(private val fooRestService: FooRestService,
private val fooPreference: FooPreference,
private val fooLocation: fooStoredLocationService) : FooRepository {
private val serviceLocation by lazy {
fooLocation.lastKnownLocation
}
override fun getFooService(): Single<FooResponse> {
safeWith(serviceLocation, {
return getFooLocal(it).flatMap({ (code, message, data) ->
if (data != null) {
Single.just(FooResponse(code, message, data))
} else {
restService.getFooService(it.longitude, it.latitude).compose(singleIo())
}
})
})
return Single.error(httpExceptionFactory(GPS_NOT_SATISFYING))
}
}
Actually i want to get value in from this field serviceLocation. Anyone has approach to do some test for that?, any advise very welcome.
thanks!
I would recommend you not to depend on SharedPreferences directly, but to have some interface LocalStorage, so you can have your SharedPrefsLocalStorage being used in the code and TestLocalStorage in the tests. SharedPrefsLocalStorage will use SharedPreferences under the hood, and TestLocalStorage some Map implementation.
Just a simple example:
// You may add other type, not Int only, or use the String and convert everything to String and back
interface LocalStorage {
fun save(key: String, value: Int)
fun get(key: String): Int?
}
class SharedPrefsLocalStorage(val prefs: SharedPreferences) : LocalStorage {
override fun save(key: String, value: Int) {
with(prefs.edit()){
putInt(key, value)
commit()
}
}
override fun get(key: String): Int? = prefs.getInteger(key)
}
class TestLocalStorage : LocalStorage {
val values = mutableMapOf<String, Any>()
override fun save(key: String, value: Int) {
values[key] = value
}
override fun get(key: String): Int? = map[value] as Int?
}