I have a custom Preferences class which uses Kotlin extensions to return a Preference string.
It works perfect in API 28, but won't compile in API 29. With Googles new rules about not allowing app updates that target below API 30, I need to update this app, but can't figure out this basic issue.
Here is my Preference class:
import android.content.SharedPreferences
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class PreferenceProperty<T> internal constructor(
private val getter: SharedPreferences.(key: String, defaultValue: T) -> T,
private val setter: SharedPreferences.Editor.(key: String, value: T) -> SharedPreferences.Editor,
private val defaultValue: T,
private val key: String? = null
) : ReadWriteProperty<SharedPreferences, T> {
override fun getValue(thisRef: SharedPreferences, property: KProperty<*>): T {
return thisRef.getter(key ?: property.name, defaultValue)
}
override fun setValue(thisRef: SharedPreferences, property: KProperty<*>, value: T) {
thisRef.edit().setter(key ?: property.name, value).apply()
}
}
fun SharedPreferences.stringPreference(defaultValue: String): PreferenceProperty<String> {
return PreferenceProperty(
SharedPreferences::getString,
SharedPreferences.Editor::putString,
defaultValue
)
}
fun SharedPreferences.nullableStringPreference(defaultValue: String? = null): PreferenceProperty<String?> {
return PreferenceProperty(
SharedPreferences::getString,
SharedPreferences.Editor::putString,
defaultValue
)
}
I am using the stringPreference, but also need nullableStringPreference for other parts of the code.
I use it as follows:
var notificationToken: String by stringPreference("")
But I get this error:
Type inference failed. Expected type mismatch: inferred type is PreferenceProperty<String?> but PreferenceProperty<String> was expected
So the issue is that, since API 29, it returns String?.
Does anyone know change in API29 caused this and how to work around it?
Thanks
Weird. There must have been something changed in how nullability annotations are interpretted to make it stricter. I don't see any signature or annotation change on the getString() method between the two versions of the SDK. Logically, this is the correct treatment of the return value since it's marked #Nullable. There's no annotation for the return value to match the nullability of a method parameter.
You can replace
SharedPreferences::getString
with
{ key, def -> getString(key, def)!! }
To get it working again. !! is logically safe here, but if you're pedantic about avoiding it you could use:
{ key, def -> getString(key, null) ?: def }
Related
The problem starts with getParcelableArrayListExtra doesn't support type check when we try to set it to a variable. Let me give an example as basic as I can.
A User Class.
import kotlinx.parcelize.Parcelize
import android.os.Parcelable
#Parcelize
data class UserClass(
var name: String? = null,
var text: String? = null,
var age: Int? = null
) : Parcelable
The random class which we'll try to set to the User variable.
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
#Parcelize
data class MessageClass(
val title: String?, = Constant.STRING_EMPTY
val text: String? = Constant.STRING_EMPTY
) : Parcelable
The class that fills intent
class FillIntentClass(){
//Let's say one of the developers added the MessageClass object inside our intent.
//Or BE sent the wrong type of object and I passed its value to the intent.
private fun DummyFunctionToSetIntent(){
val messageList = arraylistOf(MessageClass(title = "hello",text ="dummy text")
intent.putParcelableArrayListExtra(EXTRA_PAYMENT_OPTIONS_EXTRA, messageList)
}
}
Test class
class MyTestClass(){
// UserList variable
private var mUserList: ArrayList<UserClass>? = null
override fun onCreate(savedInstanceState: Bundle?) {
...
with(intent) {
// In this situation, mUserList became the type of ArrayList<MessageClass>
// But it shouldn't be possible. Because it must accept only ArrayList<UserClass>
// And that causes mostly crashes when the other code parts use it.
mUserList = getParcelableArrayListExtra(EXTRA_PAYMENT_OPTIONS_EXTRA)
// mUserList now pretend its like ArrayList<MessageClass>. But i set it as ArrayList<UserClass> at the top of the class.
// The best way to solve this is to type check with as?. If the type is not as expected it must return null.
// But I cannot use type check here. It gives me a "Not enough information to infer type variable T" error.
mUserList = getParcelableArrayListExtra(EXTRA_PAYMENT_OPTIONS_EXTRA) as? ArrayList<UserClass> //(compile error here on IDE)
// So I had to come out with the below solution. But I cannot say it's the best practice.
if (getParcelableArrayListExtra<UserClass>(EXTRA_PAYMENT_OPTIONS_EXTRA)
?.filterIsInstance<UserClass>()?.isNotEmpty() == true
) {
mUserList = getParcelableArrayListExtra(EXTRA_PAYMENT_OPTIONS_EXTRA)
}
}
}
}
Type check(as,as?) works with getParcelable functions as expected. But when it comes to the getParcelableArrayListExtra it just doesn't work and gives compile error as I explained above.
Do you have any knowledge of what's the best option for as, as? check? And how it's possible for mUserList to accept a different type of Array and pretend like it?
This is a mess for a few reasons:
You are coding in Kotlin, but the classes you are dealing with (Parcelable, Bundle, Intent, ArrayList) are actually Java
Generics in Java are a hack
I would split the problem into 2 parts:
Unparcel the ArrayList into ArrayList<Parcelable>
Check/convert the contents of the ArrayList<Parcelable> into the expected type
Check the API level and code accordingly:
if (Build.VERSION.SDK_INT >= 33) {
data = intent.getParcelableExtra (String name, Class<T> clazz)
}else{
data = intent.getParcelableExtra("data")
}
Also you can use these extensions for bundle and intent:
inline fun <reified T : Parcelable> Intent.parcelable(key: String): T? = when {
SDK_INT >= 33 -> getParcelableExtra(key, T::class.java)
else -> #Suppress("DEPRECATION") getParcelableExtra(key) as? T
}
inline fun <reified T : Parcelable> Bundle.parcelable(key: String): T? = when {
SDK_INT >= 33 -> getParcelable(key, T::class.java)
else -> #Suppress("DEPRECATION") getParcelable(key) as? T
}
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
}
}
I have a function filter here
fun filter(category: String) {
...
}
and a Class with many constant string
object Constants {
val CAT_SPORT = "CAT_SPORT"
val CAT_CAR = "CAT_CAR"
...
}
How to ensure the parameter category is a constant string from Constants (or throw warning)?
I am looking for something like #StringRes.
I know Enum may do the trick but prefer not to code refactor at this moment.
Using androidx.annotation you can do something like this:
object Constants {
#Retention(AnnotationRetention.SOURCE)
#StringDef(CAT_SPORT, CAT_CAR)
annotation class Category
const val CAT_SPORT = "CAT_SPORT"
const val CAT_CAR = "CAT_CAR"
}
fun filter(#Constants.Category category: String) {
...
}
I'd like to provide multiple different delegates from a single class, with differing types. For example:
class A {
val instanceOfB = B()
val aNumber: SomeType by instanceOfB
val anotherNumber: SomeOtherType by instanceOfB
}
class B {
operator fun <T1: SomeType> getValue(thisRef: Any?, property: KProperty<T1>): T1 {
return SomeType()
}
operator fun <T2: SomeOtherType> getValue(thisRef: Any?, property: KProperty<T2>): T2 {
return SomeOtherType()
}
}
open class SomeType {}
open class SomeOtherType {}
This example gives the following compiler error:
'operator' modifier is inapplicable on this function: second parameter must be of type KProperty<*> or its supertype
Is there some way to specify the generic type arguments such that I can achieve this?
Only way I got it to compile and run, albeit I highly advise against using it aside from proof of concept since inline will generate a lot of trash code, and each getValue call will run through entire when statement:
class B {
inline operator fun <reified T : Any>getValue(thisRef: Any?, property: KProperty<*>): T {
return when(T::class.java){
SomeType::class.java -> SomeType() as T
SomeOtherType::class.java-> SomeOtherType() as T
else -> Unit as T
}
}
}
There's also operator fun provideDelegate that generates the delegates, but it's limited to 1 return value as well. I don't think there's elegant / supported way to doing what you need right now.
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? {