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
Related
I have a default function like this:
fun <T> makeDefault(): Animal<T> = Animal<Nothing>(
name = "",
size = 0,
age = 0
)
I saw that there is the by operator, which can be used for view models like this:
val model: MyViewModel by viewModels()
Question: How I can create a function that behaves like that for my makeDefault()?
What I want to do:
val animal: Animal<Dog> by makeDefault()
You can create your own Delegate, not extension:
import kotlin.reflect.KProperty
class DefaultDelegate<T> {
private var created: Animal<T> = Animal<Nothing>(
name = "",
size = 0,
age = 0
)
operator fun getValue(thisRef: Any?, property: KProperty<*>): Animal<T> {
return created
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Animal<T>) {
created = value
}
}
Then use it:
val animal: Animal<Dog> by DefaultDelegate()
For more information, see Delegated properties doc
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() }
class SetContentView<in R : Activity, out T : ViewDataBinding>(
#LayoutRes private val layoutRes: Int) {
private var value : T? = null
operator fun getValue(thisRef: Activity, property: KProperty<*>): T {
value = value ?: DataBindingUtil.setContentView<T>(thisRef, layoutRes)
return value
}
}
val binding: ActivityDogBinding by contentView(R.layout.activity_dog)
DogActivity.kt
fun <R : Activity, T : ViewDataBinding> contentView(#LayoutRes layoutRes: Int):
SetContentView<R, T> {
return SetContentView(layoutRes)
}
When I try to call in the activity as
val binding: ActivityLoginBinding by contentView(layoutRes = R.layout.activity_login)
Its throwing error as
Type inference failed. Please try to specify type argument explicitly : Kotlin
in Android Studio. I tried similar in IntelliJ its working fine.
You need to use the R type parameter for your getValue method:
operator fun getValue(thisRef: R, property: KProperty<*>): T {
...
}
Kotlin has delegated properties which is a very nice feature. But I am figuring out how to get and set the values. Let's say I want to get value of the property which is delegated. In a get() method how i can access the value?
Here's an example of how I have implemented:
class Example() {
var p: String by DelegateExample()
}
class DelegateExample {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "${property.name} "
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
println("${value.trim()} '${property.name.toUpperCase()} '")
}
}
fun delegate(): String {
val e = Example()
e.p = "NEW"
return e.p
}
The main question I am unable to understand is, How can I set the value to the actual property on which the delegation class is assigned. When I assign "NEW" to property p, how can I store that value to the variable p or read that new value passed on to p with get? Am I missing something basic here? Any help will be much appreciated. Thanks in advance.
Just create property in delegate which will hold the value
class DelegateExample {
private var value: String? = null
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return value ?: throw IllegalStateException("Initalize me!")
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
this.value = value
}
}
To clarify - delegates aren't values holder, they are handlers of get/set operations. You can take a look how it works under the hood if you decompile your Example class (Tools -> Kotlin -> Show Kotlin bytecode -> Decompile).
public final class Example {
// $FF: synthetic field
static final KProperty[] $$delegatedProperties = ...
#NotNull
private final DelegateExample p$delegate = new DelegateExample();
#NotNull
public final String getP() {
return (String)this.p$delegate.getValue(this, $$delegatedProperties[0]);
}
public final void setP(#NotNull String var1) {
Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
this.p$delegate.setValue(this, $$delegatedProperties[0], var1);
}
}
No magic here, just creating instance of the DelegateExample and its get/set method invoking
In Kotlin, how do i define a var that has a lazy default value ?
for example, a val would be something like this:
val toolbarColor by lazy {color(R.color.colorPrimary)}
What i want to do is, have a default value for some property (toolbarColor), and i can change that value for anything else. Is it possible?
EDIT: This does the partial trick.
var toolbarColor = R.color.colorPrimary
get() = color(field)
set(value){
field = value
}
Is it possible to ease this by writing
var toolbarColor = color(R.color.colorPrimary)
set(value){
field = value
}
in a way that the default value is computed lazily? At the moment it won't work because color() needs a Context that is only initialized later.
You can create your own delegate method:
private class ColorDelegate<T>(initializer: () -> T) : ReadWriteProperty<Any?, T> {
private var initializer: (() -> T)? = initializer
private var value: T? = null
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
return value ?: initializer!!()
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
this.value = value
}
}
Declare in some delegate:
object DelegatesExt {
fun <T> lazyColor(initializer: () -> T): ReadWriteProperty<Any?, T> = ColorDelegate(initializer)
}
And use as follow:
var toolbarColor by DelegatesExt.lazyColor {
// you can have access to your current context here.
// return the default color to be used
resources.getColor(R.color.your_color)
}
...
override fun onCreate(savedInstanceState: Bundle?) {
// some fun code
// toolbarColor at this point will be R.color.your_color
// but you can set it a new value
toolbarColor = resources.getColor(R.color.new_color)
// now toolbarColor has the new value that you provide.
}
I think this could be a cleaner way to do, but I don't know yet (starting with kotlin few days ago). I will take a look and see if this could be done with less code.
You can store your property in a map to basically create a mutable lazy. You need a mutable map (like a HashMap<K, V>) with a default function to delegate to:
var toolbarColor by hashMapOf<String, Any?>()
.withDefault { toolbarColor = R.color.colorPrimary; toolbarColor }
You'll also need to import some extension functions: import kotlin.properties.getValue and import kotlin.properties.setValue.
It would be nice if Kotlin provided something built-in and optimized for this (like a mutableLazy or something). As such, I've created KT-10451.