Avoid default values in Androids SharedPreferences - android

SharedPreferences provide the following function to retrieve a string:
String getString(String key, #Nullable String defValue);
When storing an Int, the default-value is not-nullable:
int getInt(String key, int defValue);
I now am looking to store and retrieve a nullable Int?, e.g.:
var timerDuration: Int?
get() = prefs.getIntOrNull(TIMER_DURATION)
set(value) { prefs.edit { putIntOrNull(TIMER_DURATION, value) } }
Solution 1: when value is null, remove key
private fun SharedPreferences.getIntOrNull(key: String) =
if (prefs.contains(key)) {
getInt(key, 12345)
} else {
null
}
private fun SharedPreferences.Editor.putIntOrNull(key: String, value: Int?) =
if (value == null) {
remove(key)
} else {
putInt(key, value)
}
... but i'm not sure if i could get problems with multiple, quick accesses because of the asynchronous nature of shared preferences?
Solution 2: use the string-option for everything:
private fun SharedPreferences.getIntOrNull(key: String) =
prefs.getString(key, null)?.toIntOrNull()
private fun SharedPreferences.Editor.putIntOrNull(key: String, value: Int?) =
putString(key, value?.toString())
... but .toIntOrNull() feels like a lot of overhead for such a simple task?
Why would i like to do this?
I hope Kotlin Multiplattform allows me to add an iOS-Version to my existing Android-App.
My goal is a "core" module written completely in Kotlin with no plattform-specific dependencies.
The "core" then just uses this interface, which is implemented by the Android & iOS - Apps
interface SettingsStorage {
var timerDuration: Int?
[...]
}
And as i would not like to have duplicate default-value-logic i'd enjoy to handle that in my "core"-module
Are there any other (better) options? I'm feeling like i'm reinventing the wheel...

Either of your methods is fine. SharedPreferences is thread-safe. Option 1 could only really fail if you set up a service in your manifest to run in a separate process and it is modifying the preferences at the same time as the rest of your app. This isn't something you would do accidentally.
Your second method isn't any additional overhead, because under the hood, SharedPreferences are all Strings so your code is basically doing the same thing as SharedPreferences.getInt, except that returns the default value when the preference doesn't exist or cant be parsed as an Int.

When you store an Int to SharedPreferences, it is stored internally as a string. So for getting, just get a String and if it's a null returns it, or try parse it to an Int. For setting, just convert your Int to String and put it using putString method.
var timerDuration: Int?
get() = try { prefs.getString(TIMER_DURATION, null)?.toInt() } catch (e: Exception) { null }
set(value) { prefs.edit { putString(TIMER_DURATION, value.toString()) } }

Related

Reference an object in a class by using a string?

I want to reference an object within this class I have below:
class HerbData {
object Dill {
const val herbName: String = "This is Dill!"
const val scientificName: String = "Anethum Graveolens"
val dullThumbnail: Int = R.drawable.dill_thumbnail_attr
}
object Peppermint {
val herbName: String = "This is Peppermint!"
}
}
Is there anyway that I can reference the object by using a string in Kotlin? Here is somewhat what I mean:
HerbData."Dill".herbname
I can't find anything on this topic for Kotlin.
Another way you could do this is with an enum class. The advantage over a map is that you have a data structure you can reference directly in code, so you could use HerbData.Dill as well as HerbData["Dill"]. And that will enable you to take advantage of compile-time checking and lint warnings, refactoring, exhaustive pattern matching, code completion etc, because the data is defined in your code
enum class HerbData(
val herbName: String,
val scientificName: String? = null,
val dullThumbnail: Int? = null
) {
Dill("This is Dill!", "Anethum Graveolens", R.drawable.dill_thumbnail_attr),
Peppermint("This is Peppermint!");
companion object {
operator fun get(name: String): HerbData? =
try { valueOf(name) } catch(e: IllegalArgumentException) { null }
}
}
fun main() {
// no guarantee these lookups exist, need to null-check them
HerbData["Peppermint"]?.herbName.run(::println)
// case-sensitive so this fails
HerbData["peppermint"]?.herbName.run(::println)
// this name is defined in the type system though! No checking required
HerbData.Peppermint.herbName.run(::println)
}
>> This is Peppermint!
null
This is Peppermint!
Enum classes have that valueOf(String) method that lets you look up a constant by name, but it throws an exception if nothing matches. I added it as a get operator function on the class, so you can use the typical getter access like a map (e.g. HerbData["Dill"]). As an alternative, you could do something a bit neater:
companion object {
// storing all the enum constants for lookups
private val values = values()
operator fun get(name: String): HerbData? =
values.find() { it.name.equals(name, ignoreCase = true) }
}
You could tweak the efficiency on this (I'm just storing the result of values() since that call creates a new array each time) but it's pretty simple - you're just storing all the enum entries and creating a lookup based on the name. That lets you be a little smarter if you need to, like making the lookup case-insensitive (which may or may not be a good thing, depending on why you're doing this)
The advantage here is that you're generating the lookup automatically - if you ever refactor the name of an enum constant, the string label will always match it (which you can get from the enum constant itself using its name property). Any "Dill" strings in your code will stay as "Dill" of course - that's the limitation of using hardcoded string lookups
The question really is, why do you want to do this? If it's pure data where no items need to be explicitly referenced in code, and it's all looked up at runtime, you should probably use a data class and a map, or something along those lines. If you do need to reference them as objects within the code at compile time (and trying to use HerbData."Dill".herbName implies you do) then an enum is a fairly easy way to let you do both
Declare a Data Class
data class HerbData (
val scientificName: String,
val dullThumbnail: Int
)
Initialize a muteable map and put data in it
val herbData = mutableMapOf<String, HerbData>()
herbData.put("Dill", HerbData("Anethum Graveolens", R.drawable.dill_thumbnail_attr))
herbData.put("Peppermint", HerbData("Mentha piperita", R.drawable.peppermint_thumbnail_attr))
You can now just
herbData["Dill"]?.scientificName
class HerbData {
interface Herb {
val herbName: String
val scientificName: String
}
object Dill : Herb {
override val herbName: String = "This is Dill!"
override val scientificName: String = "Anethum Graveolens"
}
object Peppermint: Herb {
override val herbName: String = "This is Peppermint!"
override val scientificName: String = "Mentha piperita"
}
companion object {
operator fun get(name: String): Herb? {
return HerbData::class
.nestedClasses
.find { it.simpleName == name }
?.objectInstance as? Herb
}
}
}
println(HerbData["Dill"]?.herbName) // Prints: This is Dill!
println(HerbData["Peppermint"]?.scientificName) // Prints: Mentha piperita
println(HerbData["Pepper"]?.herbName) // Prints: null

Get Enum type by mapping Enum value always complain null issue Android Kotlin

I have enum class and I am mapping by value, when I am return Enum value it always complain about null issue.
ConversationStatus.kt
enum class ConversationStatus(val status: String) {
OPEN("open"),
CLOSED("closed");
companion object {
private val mapByStatus = values().associateBy(ConversationStatus::status)
fun fromType(status: String): ConversationStatus {
return mapByStatus[status]
}
}
}
This always complain this issue. How can I fix this? Any recommendation for that. Thanks
There's 3 possible ways to go to.
Android Studio is often good at suggested fixes as you can see in the screenshot. It suggests to change the return type to ConversationStatus? which means it might return null. It will become this then:
companion object {
private val mapByStatus = values().associateBy(ConversationStatus::status)
fun fromType(status: String): ConversationStatus? {
return mapByStatus[status]
}
}
Another way is to tell the compiler that you ensure it will always not be null by adding !! to the return statement. Like this:
companion object {
private val mapByStatus = values().associateBy(ConversationStatus::status)
fun fromType(status: String): ConversationStatus {
return mapByStatus[status]!!
}
}
This will cause a crash though if you call the function with a status that's not "open" or "closed"
Alternatively you could provide a fall back value. With this I mean that it returns a default value in case you call the function with a string that's not "open" or "closed". If you want that to be OPEN you could do like this:
companion object {
private val mapByStatus = values().associateBy(ConversationStatus::status)
fun fromType(status: String): ConversationStatus {
return mapByStatus[status] ?: OPEN
}
}

Kotlin mocking a var with get/set using Mockk

I have this Kotlin class that is a wrapper around SharedPreferences in an Android app.
class Preferences(private val context: Context) {
private val preferences: SharedPreferences =
context.getSharedPreferences("name_of_file", Context.MODE_PRIVATE)
// Integers
var coins: Int
get() = preferences.getInt(KEY_COINS, 0)
set(value) = preferences.edit { putInt(KEY_COINS, value) }
var pressure: Int
get() = preferences.getInt(KEY_PRESSURE, DEFAULT_PRESSURE)
set(value) = preferences.edit { putInt(KEY_PRESSURE, value) }
}
I need to mock this class in order to be able to use it in some unit tests for my viewmodels. I tried mocking the get/set methods of the properties but for some reason I'm getting some errors and I need a bit of help.
This is how I try to mock the Preferences class:
private val sharedPreferences = mutableMapOf<String, Any>()
...
val preferences = mockk<Preferences>()
listOf("coins", "pressure").forEach { key ->
every { preferences getProperty key } returns sharedPreferences[key]
every { preferences setProperty key } answers { // exception on this line
sharedPreferences[key] = fieldValue
fieldValue
}
}
And I get this exception when running any of the tests that involves this mock
io.mockk.MockKException: Missing mocked calls inside every { ... } block: make sure the object inside the block is a mock
I think the error is quite cryptic. Or, is there any way to mock these fields using mockk?
I have also read the examples from here where I got the inspiration for this solution but seems like there is something I'm missing.

What's the point of having a default value in sharedPref.getString?

I'm accessing my Android apps SharedPreferences via
private val sharedPref = PreferenceManager.getDefaultSharedPreferences(context)`
and then attempting to get data from it using
val lat: String = sharedPref.getString("MyKey", "Default")
But this line gives me an error reading "Type mismatch. Required String, found String?"
According to the documentation the second parameter in the getString method says "Value to return if this preference does not exist. This value may be null."
So what's the point of having a default value then if the value can be null? I cannot seem to get the default value to ever be used and the only way I can get my code to work is to use the elvis operator and rewrite my code as:
val lat: String = sharedPref.getString("MyKey", "Default") ?: "Default"
Which looks ugly. Am I crazy? What am I missing?
Consider this in a such way:
Every String preference in SharedPreferences can exist or not and can be null or not. So the code
val lat: String = sharedPref.getString("MyKey", "Default") ?: "Not Set"
will return:
Default if the preference with this Key doesn't exists (means there is no mapping for this Key)
Not Set if the preference exists, but is null (mapping Key to null created)
any other value if the preference exists and the value of the mapping isn't null.
Update
Apparently, SharedPreferences are much less smart I thought it was. Having this code internally:
String v = (String)mMap.get(key);
return v != null ? v : defValue;
it will return null only if we'll pass null as a default value (and there were no value saved). This means we actually don't need an elvis option and we will not get "Not Set" value. This method returns nullable value, just because it allows you to pass nullable as a default value.
It's because kotlin Null-Safety is kick in when reading the following code:
val lat: String = sharedPref.getString("MyKey", "Default")
if you visit the SharedPreferences code, you can see the following code:
#Nullable
String getString(String key, #Nullable String defValue);
which is give us a probability to use null as defValue parameter. So, Kotlin try to guard it and give you the matching error:
"Type mismatch. Required String, found String?"
You can fix the problem by enabling nullable for your String variable with:
val lat: String? = sharedPref.getString("MyKey", "Default")
though this against Kotlin type system purpose:
Kotlin's type system is aimed at eliminating the danger of null references from code, also known as the The Billion Dollar Mistake.
SharedPreferences is an abstraction over Key/Value databasing provided by Google, how you use it is up to you. If you dislike this syntax, then create a wrapper or extension for your getString(). Just to give an example:
fun PreferenceManager.getStringTheFancyWay(key: String, defaultValue: String): String {
return getString(key, defaultValue) ?: defaultValue
}
val value = getStringTheFancyWay("a", "b")
Personally I dislike this, because null allows for a better control flow over non-existing keys.
This is how I use SharedPreferences
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
val value = preferences.getString("username", null)
if (value != null) {
toast("Hello $value")
} else {
toast("Username not found")
}
or
preferences.getString("username", null)?.let { username ->
toast("Hello $username")
}
Notice the difference?
The fact is simple, just imagine you haven't saved any value regarding to that key(for your case 'MyKey') and tried to get the value for that key(for your case 'MyKey'). What will SharedPreference return ? It will simply return the default value.
You will see that, you must assign null or any other string to default for String type, 0 or any other int value to default for integer type and true or false default value for bolean type. I hope you got the answer.

How can I tell kotlin that a function doesn't return null if the parameter is not null?

I want to write a convenience extension to extract values from a Map while parsing them at the same time. If the parsing fails, the function should return a default value. This all works fine, but I want to tell the Kotlin compiler that when the default value is not null, the result won't be null either. I could to this in Java through the #Contract annotation, but it seems to not work in Kotlin. Can this be done? Do contracts not work for extension functions? Here is the kotlin attempt:
import org.jetbrains.annotations.Contract
private const val TAG = "ParseExtensions"
#Contract("_, !null -> !null")
fun Map<String, String>.optLong(key: String, default: Long?): Long? {
val value = get(key)
value ?: return default
return try {
java.lang.Long.valueOf(value)
} catch (e: NumberFormatException) {
Log.e(TAG, e)
Log.d(TAG, "Couldn't convert $value to long for key $key")
default
}
}
fun test() {
val a = HashMap<String, String>()
val something: Long = a.optLong("somekey", 1)
}
In the above code, the IDE will highlight an error in the assignment to something despite optLong being called with a non null default value of 1. For comparison, here is similar code which tests nullability through annotations and contracts in Java:
public class StackoverflowQuestion
{
#Contract("_, !null -> !null")
static #Nullable Long getLong(#NonNull String key, #Nullable Long def)
{
// Just for testing, no real code here.
return 0L;
}
static void testNull(#NonNull Long value) {
}
static void test()
{
final Long something = getLong("somekey", 1L);
testNull(something);
}
}
The above code doesn't show any error. Only when the #Contract annotation is removed will the IDE warn about the call to testNull() with a potentially null value.
You can do this by making the function generic.
fun <T: Long?> Map<String, String>.optLong(key: String, default: T): T
{
// do something.
return default
}
Which can be used like this:
fun main(args: Array<String>) {
val nullable: Long? = 0L
val notNullable: Long = 0L
someMap.optLong(nullable) // Returns type `Long?`
someMap.optLong(notNullable) // Returns type `Long`
}
This works because Long? is a supertype of Long. The type will normally be inferred in order to return a nullable or non-nullable type based on the parameters.
This will "tell the Kotlin compiler that when the default value is not null, the result won't be null either."
It's a pity that you can't do this, in Kotlin 1.2 or below.
However, Kotlin is working on contract dsl which is unannounced yet, which is not available ATM (since they're declared internal in the stdlib) but you can use some hacks to use them in your codes (by compiling a stdlib yourself, make all of them public).
You can see them in the stdlib ATM:
#kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}
Maybe there will be something like
contract {
when(null != default) implies (returnValue != null)
}
in the future that can solve your problem.
Workaround
Personally I'd recommend you to replace default's type with a NotNull Long and call it like
val nullableLong = blabla
val result = nullableLong?.let { oraora.optLong(mudamuda, it) }
result is Long? and it's null only when nullableLong is null.
#Contract does work with Kotlin extension functions, it just needs to be changed to work with the compiled bytecode. An extension function is compiled in bytecode as a static method:
fun ClassA?.someMethod(arg: ClassB): ClassC? {
return this?.let { arg.someMethod(it)!! }
}
Java will see this as nullable, so it will require you to null-check the result. But the real contract is: "if ClassA is null, returns null; otherwise if ClassA is not null, returns non-null". But IntelliJ does not understand that (at least from a Java source).
When that method gets compiled to Java bytecode it's actually:
#Nullable static ClassC someMethod(#Nullable ClassA argA, #NonNull ClassB argB) {}
So you need to account for the synthetic first argument, when writing your #Contract:
#Contract("null, _ -> null; !null, _ -> !null")
fun ClassA?.someMethod(arg: ClassB): ClassC? {...}
After that, IntelliJ will understand the contract of the static method, and will understand that the return value's nullability is dependent on the first argument's nullness.
So the short version, as it pertains to this question is, you just need to add an extra _ argument to the Contract, to represent the "this" argument:
#Contract("_, _, !null -> !null") // args are: ($this: Map, key: String, default: Long?)
fun Map<String, String>.optLong(key: String, default: Long?): Long? {

Categories

Resources