I am working on an Android app in which I have to make changes to my HomeScreen by observing LiveData of SharedPreferences which lives in Settings screen. I am following MVVM architecture for it.
I have already checked this LiveData with shared preferences, but it has a lot of boilerplate in the form of creating different classes for different types of SharedPreferences data. I am looking for something more generic. That is why I have created a LiveSharedPreference class and getter for SharedPreferences with Kotlin extension functions and reified types. Here is the custom LiveData class and SharedPreferences getter.
/**************** LiveSharedPreferences.kt ****************/
class LiveSharedPreferences<T : Any>(
private val sharedPreferences: SharedPreferences, private val preferenceKey: String,
private val defValue: T, private val clas: Class<T>
) : LiveData<T>() {
private val preferenceChangeListener =
SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
if (key == preferenceKey) {
// error in this line
value = sharedPreferences.get(key, defValue)
}
}
override fun onActive() {
super.onActive()
// error in this line
value = sharedPreferences.get(preferenceKey, defValue)
sharedPreferences.registerOnSharedPreferenceChangeListener(preferenceChangeListener)
}
override fun onInactive() {
sharedPreferences.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener)
super.onInactive()
}
}
/****** Custom getter with reified type T defined in Extensions.kt ******/
inline fun <reified T: Any> SharedPreferences.get(prefKey: String?, defaultValue: T? = null): T? {
return when (T::class) {
String::class -> getString(prefKey, defaultValue as? String) as T?
Int::class -> getInt(prefKey, defaultValue as? Int ?: -1) as T?
Boolean::class -> getBoolean(prefKey, defaultValue as? Boolean ?: false) as T?
Float::class -> getFloat(prefKey, defaultValue as? Float ?: -1f) as T?
Long::class -> getLong(prefKey, defaultValue as? Long ?: -1) as T?
else -> throw UnsupportedOperationException("Invalid operation")
}
}
But using this get function gives me an error:
Cannot use 'T' as reified type parameter. Use a class instead
I know it has something to do with the way I am dealing with type T. Is there a way I can deal with this problem without much boilerplate?
Related
/**
this "T::class.java" report an error :Cannot use 'T' as reified type parameter. Use a class instead!
so how can i fix it or what can i do to realize this way?please.
**/
see the next kotlin code
data class PostHttpResultBean<T>(private var errno:Int,private var error:String,private var data:String):IHttpResultEntity<T>{
override val errorCode: Int
get() = errno
override val errorMessage: String
get() = error
override val isSuccess: Boolean
get() = errno==0
override val result:T
get() = RSAUtil.dataDecrypt(RSAUtil.getKeyPassword(), data,T::class.java)!!
class RSAUtil {
companion object {
fun <T> dataDecrypt(password: String, data: String, java: Class<T>): T? {
val content = Base64.decode(data.toByteArray(), Base64.NO_WRAP)
try {
var deString = decrypt(content, password)
if (!deString.isEmpty()){
val first = deString.substring(0, deString.lastIndexOf(":") + 1)
deString = "$first$deString}"
return Gson().fromJson(deString,java)
}
return null
} catch (e: Exception) {
e.printStackTrace()
}
return null
}
}
}
You should change dataDecrypt like that:
inline fun <reified T> dataDecrypt(password: String, data: String): T? {
...
try {
...
if (!deString.isEmpty()){
...
return Gson().fromJson(deString, T::class.java)
}
...
}
}
And on the call site the T type will be inferred from result:
override val result:T
get() = RSAUtil.dataDecrypt(RSAUtil.getKeyPassword(), data)!!
You can read more about inline functions and reified types here and I strongly recommend to do so. I would also point out that your code is ill-formatted, it is advised to use ?: instead of !! in nullability checks and companion objects are discouraged in Kotlin, you could define functions outside of class and use (or import) them as if they were static.
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
Android Studio 3.4
Kotlin 1.3.10
I have the following method that calls findPreferences to return the correct value that is stored in shared preferences. However, as I am using reified the findPreferences give me an error: Cannot use type T as a reified parameter.
Is there anyway I can get this to work?
fun <T: Any> getValue(key: String, defaultValue: T?): T {
return findPreferences(key, defaultValue)
}
This is the method that will return the value based on the key
#Suppress("unchecked_cast")
inline fun <reified T: Any> findPreferences(key: String, defaultValue: T?): T {
with(sharedPreferences) {
val result: Any = when(defaultValue) {
is Boolean -> getBoolean(key, default)
is Int -> getInt(key, defaultValue)
is Long -> getLong(key, defaultValue)
is Float -> getFloat(key, defaultValue)
is String -> getString(key, defaultValue)
else -> {
throw UnsupportedOperationException("Cannot find preference casting error")
}
}
return result as T
}
}
Remove reified or change getValue to inline fun <reified T: Any> getValue....
With reified, we pass a type to this function(https://kotlinlang.org/docs/reference/inline-functions.html#reified-type-parameters). reified requires type to be known at compile time and obviously there is no enough type information in the getValue.
Extension functions are great for the SharedPreference api in android. Jake Wharton has an interesting implementation at time code 32:30 of this video tutorial where he implements SharedPreferences extension function like so:
preferences.edit{
set(USER_ID /*some string key constant somewhere*/, 42)
//...
}
while this is ok, its kind of verbose.
This tutorial by Krupal Shah explains how you can reduce the getter/setter extension functions of SharedPreferences to:
preferences[USER_ID] = 42
Log.i("User Id", preferences[USER_ID]) //User Id: 42
This is pretty good, but the brackets imply iterable semantics, IMO. While not the worst thing in the world, you just wish that you could implement a field extension of a SharedPreferences value by the key constant itself.
My question is, is there any way to implement this type of extension on SharedPreferences?
preferences.USER_ID = 42
Log.i("User Id", preferences.USER_ID) //User Id: 42
First, let's create general interface for providing instance of SharedPreferences:
interface SharedPreferencesProvider {
val sharedPreferences: SharedPreferences
}
After we have to create delegate for property which will read/write value to preferences:
object PreferencesDelegates {
fun string(
defaultValue: String = "",
key: String? = null
): ReadWriteProperty<SharedPreferencesProvider, String> =
StringPreferencesProperty(defaultValue, key)
}
private class StringPreferencesProperty(
private val defaultValue: String,
private val key: String?
) : ReadWriteProperty<SharedPreferencesProvider, String> {
override fun getValue(
thisRef: SharedPreferencesProvider,
property: KProperty<*>
): String {
val key = key ?: property.name
return thisRef.sharedPreferences.getString(key, defaultValue)
}
override fun setValue(
thisRef: SharedPreferencesProvider,
property: KProperty<*>,
value: String
) {
val key = key ?: property.name
thisRef.sharedPreferences.save(key, value)
}
}
PreferencesDelegates needed to hide implementation and add some readability to code. In the end it can be used like this:
class AccountRepository(
override val sharedPreferences: SharedPreferences
) : SharedPreferencesProvider {
var currentUserId by PreferencesDelegates.string()
var currentUserName by string() //With import
var currentUserNickname by string(key = "CUSTOM_KEY", defaultValue = "Unknown")
fun saveUser(id: String, name: String) {
this.currentUserId = id
this.currentUserName = name
}
}
Similar can be implemented int, float or even custom type:
open class CustomPreferencesProperty<T>(
defaultValue: T,
private val key: String?,
private val getMapper: (String) -> T,
private val setMapper: (T) -> String = { it.toString() }
) : ReadWriteProperty<SharedPreferencesProvider, T> {
private val defaultValueRaw: String = setMapper(defaultValue)
override fun getValue(
thisRef: SharedPreferencesProvider,
property: KProperty<*>
): T {
val key = property.name
return getMapper(thisRef.sharedPreferences.getString(key, defaultValueRaw))
}
override fun setValue(
thisRef: SharedPreferencesProvider,
property: KProperty<*>,
value: T
) {
val key = property.name
thisRef.sharedPreferences.save(key, setMapper(value))
}
}
I wrote small library which covers such case. You can find rest of implemented preferences here
EDIT. In case if you are using dagger:
class AccountRepository #Injcet constructor() : SharedPreferencesProvider {
#Inject
override lateinit var sharedPreferences: SharedPreferences
var currentUserId by PreferencesDelegates.string()
...
}
You could define a simple extension property with a getter and a setter
var SharedPreferences.userId
get() = getInt(USER_ID, 0)
set(value: Int) { edit().putInt(USER_ID, value).apply() }
I put some utility in Tool.kt, Both Method A and Method B can work well.
I think Method B will keep in memory when I start an app even if I never invoke fun <T> preference(context: Context, name: String, default: T)
I think Method A only allocate memory when I invoke DelegatesExt.preference(this,"ZipCode",100L)
So I think Method A is the better than Method B, right?
Method A
object DelegatesExt {
fun <T> preference(context: Context, name: String, default: T) = Preference(context, name, default)
}
class Preference<T>(private val context: Context, private val name: String,
private val default: T) {
private val prefs: SharedPreferences by lazy {
context.getSharedPreferences("default", Context.MODE_PRIVATE)
}
operator fun getValue(thisRef: Any?, property: KProperty<*>): T = findPreference(name, default)
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
putPreference(name, value)
}
#Suppress("UNCHECKED_CAST")
private fun findPreference(name: String, default: T): T = with(prefs) {
val res: Any = when (default) {
is Long -> getLong(name, default)
is String -> getString(name, default)
is Int -> getInt(name, default)
is Boolean -> getBoolean(name, default)
is Float -> getFloat(name, default)
else -> throw IllegalArgumentException("This type can be saved into Preferences")
}
res as T
}
#SuppressLint("CommitPrefEdits")
private fun putPreference(name: String, value: T) = with(prefs.edit()) {
when (value) {
is Long -> putLong(name, value)
is String -> putString(name, value)
is Int -> putInt(name, value)
is Boolean -> putBoolean(name, value)
is Float -> putFloat(name, value)
else -> throw IllegalArgumentException("This type can't be saved into Preferences")
}.apply()
}
}
Method B
fun <T> preference(context: Context, name: String, default: T) = Preference(context, name, default)
class Preference<T>(private val context: Context, private val name: String,
private val default: T) {
private val prefs: SharedPreferences by lazy {
context.getSharedPreferences("default", Context.MODE_PRIVATE)
}
operator fun getValue(thisRef: Any?, property: KProperty<*>): T = findPreference(name, default)
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
putPreference(name, value)
}
#Suppress("UNCHECKED_CAST")
private fun findPreference(name: String, default: T): T = with(prefs) {
val res: Any = when (default) {
is Long -> getLong(name, default)
is String -> getString(name, default)
is Int -> getInt(name, default)
is Boolean -> getBoolean(name, default)
is Float -> getFloat(name, default)
else -> throw IllegalArgumentException("This type can be saved into Preferences")
}
res as T
}
#SuppressLint("CommitPrefEdits")
private fun putPreference(name: String, value: T) = with(prefs.edit()) {
when (value) {
is Long -> putLong(name, value)
is String -> putString(name, value)
is Int -> putInt(name, value)
is Boolean -> putBoolean(name, value)
is Float -> putFloat(name, value)
else -> throw IllegalArgumentException("This type can't be saved into Preferences")
}.apply()
}
}
Method A will allocate object DelegatesExt during static class initialization - as soon as you reference DelegatesExt in your code, because object in Kotlin is singleton with lazy initialization.
Then, when you'll call DelegatesExt.preference(...), it will allocate your Preference<T> object. By the way, it will allocate a new instance on each call, which is not a good idea.
Then, when you'll call either getValue or setValue, SharedPreferences will be allocated (once only per Preference<T> instance).
Method B doesn't allocate a redundant object DelegatesExt, and Preference<T> will be allocated on each method call as well.
This will be compiled to effectively the same code as a class with a static method in Java.
But Preference<T> won't be allocated before a preference method call (in both cases).
Long story short, both options are almost the same, except of the object DelegatesExt being allocated or not. But it's worth to stop allocating a new Preference<T> on each preference method call.
I think Method B will keep in memeory when I start a app even if I never invoke fun <T> preference(context: Context, name: String, default: T)
What exactly would it keep in memory?
No, the methods are the same except for use when invoking in Kotlin. But in fact, the method B preference is inside class ToolKt which you can see if you try to call it from Java.
Why define either of the preference functions instead of using Preference constructor directly? Kotlin constructors don't have issues with type inference like Java's do.