New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Request for community comments: Better Kotlin support #4701

Closed
cmelchior opened this Issue May 24, 2017 · 26 comments

Comments

Projects
None yet
8 participants
@cmelchior
Copy link
Contributor

cmelchior commented May 24, 2017

With an awesome I/O17 being over, one of the big announcements were official support for Kotlin.

Realm is already fully interoperable with Kotlin and we have a Kotlin example here and here that demonstrates how it works.

But we want to make that support even better and In order to do that, we are looking for feedback on what you would most like to see changed so Realm works even better with Kotlin

We have a few ideas ourselves:

  1. (DONE): Add extension functions for all classes with methods that currently accept Class<? extends RealmModel> so KClass can be used directly, e.g. realm.where(Person::class) instead of realm.where(Person::class.java).

  2. (DONE) Detect nullability automatically in model classes. Right now you are required to use the Realm @Required annotation, but we can add support for JetBrains @NotNull as well which is how the Kotlin bytecode indicate not-null values. Example var name : String instead of of @Required var name : String

  3. (DONE) Annotate our public API with JSR305 annotations in a similar way to Okio/OKHttp. This means that Realm will expose correctly the nullability of return values and input paramaters. For example: val result : RealmResults<Person> = realm.where(Person::class).findAll() instead of val results : RealmResults<Person>? = realm.where(Person::class).findAll(). Also being tracked here: #4643

But we are very interested what problems you are running into when using Realm from Kotlin.

@levibostian

This comment has been minimized.

Copy link

levibostian commented May 24, 2017

🎊 Kotlin!!! 🎊

When you say problems you are having with Realm using Kotlin, I do not have many problems. realm-java works really well with Kotlin at the moment. I have a few apps that are 100% Kotlin and Realm code bases. Works great.

My vote for priority is your list item #2. Here is an example of a Realm model I create in a Kotlin app using Kotlin nullability data type in them for optional fields:

open class FooModel(@PrimaryKey override var realm_id: Int = 0,
                      @SerializedName("id") override var api_id: Int = 0,
                      var name: String? = null,
                      var created_at: Date = Date(),
                      var updated_at: Date = Date(),
                      var position: Int? = null,
                      var bar: BarModel? = null): RealmObject() 

So not needing annotations and instead using the data types of my Kotlin properties would be awesome.

@Zhuinden

This comment has been minimized.

Copy link
Contributor

Zhuinden commented May 27, 2017

Was there a way to support a non-null initializing value (like "empty RealmResults") for a @LinkingObjects?

@cmelchior

This comment has been minimized.

Copy link
Contributor

cmelchior commented May 27, 2017

It would be relatively easy if we throw on creating queries, basically just creating an empty stub object, but if queries should be allowed it gets enormously more tricky.

@zaki50

This comment has been minimized.

Copy link
Contributor

zaki50 commented May 31, 2017

I think we can do something for async/await (coroutine). It's still experimental though.

https://kotlinlang.org/docs/reference/coroutines.html

@cmelchior

This comment has been minimized.

Copy link
Contributor

cmelchior commented Jun 1, 2017

@zaki50 Got anything specific in mind?

@zaki50

This comment has been minimized.

Copy link
Contributor

zaki50 commented Jun 1, 2017

@cmelchior Sorry, no.

@heinrichreimer

This comment has been minimized.

Copy link

heinrichreimer commented Jun 4, 2017

Maybe async queries could use Kotlin coroutines.

@zaki50

This comment has been minimized.

Copy link
Contributor

zaki50 commented Jun 4, 2017

@heinrichreimer

This comment has been minimized.

Copy link

