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() }
Related
I have a model
data class RegisterPostDataWithPwdCheck(
var phone_number: String?,
var name: String?,
var password: String?,
var secondPassword: String?)
And a ViewModel
class SignUpViewModel(application: Application) : BaseViewModel(application){
val registerPostData = MutableLiveData<RegisterPostDataWithPwdCheck>...
fun checkPassword(){}...}
I also have a View that has this code inside
viewModel.registerPostData.observe(viewLifecycleOwner, Observer {
viewModel.checkPassword()
})
In the XML there are two fields of interest
<EditText
android:id="#+id/edittext_sign_up_password"
android:text="#={view_model.registerPostData.password}" />
<EditText
android:id="#+id/edittext_sign_up_second_pw"
android:text="#={view_model.registerPostData.secondPassword}" />
What I understood so far is that the .observe will be called only when the entire RegisterPostDataWithPwdCheck object changes and I don't want that. I want it to be triggered when any of the parameters changes so I can call the fun checkPassword(){} in order to see if the two fields match. Is this possible?
Using #mahdi-shahbazi comment I've managed to work this out in Kotlin. My Model is now:
data class RegisterPostDataWithPwdCheck(
#SerializedName(value = "phone_number")
private var phoneNumber: String?,
private var name: String?,
private var password: String?,
private var secondPassword: String?
) : BaseObservable() {
#Bindable
fun getPhoneNumber(): String? {
return phoneNumber
}
fun setPhoneNumber(value: String) {
if (value != phoneNumber) {
phoneNumber = value
notifyPropertyChanged(BR.phoneNumber)
}
}
#Bindable
fun getName(): String? {
return name
}
fun setName(value: String?) {
if (value != name) {
name = value
notifyPropertyChanged(BR.name)
}
}
#Bindable
fun getPassword(): String? {
return password
}
fun setPassword(value: String?) {
if (value != password) {
password = value
notifyPropertyChanged(BR.password)
}
}
#Bindable
fun getSecondPassword(): String? {
return secondPassword
}
fun setSecondPassword(value: String?) {
if (value != secondPassword) {
secondPassword = value
notifyPropertyChanged(BR.secondPassword)
}
}
}
And creating custom LiveData class:
class PropertyAwareMutableLiveData<T : BaseObservable> : MutableLiveData<T>()
{
private val callback = object : Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
value = value
}
}
override fun setValue(value: T?) {
super.setValue(value)
value?.addOnPropertyChangedCallback(callback)
}
}
What I still don't know if there is a way to automate this #Binding process which is terribly slow and boring and also forces some changes (turning parameters to private).
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?
I would like to find a way to generate getters and setters of some Kotlin property automatically. In java there is no problem to do it.
I am working with data binding and I have many classes which looks like so:
class AnimalListItemPresenter(private var _animal: String) : BaseObservable() {
var animal: String
#Bindable get() = _animal
set(value) {
_animal = value
notifyPropertyChanged(BR.item)
}
}
I know that it is not possible not possible to generate the logic in setter but can I at leat somehow generate the standard getter and setter?
Standard getters and setters are built into Kotlin.
example:
class Customer {
var id = "",
var name = ""
}
and you can use it like:
fun copyCustomer(customer: Customer) : Customer {
val result = Customer()
result.name = customer.name
.
.
return result
}
You can also override the default getter and setter in the manner you have done in the code snippet. Good Resource: https://kotlinlang.org/docs/reference/properties.html
If you want a quick way of generating boilerplate code in Android Studio -> Alt + Enteron the property and you canAdd GetterorAdd Setter` among different options
class BannerBean {
private var id: String? = null
private var image: String? = null
private var link: String? = null
fun getId(): String? {
return id
}
fun setId(id: String?) {
this.id = id
}
fun getImage(): String? {
return image
}
fun setImage(image: String?) {
this.image = image
}
fun getLink(): String? {
return link
}
fun setLink(link: String?) {
this.link = link
}
}
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?
}
In my activity I have a field that should be non-nullable and has a custom setter. I want to initialize the field in my onCreate method so I added lateinit to my variable declaration. But, apparently you cannot do that (at the moment): https://discuss.kotlinlang.org/t/lateinit-modifier-is-not-allowed-on-custom-setter/1999.
These are the workarounds I can see:
Do it the Java way. Make the field nullable and initialize it with null. I don't want to do that.
Initialize the field with a "default instance" of the type. That's what I currently do. But that would be too expensive for some types.
Can someone recommend a better way (that does not involve removing the custom setter)?
Replace it with a property backed by nullable property:
private var _tmp: String? = null
var tmp: String
get() = _tmp!!
set(value) {_tmp=value; println("tmp set to $value")}
Or this way, if you want it to be consistent with lateinit semantics:
private var _tmp: String? = null
var tmp: String
get() = _tmp ?: throw UninitializedPropertyAccessException("\"tmp\" was queried before being initialized")
set(value) {_tmp=value; println("tmp set to $value")}
This can be achieved by using a backing property (as per Pavlus's answer); however, I prefer to wrap it inside a delegate to avoid exposing it outside of the property's context:
open class LateInit<T: Any> : ReadWriteProperty<Any?, T> {
protected lateinit var field: T
final override fun getValue(thisRef: Any?, property: KProperty<*>) = get()
final override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) = set(value)
open fun get() = field
open fun set(value: T) { field = value }
}
This provides standard getters and setters that can be overridden with a custom implementation:
var upperCaseString by object : LateInit<String>() {
override fun set(value: String) {
field = value.toUpperCase()
}
}
However, since this implementation requires extending the delegate, the generic type cannot be inferred from the property type. This can overcome by taking the custom getter and setter as parameters:
class LateInit<T: Any>(private val getter: FieldHolder<T>.() -> T = { field },
private val setter: FieldHolder<T>.(T) -> Unit = { field = it }) :
ReadWriteProperty<Any?, T> {
private val fieldHolder = FieldHolder<T>()
override fun getValue(thisRef: Any?, property: KProperty<*>) = fieldHolder.getter()
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) =
fieldHolder.setter(value)
class FieldHolder<T: Any> {
lateinit var field: T
}
}
Which can then be used like this:
private var upperCaseString: String by LateInit(setter = { field = it.toUpperCase() })
I realized that you can also make your private property lateinit instead of making it nullable:
var tmp: T
get() = _tmp
set(value) {
_tmp = value
println("tmp set to $value")
}
private lateinit var _tmp: T