I am using Androids shared preferences as a simple Storage like this:
class Storage(context: Context) {
private val storage = context.getSharedPreferences("my_storage", Context.MODE_PRIVATE)
private val myKey = "my_string_key"
fun getMyString(): String {
return storage.getString(myKey, "default String") ?: "default String"
}
fun setMyString(str: String) {
storage.edit().apply {
putString(myKey, str)
apply()
}
}
}
However, I don't like how this introduces boilerplate code each time I add another stored value.
This is my current workaround:
class Storage(context: Context) {
private val storage = context.getSharedPreferences("my_storage", Context.MODE_PRIVATE)
private inline fun put(block: (SharedPreferences.Editor) -> Unit) {
storage().edit().apply {
block(this)
apply()
}
}
var myString: String = "default String"
set(str) = put { it.putString(::myString.name, str) }
get() = storage.getString(::myString.name, field) ?: field
}
Now my Questions are:
Is this even a good idea? (Im new to programming on the Android platform and I haven't seen anyone do this, so there might be a good reason to stick to the function approach)
Can this be optimised further? Ideally I would like to only declare the variable and have the getter and setter generated somehow.
Thank you in advance.
I would choose different approach. You just need keys for this. For example something like this:
class Storage(context: Context) {
private val storage = context.getSharedPreferences("my_storage", Context.MODE_PRIVATE)
public fun getKey(key: String): String = storage.getString(key, "defaultString") ?: "defaultString"
public fun set(key: String, value: String) = storage.edit().putString(key, value)
object Keys {
const val key1 = "Key1"
const val key2 = "Key2"
}
}
Or, if you want more safety for keys to be used as constants, not strings
enum class Keys {
key1, key2
}
class Storage(context: Context) {
private fun getDefaultValue(key:Keys): String = when(key) {
Keys.key1 -> "string"
Keys.key2 -> "String2"
}
private val storage = context.getSharedPreferences("my_storage", Context.MODE_PRIVATE)
public fun getKey(key: Keys): String =
storage.getString(key.name, null) ?: getDefaultValue(key)
public fun set(key: Keys, value: String) = storage.edit().putString(key.name, value)
}
Related
I want to implement search functionality among lists of banks in my app.
So somehow I need to use stringResId() but you can't call it without composable func(). Also by using Resources.getSystem().getString() is giving me resources not found exception.
This is my viewModel code
class BankViewModel: ViewModel() {
private val _bankAccount = MutableStateFlow(BankAccount())
val bankAccount: StateFlow<BankAccount> = _bankAccount.asStateFlow()
var bankList = mutableStateOf(Banks)
private var cachedBankList = listOf<Bank>()
private var isSearchStarting = true
var isSearching = mutableStateOf(false)
fun updateBankSearch(searchName: String) {
_bankAccount.update { bankAccount ->
bankAccount.copy(bankName = searchName)
}
}
fun searchBankName(query: String) {
val listToSearch = if(isSearchStarting) {
bankList.value
}else {
cachedBankList
}
viewModelScope.launch {
if (query.isEmpty()) {
bankList.value = cachedBankList
isSearching.value = false
isSearchStarting = true
return#launch
}
val results = listToSearch.filter {
Resources.getSystem().getString(it.bankName).contains(query.trim(), ignoreCase = true)
}
if (isSearchStarting) {
cachedBankList = bankList.value
isSearchStarting = false
}
bankList.value = results
isSearching.value = true
}
}
}
This is my Bank
data class Bank (
#StringRes val bankName: Int,
#DrawableRes val bankLogo: Int = R.drawable.bank_image_2
)
So my question is how can I get a string by using id so that I can compare it with the query??
You need to use the Application Context to retrieve resources like these.
If you're not using any DI framework, or you don't want to use custom ViewModelProvider.Factory, extend AndroidViewModel (which holds a reference to Application) instead of regular ViewModel.
Ideally ViewModels shouldn't contain any Android-specific objects, and you would use some custom StringProvider, or something similar, but that's not a topic of your question.
I have a code where I called sharedPref.edit() and sharedPref.apply() multiple times. How to make convert it to call only once.
if (success) {
val data = response.getJSONObject("data")
sharedPreferences.edit().putBoolean("isLoggedIn", true).apply()
sharedPreferences.edit()
.putString("user_id", data.getString("user_id")).apply()
sharedPreferences.edit().putString("name", data.getString("name"))
.apply()
sharedPreferences.edit().putString("email", data.getString("email"))
.apply()
sharedPreferences.edit()
.putString("mobile_number", data.getString("mobile_number"))
.apply()
sharedPreferences.edit()
.putString("address", data.getString("address")).apply()
StyleableToast.Builder(this)
.text("Welcome " + data.getString("name"))
.backgroundColor(Color.RED)
.textColor(Color.WHITE).show()
userSuccessfullyLoggedIn()
}
I want to use the method call only once.
This can be called once, the returned editor instance can be stored in
a variable and re-used.
How to do this ??
These little steps will organize your code.
You can put it like this:
val editor = sharedPreferences.edit()
Then use it :
editor.putBoolean("isLoggedIn", true)
And Add others values without ".apply()"
Then Put at the End:
editor.apply()
you can create your custom Shared Preferences
class CustomSharedPreferences {
companion object {
private val PREFERENCES_USER_NAME = "preferences_user_name"
private var sharedPreferences: SharedPreferences? = null
#Volatile private var instance: CustomSharedPreferences? = null
private val lock = Any()
operator fun invoke(context: Context) : CustomSharedPreferences = instance ?: synchronized(lock){
instance ?: makeCustomSharedPreferences(context).also {
instance = it
}
}
private fun makeCustomSharedPreferences(context: Context) : CustomSharedPreferences{
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
return CustomSharedPreferences()
}
}
fun saveUser(name: String, email: String){
sharedPreferences?.edit(commit = true){
putString(PREFERENCES_USER_NAME, name)
}
}
fun getUser() = sharedPreferences?.getString(PREFERENCES_USER_NAME, "")
}
You can save all information to SP in saveUser().
My understanding of AndroidX DataStore is the operations are supposed to be thread-safe and transactional. But I'm setting a value then immediately reading it, and the value has not been updated. What am I doing wrong? This shouldn't be possible should it?
Here are my "get" and "set" functions.
fun getValue(keyStr: String): String
{
val key = stringPreferencesKey(keyStr)
val value = runBlocking {
context.dataStore.data.map { it[key] ?: "" }
}
return runBlocking { value.first() }
}
fun setValue(keyStr: String, valueStr: String) {
val key = stringPreferencesKey(keyStr)
runBlocking {
context.dataStore.edit { preferences -> preferences[key] = valueStr }
}
}
And here is how they call them in my Application's CREATE method.
setValue("TEST", "testing")
val test = getValue("TEST")
After the "get" call, test=="".
I need to write test case for the switch condition in kotlin.
Class.kt
fun getEnvSwitchURL(applicationContext: Context, envSwitchInfo: String): String {
val resources = applicationContext.getResources()
val assetManager = resources.getAssets()
val properties = Properties()
try {
val inputStream = assetManager.open("configuration.properties")
properties.load(inputStream)
val urlPref = applicationContext.getSharedPreferences(SELECTED_ENV, Context.MODE_PRIVATE)
val editor = urlPref.edit()
when (envSwitchInfo) {
"Production" ->{
editor.putString("selectedUrl", properties.getProperty("prodUrl"))
editor.apply()
selectedUrl=properties.getProperty("prodUrl")
}
"Development" ->{
editor.putString("selectedUrl", properties.getProperty("devUrl"))
editor.apply()
selectedUrl=properties.getProperty("devUrl")
}
"Testing" ->{
editor.putString("selectedUrl", properties.getProperty("testUrl"))
editor.apply()
selectedUrl=properties.getProperty("testUrl")
}
}
inputStream.close()
}
return selectedUrl
}
test.kt
#BeforeEach
fun runBeforeTest() {
testApplicationContext = Mockito.mock(Context::class.java)
testResource = Mockito.mock(Resources::class.java)
testAsset = Mockito.mock(AssetManager::class.java)
testInputStream = Mockito.mock(InputStream::class.java)
testSharedPref=Mockito.mock(SharedPreferences::class.java)
testEditor=Mockito.mock(SharedPreferences.Editor::class.java)
testProperties=Mockito.mock(Properties::class.java)
testProperties.setProperty("prodUrl", "Value");
}
#Test
fun getEnvSwitchURL() {
Mockito.`when`(testApplicationContext.getResources()).thenReturn(testResource)
Mockito.`when`(testResource.assets).thenReturn(testAsset)
Mockito.`when`(testAsset.open(Mockito.anyString())).thenReturn(testInputStream)
PowerMockito.whenNew(Properties::class.java).withNoArguments().thenReturn(testProperties)
Mockito.doNothing().`when`(testProperties).load(Mockito.any(InputStream::class.java))
Mockito.`when`(testApplicationContext.getSharedPreferences(anyString(),anyInt())).thenReturn(testSharedPref)
Mockito.`when`(testSharedPref.edit()).thenReturn(testEditor)
envSwitchUtils.getEnvSwitchURL(testApplicationContext, testEnvSwitchInfo)
}
Above written test case is working fine. I need to find out how to write test case for switch condition for the above class. Kindly help me to write the same
I haven't answered your question, but perhaps refactoring your code slightly makes it more obvious to test:
private val SELECTED_ENV = "";
fun getEnvSwitchURL(applicationContext: Context, envSwitchInfo: String): String {
val resources = applicationContext.resources
val assetManager = resources.assets
val properties = Properties()
val selectedUrl: String
try {
val inputStream = assetManager.open("configuration.properties")
properties.load(inputStream)
val urlPref = applicationContext.getSharedPreferences(SELECTED_ENV, Context.MODE_PRIVATE)
val editor = urlPref.edit()
selectedUrl = get(envSwitchInfo, properties)
editor.putString("selectedUrl", selectedUrl)
editor.apply()
inputStream.close()
}
return selectedUrl
}
fun get(envSwitchInfo: String, properties: Properties): String {
when (envSwitchInfo) {
"Production" -> {
return properties.getProperty("prodUrl")
}
"Development" -> {
return properties.getProperty("devUrl")
}
"Testing" -> {
return properties.getProperty("testUrl")
}
else -> throw IllegalStateException("Unhandled environment $envSwitchInfo")
}
}
You could do a lot more here, look into the Single Responsibilty Principle. This is a start, for unit testing you don't want to test that SharePreferences works correctly because then you are testing the platform and not your code. You may want to test only that when you pass an environment like "Production", then the selectedUrl you get is returned.
Testing inputs and outputs as described above would be something like this:
String url = envSwitchUtils.getEnvSwitchURL(testApplicationContext, "Production")
assertEquals(url, "http://myProdUrl")
and another test
String url = envSwitchUtils.getEnvSwitchURL(testApplicationContext, "Development")
assertEquals(url, "http://myDevUrl")
So I added a context on my API class that retrieves data from Firebase. If the retrieval fails I return a String that I get from my resources. I just wondered let's say the fragment or activity was suddenly destroyed. Won't I get a ResourcesNotFoundException?
E.g.
I retrieved data > Locked my Screen > Retrieval Fails > (Possible ResourceNotFoundException)?
If this is true then would it be okay to make a StringFactory in a AndroidViewModel, and use the applicationContext to retrieve the String
or
Just fully return a constant var that the Observer will receive, then allow the Observer to access the Context to get the String I need from Resources?
Here are my classes below.
interface BaseApi<T> {
val process: MutableLiveData<Response<T>>
}
class AccountApi(val context: Context, val store: FirebaseFirestore) : BaseApi<AccountDocument> {
override val process: MutableLiveData<Response<AccountDocument>> = MutableLiveData()
fun getAccountDocument(email: String): LiveData<Response<AccountDocument>> {
val collection = store.collection("accounts")
collection.document(email)
.get()
.addOnSuccessListener { task ->
process.value = Response<AccountDocument>(true).data(task.toObject(AccountDocument::class.java))
}
.addOnFailureListener {
process.value = Response<AccountDocument>(false).message(context.getString(R.string.account_does_not_exist))
}
return process
}
}
data class Response<T>(
val isSuccessful: Boolean = false
) {
private var _message: String = ""
private var _data: T? = null
fun message(message: String): Response<T> {
_message = message
return this
}
fun getMessage() = _message
fun data(data: T?): Response<T> {
_data = data
return this
}
fun getData() = _data
}
Well, This is a common issue when you add the string to a different locale than the default strings.xml file and forget to add it to the default strings.xml file.
So when you are in the default locale, the app will not be able to see the value because it's not inside the default strings.xml