heinrichreimer commented Jun 4, 2017

  1. For @cmelchior's first suggestion:
    /**
     * Use [Realm.where] with [KClass]es.
     */
    fun <E : RealmModel> Realm.where(clazz: KClass<E>): RealmQuery<E> {
        return where(clazz.java)
    }
  2. Group RealmQuery conditions:
    /**
     * Group conditions on a [RealmQuery].
     */
    fun <E : RealmModel> RealmQuery<E>.group(body: () -> Unit) {
        beginGroup()
        body()
        endGroup()
    }
  3. LiveData support:
    /**
     * Map [RealmResults] to a [LiveData].
     */
    fun <E : RealmModel> RealmResults<E>.toLiveData(): LiveData<RealmResults<E>> {
        val liveData = MutableLiveData<RealmResults<E>>()
        addChangeListener{ results ->
            liveData.value = results
        }
        return liveData
    }
    
    /**
     * Map [RealmObject] to a [LiveData].
     */
    fun <E : RealmObject> E.toLiveData(): LiveData<E> {
        val liveData: MutableLiveData<E> = MutableLiveData()
        this.addChangeListener<E> {
            result -> liveData.value = result
        }
        return liveData
    }
@Zhuinden

This comment has been minimized.

Copy link
Contributor

Zhuinden commented Jun 4, 2017

@heinrichreimer

why not something like

/**
 * Group conditions on a [RealmQuery].
 */
fun <E : RealmModel> RealmQuery<E>.group(body: (RealmQuery<E>) -> Unit) { // <-- changed here
    beginGroup()
    body()
    endGroup()
}

I'm not sure about Kotlin generic syntax, but you get the idea.

@heinrichreimer

This comment has been minimized.

Copy link

heinrichreimer commented Jun 4, 2017

@Zhuinden You're right! But it would be even better if the higher order function (body) passed to the group would be an extension function on the RealmQuery itself.

/**
 * Group conditions on a [RealmQuery].
 */
fun <E : RealmModel> RealmQuery<E>.group(body: RealmQuery<E>.() -> Unit) {
    beginGroup()
    body()
    endGroup()
}
@cmelchior

This comment has been minimized.

Copy link
Contributor

cmelchior commented Jun 9, 2017

Another Kotlin fun fact:

Long::class.java actually returns a Class reference to long not Long, this is really easy mistaken when writing migrations:

realm?.schema?.create("Person")
        ?.addField("id", Long::class.java) // Non-nullable

// Instead we should document that people need to use these instead
.addField("id", Long::class.javaObjectType) // Nullable
.addField("id", Long::class.javaPrimitiveType) // Non-nullable

Even better would be supporting the KClass equivalents directly:

.addField("id", Long::class) // Non-nullable
.addField("id", Long::class).setNullable("id") // Being nullable is hard in Kotlin
@heinrichreimer

This comment has been minimized.

Copy link

heinrichreimer commented Jun 12, 2017

@cmelchior I wonder if there would be something like Long?::class in Kotlin...

@rdadkins

This comment has been minimized.

Copy link

rdadkins commented Jul 10, 2017

@zaki50

Maybe async queries could use Kotlin coroutines.

@cmelchior

@zaki50 Got anything specific in mind?

Perhaps a Realm context with a Continuation that exposes the Realm thread to work with objects "across" threads.

@zaki50

This comment has been minimized.

Copy link
Contributor

zaki50 commented Jul 15, 2017

Adding a method similar to executeTransaction() but it can have return value and this becomes Realm.

fun <T> Realm.callTransaction(transaction: Realm.() -> T): T {
    val ref = AtomicReference<T>()
    executeTransaction {
        ref.set(transaction(it))
    }
    return ref.get()
}

We can write a transaction like:

val (group, artifact) = realm.callTransaction {
    val group = createObject(Group::class)
    group.name = "foo"

    val artifact = createObject(Artifact::class)
    artifact.name = "bar"

    Pair(group, artifact)
}

group.name == "foo"
val (group, subGroup, artifact) = realm.callTransaction {
    val group = createObject(Group::class)
    group.name = "foo"

    val subGroup = createObject(SubGroup::class)
    subGroup.name = "bar"

    val artifact = createObject(Artifact::class)
    artifact.name = "baz"

    Triple(group, subGroup, artifact)
}

artifact.name == "baz"
@zaki50

This comment has been minimized.

Copy link
Contributor

zaki50 commented Jul 15, 2017

Adding static methods in RealmObject as extension functions to RealmModel like

fun RealmModel.deleteFromRealm() {
    RealmObject.deleteFromRealm(this)
}
@RealmClass
open class Foo(
    var name: String? = null
) : RealmModel

