Kotlin: workaround for no lateinit when using custom setter? - android

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

How to write my own by extension function

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

Kotlin extension field for SharedPreference value by string constant key

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() }

Type inference failed. Please try to specify type argument explicity : Kotlin

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 delegate property ,In a get() method how i can access the value?

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

Kotlin lazy default property

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.

Categories

Resources