I am currently trying to learn Kotlin with the help of the book "Kotlin Programming The Big Nerd Ranch Guide" and so far everything worked.
But now I am struggling with the "lazy" initialization which throws a NullPointerException which says
Cannot invoke "kotlin.Lazy.getValue()" because "< local1>" is null
The corresponding lines are:
val hometown by lazy { selectHometown() }
private fun selectHometown(): String = File("data/towns.txt").readText().split("\n").shuffled().first()
In case you want to compile it yourself or need more code for a better understanding I provide the Game.kt and Player.kt down below. If "lazy" is dropped for a "normal" initialization the hometown gets assigned as intended.
Any tips for solving the problem and understanding the cause of it is welcome.
// Game.kt
package com.bignerdranch.nyethack
fun main(args: Array<String>) {
val player = Player("Madrigal")
player.castFireball()
}
private fun printPlayerStatus(player: Player) {
println("(Aura: ${player.auraColor()}) " + "(Blessed: ${if (player.isBlessed) "YES" else "NO"})")
println("${player.name} ${player.formatHealthStatus()}")
}
// Player.kt
package com.bignerdranch.nyethack
import java.io.File
class Player(_name: String, var healthPoints: Int = 100, val isBlessed: Boolean, private val isImmortal: Boolean) {
var name = _name
get() = "${field.capitalize()} of $hometown"
private set(value) {
field = value.trim()
}
constructor(name: String) : this(name, isBlessed = true, isImmortal = false) {
if (name.toLowerCase() == "kar") healthPoints = 40
}
init {
require(healthPoints > 0, { "healthPoints must be greater than zero." })
require(name.isNotBlank(), { "Player must have a name" })
}
val hometown by lazy { selectHometown() }
private fun selectHometown(): String = File("data/towns.txt").readText().split("\n").shuffled().first()
fun castFireball(numFireballs: Int = 2) =
println("A glass of Fireball springs into existence. (x$numFireballs)")
fun auraColor(): String {
val auraVisible = isBlessed && healthPoints > 60 || isImmortal
return if (auraVisible) "GREEN" else "NONE"
}
fun formatHealthStatus() =
when (healthPoints) {
100 -> "is an excellent condition!"
in 90..99 -> "has a few scratches."
in 75..89 -> if (isBlessed) {
"has some minor wounds but is healing quite quickly"
} else {
"has some minor wounds"
}
in 15..74 -> "looks pretty hurt"
else -> "is in awful condition!"
}
}
I forgot the towns.txt so here it is (not that it matters much)
Neversummer
Abelhaven
Phandoril
Tampa
Sanorith
Trell
Zan'tro
Hermi Hermi
Curlthistle Forest
When something like this happens, it's usually due to bad ordering of initialization.
The initialization of the Player class goes this way:
the name property has its backing field initialized with the _name value
the init block is run, and tries to access name
the getter of name tries to read the hometown property, but fails because hometown is still not initialized
...if things had gone right, the hometown property would be initialized now with the lazy delegate
So basically you're trying to access hometown before the lazy delegate is configured.
If you move hometown's declaration above the init block, you should be fine.
You can see the fix in action on the playground
Related
I'm building my first Room project and need a fresh pair of eyes to see what I'm doing wrong.
Android studio keeps telling me the call to insertBopa or deleteBopa in the BopaRoomDao is an unresolved reference. My code seeme to match other examples I've looked at and tutorials but I just can't work out what I'm doing wrong.
This is my repository.kt
package com.example.mytestapp
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
class BopaRepository(private val bopaRoomDao: BopaRoomDao) {
val allBopaRoomEntry: LiveData<List<BopaRoomEntry>> = bopaRoomDao.allBopas()
val searchResults = MutableLiveData<List<BopaRoomEntry>>()
private val coroutineScope = CoroutineScope(Dispatchers.Main)
fun insertBopaEntry(newbopa: BopaRoomEntry) {
coroutineScope.launch(Dispatchers.IO) {
BopaRoomDao.insertBopa(newbopa)
}
}
fun deleteBopa(name: String) {
coroutineScope.launch(Dispatchers.IO) {
BopaRoomDao.deleteBopa(name)
}
}
fun findBopa(name: String) {
coroutineScope.launch(Dispatchers.Main) {
searchResults.value = asyncFind(name).await()
}
}
fun allBopas(): LiveData<List<BopaRoomEntry>> {
return bopaRoomDao.allBopas()
}
private fun asyncFind(name: String): Deferred<Flow<List<BopaRoomEntry>>> =
coroutineScope.async(Dispatchers.IO) {
return#async bopaRoomDao.findBopa(name)
}
}
This is my Dao
package com.example.mytestapp
import androidx.lifecycle.LiveData
import androidx.room.*
//import java.util.concurrent.Flow
import kotlinx.coroutines.flow.Flow
#Dao
interface BopaRoomDao {
//add new entry to db
#Insert
fun insertBopa(bopaRoomEntry: BopaRoomEntry)
//change entry on db
#Update
fun updateBopa(bopaRoomEntry: BopaRoomEntry)
#Delete
fun deleteBopa(bopaRoomEntry: BopaRoomEntry)
//open list of previous entries from db
#Query("SELECT * FROM bopa_table")
fun findBopa(name: String): Flow<List<BopaRoomEntry>>
#Query("SELECT * FROM bopa_table")
fun allBopas(): LiveData<List<BopaRoomEntry>>
}
This is the BopaRoomEntry class
package com.example.mytestapp
import androidx.annotation.NonNull
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.lang.reflect.Constructor
#Entity (tableName = "BOPA_TABLE")
class BopaRoomEntry {
#PrimaryKey(autoGenerate = true)
#NonNull
#ColumnInfo(name = "bopaId")
var id: Int = 0
#ColumnInfo(name = "bopa_topic")
var bopaTopic: String = ""
#ColumnInfo(name = "bopa_content")
var bopaContent: String = ""
constructor(){}
constructor(bopatopic: String, bopacontent: String) {
//this.id = id
this.bopaTopic = bopatopic
this.bopaContent = bopacontent
}
}
I'm adding the database class to see if it helps clarify one of the answers...
package com.example.mytestapp
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
#Database(entities = [(BopaRoomEntry::class)], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun bopaRoomDao(): BopaRoomDao
companion object {
#Volatile
private var INSTANCE: AppDatabase? = null
fun getInstance(context: Context): AppDatabase? {
synchronized(this) {
var instance = INSTANCE
if (INSTANCE == null) {
instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"bopa-database.db"
).fallbackToDestructiveMigration()
.build()
INSTANCE = instance
}
return instance
}
}
}
}
Any help appreciated :-P
After a closer look:
Could it be you're missing the BopaRoomDao.insertBopa(newbopa) vs the lower-case version: bopaRoomDao.insertBopa(newbopa)?
Do you have a abstract class XXXX : RoomDatabase() { where you define your abstract bopaDao() = BopaRoomDao and is annotated with
#Database(
entities = [
BopaRoomEntry::class,
],
version = 1,
exportSchema = false
)
If so you should be using the "daos" provided by this:
val db = ... //obtain your DB
db.bopaDao().allBopas()
Update
After cloning your project, I see a few issues:
MainViewModel:
You obtain your DB here, in an attempt to construct the Repository. This is fine (though with Hilt/DependencyInjection you would not need to worry) but your Repository is -correctly- expecting a non-nullable version of your DB. So
val bopaDb = AppDatabase.getInstance(application)
val bopaDao = bopaDb.bopaRoomDao()
repository = BopaRepository(bopaDao)
Should really be changed to ensure getInstance cannot return null.
(maybe make INSTANCE a lateinit since you must have a DB to function it appears).
If having a DB is optional, then the repository must either deal with it or the viewmodel must not attempt to use/create a repository. As you can see this can get weird really fast. I'd say having a DB cannot fail or you have other issues.
If you still leave it as optional, then the sake of this demo, change it to:
val bopaDb = AppDatabase.getInstance(application)
val bopaDao = bopaDb?.bopaRoomDao() //add the required `?`
repository = BopaRepository(bopaDao!!) //not good to force unwrap !! but will work.
Alternatively you can make your BopaRepository nullable BopaRepository? and use
repository = bopaDao?.let { BopaRepository(it) } ?: null
but then you have to add ? every time you want to use it... and this in turn will make this more messy.
I'd say your DB method should not return null, if it is null for some random other problem (say the filesystem is full and the DB cannot be created) then you should handle this gracefully elsewhere as this is an exception outside of your control. OR... your repository should fetch the DB and work with a different storage internally, you, the caller, should not care.
Anyway.. after taking care of that...
Let's look at BopaRepository
You have it defined like
class BopaRepository(private val bopaRoomDao: BopaRoomDao) {
The important bit is bopaRoomDao.
(note: I would pass the DB here, not a specific DAO, since the repo may need access to other Daos (though you could argue then it should receive the other Repositories instead) so... your choice).
Red Line 1:
val allBopaRoomEntry: LiveData<List<BopaRoomEntry>> = bopaRoomDao.allBopaEntries()
The problem is that allBopaEntries doesn't exist. In the BopaRoomDao interface, the method is called: allBopas()
So change that to
val allBopaRoomEntry: LiveData<List<BopaRoomEntry>> = bopaRoomDao.allBopas()
Red Line #2
In fun insertBopaEntry(newbopa: BopaRoomEntry) {
BopaRoomDao.insertBopaEntry(newbopa) should be:
bopaRoomDao.insertBopa(newbopa)
Red Line #3:
coroutineScope.launch(Dispatchers.IO) {
BopaRoomDao.deleteBopaEntry(name)
}
}
The DAO in the repo doesn't have a delete method (forgot?)
but should look like bopaRoomDao.delete(theBopaYouWantToDelete)
So:
#Delete
fun deleteBopa(bopaRoomEntry: BopaRoomEntry)
This means you cannot pass a name to the delete method (you could) but then because as far as I remember Room doesn't support a #Delete(...), you need to change it to a "custom" query:
#Query("DELETE FROM bopa_table WHERE bopa_topic=:name")
fun deleteByTopic(topic: String);
In truth, you should probably FETCH the row you want to delete and pass that to the original method.
For more info take a look at this SO answer.
Red Line #4
fun findBopa(name: String) {
You need to collect the flow:
fun findBopa(name: String){
coroutineScope.launch(Dispatchers.Main) {
val result = asyncFind(name).await()
result.collect {
searchResults.postValue(it)
}
}
}
This will have another issue though. You're not using the name you pass to find:
So it should look like:
//open list of previous entries from db
#Query("SELECT * FROM bopa_table WHERE bopa_topic=:name")
fun findBopa(name: String): Flow<List<BopaRoomEntry>>
(assuming name is the bopa_topic).
Red Line #5
fun allBopas(): LiveData<List<BopaRoomEntry>> {
Should do return bopaRoomDao.allBopas() (incorrect name)
This one is strange as allBopaRoomEntry is a public variable, you should either make that one private or remove it, since you have this method that returns the reference to the same thing.
Red Line #6
Last but not least,
fun asyncFind(name: String): Deferred<Flow<List<BopaRoomEntry>>>
returns a Flow (deferred but flow) so I think you'd want to do this:
= coroutineScope.async(Dispatchers.IO) {
return#async bopaRoomDao.findBopa(name)
}
Given that findBopa returns a Flow<List<BopaRoomEntry>> already.
With these changes, the project almost built correctly, but there's another issue in MainActivity:
//button actions
binding.saveBopaEntry.setOnClickListener{
//code for sending editText to db
BopaRoomDao.updateBopa(bopaTopic = R.id.bopaTopic, bopaContent = R.id.bopaContent)
}
This shouldn't be there. The click listener should tell the ViewModel: The User pressed save on this item.
viewModel.onSaveBopa(...)
And the ViewModel should launch a coroutine in its scope:
fun onSaveBopa(bopa: Bopa) {
viewModelScope.launch {
repo.updateBopa(bopa)
}
}
Keep in mind this is pseudo-code. If you pass the topic/content directly, then also pass the ID so the viewModel knows what BOPA must be updated in the database...
fun onSaveBopa(id: String, topic: String, content: String)
That's a more plausible method to call from your activity. But it really depends on what you're trying to do. in any case the activity should not need to deal with DB, Room, Daos, etc. Rely on your ViewModel, that's what it's doing there.
Anyway, commenting that in the Activity... made the project finally build
I hope that helps you ;) Good Luck.
Say I have a class Foo that looks like this (Kotlin code):
class Foo {
fun ok(): Boolean = true
fun notOk(): Boolean = throw IllegalArgumentException("test")
}
Now say I have a schema where I define the following:
type Query {
getFoo: Foo!
}
type Foo {
ok: Boolean!
notOk: Boolean!
}
Assume that getFoo is in a resolver somewhere. Now if I do the following query:
{
getFoo {
ok
notOk
}
}
The whole thing just fails (as expected), but what I want to achieve is that only the notOk field should turn up in the error part of the response and the ok field should turn up as part of the data part of the response.
For reference, I am using: https://github.com/excitement-engineer/ktor-graphql along with ktor.
As far as I can understand, I need to make a DataFetcherExceptionHandler object, but I don't understand how I can both fulfill the non-erroring fields and the erroring field. Any ideas? All I have achieved so far, is to be able to have errors and data in the same query as long as they are top-level fields.
val exceptionHandler = DataFetcherExceptionHandler { handlerParameters ->
val exception = handlerParameters.exception
val sourceLocation = handlerParameters.sourceLocation
val path = handlerParameters.path
val error = object : GraphQLError {
override fun getMessage() = exception.message
override fun getLocations() = mutableListOf(sourceLocation)
override fun getErrorType() = ErrorType.DataFetchingException
}
DataFetcherExceptionHandlerResult.newResult().error(error).build()
}
Looking for a natural Kotlin way to let startTime be initialized only in a particular place and exactly once.
The following naive implementation have two problems:
it is not thread safe
it does not express the fact "the variable was or will be assigned exactly once in the lifetime of an Item instance"
class Item {
var startTime: Instant?
fun start(){
if (startTime == null){
startTime = Instant.now()
}
// do stuff
}
}
I believe some kind of a delegate could be applicable here. In other words this code needs something similar to a lazy variable, but without initialization on first read, instead it happens only after explicit call of "touching" method. Maybe the Wrap calls could give an idea of possible implementation.
class Wrap<T>(
supp: () -> T
){
private var value: T? = null
private val lock = ReentrantLock()
fun get(){
return value
}
fun touch(){
lock.lock()
try{
if (value == null){
value = supp()
} else {
throw IllegalStateExecption("Duplicate init")
}
} finally{
lock.unlock()
}
}
}
How about combining AtomicReference.compareAndSet with a custom backing field?
You can use a private setter and make sure that the only place the class sets the value is from the start() method.
class Item(val value: Int) {
private val _startTime = AtomicReference(Instant.EPOCH)
var startTime: Instant?
get() = _startTime.get().takeIf { it != Instant.EPOCH }
private set(value) = check(_startTime.compareAndSet(Instant.EPOCH, value)) { "Duplicate set" }
fun start() {
startTime = Instant.now()
}
override fun toString() = "$value: $startTime"
}
fun main() = runBlocking {
val item1 = Item(1)
val item2 = Item(2)
println(Instant.now())
launch { println(item1); item1.start(); println(item1) }
launch { println(item1) }
delay(1000)
println(item2)
item2.start()
println(item2)
println(item2)
item2.start()
}
Example output:
2021-07-14T08:20:27.546821Z
1: null
1: 2021-07-14T08:20:27.607365Z
1: 2021-07-14T08:20:27.607365Z
2: null
2: 2021-07-14T08:20:28.584114Z
2: 2021-07-14T08:20:28.584114Z
Exception in thread "main" java.lang.IllegalStateException: Duplicate set
I think your Wrap class is a good starting point to implement this. I would definitely make it a property delegate and touch() could be much simplified:
fun touch() {
synchronized(this) {
check(value == null) { "Duplicate init" }
value = supp()
}
}
Then you can remove lock. But generally, this is a good approach.
If you would like to reuse lazy util from stdlib then you can do this by wrapping it with another object which does not read its value until asked:
class ManualLazy<T : Any>(private val lazy: Lazy<T>) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): T? {
return if (lazy.isInitialized()) lazy.value else null
}
fun touch() {
lazy.value
}
}
class Item {
private val _startTime = ManualLazy(lazy { Instant.now() })
val startTime: Instant? by _startTime
fun start(){
_startTime.touch()
}
}
Of course, depending on your needs you can implement it in a much different way, using a similar technique.
This may be considered exploiting or hacking lazy util. I agree and I think Wrap approach is a better one.
I've a question about, how would you handle this case?
Imagine that you have to do a validation of an object and that validation should have a sort of importance, in this case we only have 3 validations, each one can result Valid or his own QualityCheck enum value.
This is the method example in kotlin and the validations
sealed class Validation {
abstract fun validate(bobject: ObjectToCheck): QualityCheck
object VeryImportantValidation : Validation() {
override fun validate(bobject: ObjectToCheck): QualityCheck =
if (isValid(bobject.valueX)) QualityCheck.Valid
else QualityCheck.VeryImportantInvalid
}
object SecondMostImportant : Validation() {
override fun validate(bobject: ObjectToCheck): QualityCheck =
if (isValid(bobject.valueNotSoImportant)) QualityCheck.Valid
else QualityCheck.SecondMostImportantInvalid
}
object NotSoImportant : Validation() {
override fun validate(bobject: ObjectToCheck): QualityCheck =
if (isValid(bobject.valueNothingImportant)) QualityCheck.Valid
else QualityCheck.NotSoImportantInvalid
}
}
fun getQualityCheck(object: ObjectToCheck): QualityCheck =
if (VeryImportantValidation.validate(object) === QualityCheck.Valid) {
if (SecondMostImportant.validate(object) === QualityCheck.Valid) {
NotSoImportant(paymentsRepository.getSystemPayments()).validate(object)
} else {
QualityCheck.SecondMostImportantInvalid
}
} else {
QualityCheck.VeryImportantInvalid
}
I think this is not scalable neither easy to read/understand or modify if we would want to add a new one.
There is any kind to do this elegant and easier to include more validations?
If you invert your Boolean conditions, you can eliminate the nesting. Then you can change it to a when statement for simplicity:
fun getQualityCheck(object: ObjectToCheck): QualityCheck = when {
VeryImportantValidation.validate(object) !== QualityCheck.Valid ->
QualityCheck.VeryImportantInvalid
SecondMostImportant.validate(object) !== QualityCheck.Valid ->
QualityCheck.SecondMostImportantInvalid
else ->
NotSoImportant(paymentsRepository.getSystemPayments()).validate(object)
}
Validation like this is a perfect candidate for the "Rules engine pattern"... mostly known as a for loop.
You just set up a List<Validation> with all of the validations you want to run and iterate over them calling the validate method. You have 2 options, collect all errors (doing a fold on the list), or stop the loop after the first error with a asSequence().map().takeWhile().
I forgot to say, you don't need to seal the Validation class. What is your intent with that?
Scalability/Extensibility would depend from situation to situation and a code cannot be open to all types of changes. One rule of thumb is to keep it as simple as possible and when a requirement is changed we ensure that the code is open to such kind of changes.
Also, I agree with #Augusto. Your use of the sealed class is not really how it is intended to be used.
Anyways let's look at how it would be easier to add a new validation, change the severity of the violation, or have several validations with the same severity.
Lets define an interface for Validations.
interface Validation {
fun validate(value: Int): Boolean
}
Now let's define a few Validations
class LimitValidation: Validation{
override fun validate(value: Int) = value < 100
}
class PositiveValidation: Validation {
override fun validate(value: Int) = value > 0
}
class EvenValidation: Validation {
override fun validate(value: Int) = value % 2 == 0
}
Let's say you have the following Violations
enum class Violation {
SEVERE,
MODERATE,
TYPICAL
}
We can make use of sealed class to define the quality.
sealed class Quality {
object High : Quality()
data class Low(val violation: Violation) : Quality()
}
We can create a class responsible for checking the Quality.
class QualityEvaluator {
private val violationMap: MutableMap<KClass<*>, Violation> = mutableMapOf()
init {
violationMap[LimitValidation::class] = Violation.SEVERE
violationMap[PositiveValidation::class] = Violation.MODERATE
violationMap[EvenValidation::class] = Violation.TYPICAL
}
fun evaluateQuality(value: Int, validations: List<Validation>) : Quality {
val sortedValidations = validations.sortedBy(::violationFor)
sortedValidations.forEach {
if(!it.validate(value)) {
return Quality.Low(violationFor(it))
}
}
return Quality.High
}
private fun <T: Validation> violationFor(validation: T): Violation {
return if (violationMap.containsKey(validation::class)) {
requireNotNull(violationMap[validation::class])
} else {
Violation.TYPICAL
}
}
}
Finally, we can use all this like so:
val validations = listOf(LimitValidation(), PositiveValidation(), EvenValidation())
when(val quality = QualityEvaluator().evaluateQuality(8, validations)) {
is Quality.High -> println("Quality is High")
is Quality.Low -> println("Quality is Low. Violation: ${quality.violation}")
}
I played about with Kotlin's unsupported JavaScript backend in 1.0.x and am now trying to migrate my toy project to 1.1.x. It's the barest bones of a single-page web app interfacing with PouchDB. To add data to PouchDB you need JavaScript objects with specific properties _id and _rev. They also need to not have any other properties beginning with _ because they're reserved by PouchDB.
Now, if I create a class like this, I can send instances to PouchDB.
class PouchDoc(
var _id: String
) {
var _rev: String? = null
}
However, if I do anything to make the properties virtual -- have them override an interface, or make the class open and create a subclass which overrides them -- the _id field name becomes mangled to something like _id_mmz446$_0 and so PouchDB rejects the object. If I apply #JsName("_id") to the property, that only affects the generated getter and setter -- it still leaves the backing field with a mangled name.
Also, for any virtual properties whose names don't begin with _, PouchDB will accept the object but it only stores the backing fields with their mangled names, not the nicely-named properties.
For now I can work around things by making them not virtual, I think. But I was thinking of sharing interfaces between PouchDoc and non-PouchDoc classes in Kotlin, and it seems I can't do that.
Any idea how I could make this work, or does it need a Kotlin language change?
I think your problem should be covered by https://youtrack.jetbrains.com/issue/KT-8127
Also, I've created some other related issues:
https://youtrack.jetbrains.com/issue/KT-17682
https://youtrack.jetbrains.com/issue/KT-17683
And right now You can use one of next solutions, IMO third is most lightweight.
interface PouchDoc1 {
var id: String
var _id: String
get() = id
set(v) { id = v}
var rev: String?
var _rev: String?
get() = rev
set(v) { rev = v}
}
class Impl1 : PouchDoc1 {
override var id = "id0"
override var rev: String? = "rev0"
}
interface PouchDoc2 {
var id: String
get() = this.asDynamic()["_id"]
set(v) { this.asDynamic()["_id"] = v}
var rev: String?
get() = this.asDynamic()["_rev"]
set(v) { this.asDynamic()["_rev"] = v}
}
class Impl2 : PouchDoc2 {
init {
id = "id1"
rev = "rev1"
}
}
external interface PouchDoc3 { // marker interface
}
var PouchDoc3.id: String
get() = this.asDynamic()["_id"]
set(v) { this.asDynamic()["_id"] = v}
var PouchDoc3.rev: String?
get() = this.asDynamic()["_rev"]
set(v) { this.asDynamic()["_rev"] = v}
class Impl3 : PouchDoc3 {
init {
id = "id1"
rev = "rev1"
}
}
fun keys(a: Any) = js("Object").getOwnPropertyNames(a)
fun printKeys(a: Any) {
println(a::class.simpleName)
println(" instance keys: " + keys(a).toString())
println("__proto__ keys: " + keys(a.asDynamic().__proto__).toString())
println()
}
fun main(args: Array<String>) {
printKeys(Impl1())
printKeys(Impl2())
printKeys(Impl3())
}
I got a good answer from one of the JetBrains guys, Alexey Andreev, over on the JetBrains forum at https://discuss.kotlinlang.org/t/controlling-the-jsname-of-fields-for-pouchdb-interop/2531/. Before I describe that, I'll mention a further failed attempt at refining #bashor's answer.
Property delegates
I thought that #bashor's answer was crying out to use property delegates but I couldn't get that to work without infinite recursion.
class JSMapDelegate<T>(
val jsobject: dynamic
) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
return jsobject[property.name]
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
jsobject[property.name] = value
}
}
external interface PouchDoc4 {
var _id: String
var _rev: String
}
class Impl4() : PouchDoc4 {
override var _id: String by JSMapDelegate<String>(this)
override var _rev: String by JSMapDelegate<String>(this)
constructor(_id: String) : this() {
this._id = _id
}
}
The call within the delegate to jsobject[property.name] = value calls the set function for the property, which calls the delegate again ...
(Also, it turns out you can't put a delegate on a property in an interface, even though you can define a getter/setter pair which work just like a delegate, as #bashor's PouchDoc2 example shows.)
Using an external class
Alexey's answer on the Kotlin forums basically says, "You're mixing the business (with behaviour) and persistence (data only) layers: the right answer would be to explicitly serialise to/from JS but we don't provide that yet; as a workaround, use an external class." The point, I think, is that external classes don't turn into JavaScript which defines property getters/setters, because Kotlin doesn't let you define behaviour for external classes. Given that steer, I got the following to work, which does what I want.
external interface PouchDoc5 {
var _id: String
var _rev: String
}
external class Impl5 : PouchDoc5 {
override var _id: String
override var _rev: String
}
fun <T> create(): T = js("{ return {}; }")
fun Impl5(_id: String): Impl5 {
return create<Impl5>().apply {
this._id = _id
}
}
The output of keys for this is
null
instance keys: _id
__proto__ keys: toSource,toString,toLocaleString,valueOf,watch,unwatch,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,__defineGetter__,__defineSetter__,__lookupGetter__,__lookupSetter__,__proto__,constructor
Creating external classes
Three notes about creating instances of external classes. First, Alexey said to write
fun <T> create(): T = js("{}")
but for me (with Kotlin 1.1) that turns into
function jsobject() {
}
whose return value is undefined. I think this might be a bug, because the official doc recommends the shorter form, too.
Second, you can't do this
fun Impl5(_id: String): Impl5 {
return (js("{}") as Impl5).apply {
this._id = _id
}
}
because that explicitly inserts a type-check for Impl5, which throws ReferenceError: Impl5 is not defined (in Firefox, at least). The generic function approach skips the type-check. I'm guessing that's not a bug, since Alexey recommended it, but it seems odd, so I'll ask him.
Lastly, you can mark create as inline, though you'll need to suppress a warning :-)