f: Foo = realm.where(Foo::class).findFirst()
f.deleteFromRealm()
@zaki50

This comment has been minimized.

Copy link
Contributor

zaki50 commented Jul 15, 2017

Supporting type-safe query.

fun <T : RealmModel> RealmQuery<T>.equalTo(property: KMutableProperty1<T, out String?>,
                                           value: String, case: Case = Case.SENSITIVE): RealmQuery<T> {
    return this.equalTo(property.name, value, case)
}

fun <T : RealmModel> RealmQuery<T>.equalTo(property: KMutableProperty1<T, out Int?>,
                                           value: Int): RealmQuery<T> {
    return this.equalTo(property.name, value)
}

...
@RealmClass
open class Foo(
    var name: String? = null,
) : RealmModel

@RealmClass
open class Bar(
    var name: String? = null,
) : RealmModel

realm.where(Foo::class).equalTo(Foo::name, "abc") // OK
realm.where(Foo::class).equalTo(Foo::name, 0) // compile error (name field is not Int
realm.where(Foo::class).equalTo(Bar::name, "abc") // compile error(Model classes are different
@cmelchior

This comment has been minimized.

Copy link
Contributor

cmelchior commented Jul 17, 2017

Type-safe queries are a nice idea, but right now it would require a dependency to kotlin-reflect which afaik pulls in ~10K extra methods.

@heinrichreimer

This comment has been minimized.

Copy link

heinrichreimer commented Jul 17, 2017

Maybe it would be worth it to pull out Kotlin support in a separate dependency. Then developers could decide on their own if they need type safety.

@zaki50

This comment has been minimized.

Copy link
Contributor

zaki50 commented Jul 19, 2017

use reified when defining extension functions that take Class parameter.

inline fun <reified T : RealmModel> Realm.where(): RealmQuery<T> {
    return this.where(T::class.java)
}

realm.where<User>().equalTo(...)
@zaki50

This comment has been minimized.

Copy link
Contributor

zaki50 commented Jul 20, 2017

@cmelchior

Type-safe queries are a nice idea, but right now it would require a dependency to kotlin-reflect which afaik pulls in ~10K extra methods.

I was thinking so, but it seems to be working without kotlin-reflect dependency.
I tested it with https://github.com/zaki50/GoogleRepositoryChecker/

@ericmaxwell2003

This comment has been minimized.

Copy link

ericmaxwell2003 commented Sep 8, 2017

@zaki50 @cmelchior

I was thinking so, but it seems to be working without kotlin-reflect dependency.

Right, it's not using reflection. I thought this SO answer was a great explanation, helped me grok it a bit better. https://stackoverflow.com/questions/45949584/what-does-the-reified-keyword-in-kotlin-really-do

I wonder if Type Safe builders could be used in some creative way for complex queries

val peopleWithPuppies = realm.where<Person>() { dog { age {  lessThan(2) }}

// given
@RealmClass
open class Person : RealmModel {
    @PrimaryKey var id=""
    var name =""
    var dog: Dog? = null
}

@RealmClass
open class Dog : RealmModel {
    @PrimaryKey var id=""
    var name = ""
    var age = 0
}

// Where that query would equate to
realm.where<Person>().lessThan("dog.age", 2)

Not super elegant, but it is type safe. :-)

@heinrichreimer

This comment has been minimized.

Copy link

heinrichreimer commented Oct 11, 2017

In Kotlin in is a keyword, so there should be a way to call in(String fieldName, String[] values) with another method name. I would suggest:

/**
 * Map Realm's [RealmQuery.in] methods to `oneOf` to bypass using Kotlin's `in` keyword
 * as a method name.
 */
fun <E : RealmModel> RealmQuery<E>.oneOf(fieldName: String,
        values: Array<String>): RealmQuery<E> = `in`(fieldName, values)
@zaki50

This comment has been minimized.

Copy link
Contributor

zaki50 commented Oct 13, 2017

@Zhuinden

This comment has been minimized.

Copy link
Contributor

Zhuinden commented Jun 18, 2018

@heinrichreimer the future says that the function we mentioned should have inline modifier for better performance 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment