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.
Related
I have a data structure which has members that are not thread safe and the caller needs to lock the resource for reading and writing as appropriate. Here's a minimal code sample:
class ExampleResource : LockableProjectItem {
override val readWriteLock: ReadWriteLock = ReentrantReadWriteLock()
#RequiresReadLock
val nonThreadSafeMember: String = ""
}
interface LockableProjectItem {
val readWriteLock: ReadWriteLock
}
fun <T : LockableProjectItem, Out> T.readLock(block: T.() -> Out): Out {
try {
readWriteLock.readLock().lock()
return block(this)
} finally {
readWriteLock.readLock().unlock()
}
}
fun <T : LockableProjectItem, Out> T.writeLock(block: T.() -> Out): Out {
try {
readWriteLock.writeLock().lock()
return block(this)
} finally {
readWriteLock.writeLock().unlock()
}
}
annotation class RequiresReadLock
A call ExampleResource.nonThreadSafeMember might then look like this:
val resource = ExampleResource()
val readResult = resource.readLock { nonThreadSafeMember }
To make sure that the caller is aware that the resource needs to be locked, I would like the IDE to issue a warning for any members that are annotated with #RequiresReadLock and are not surrounded with a readLock block. Is there any way to do this in IntelliJ without writing a custom plugin for the IDE?
I think this is sort of a hack, but using context receivers might work. I don't think they are intended to be used in this way though.
You can declare a dummy object to act as the context receiver, and add that as a context receiver to the property:
object ReadLock
class ExampleResource : LockableProjectItem {
override val readWriteLock: ReadWriteLock = ReentrantReadWriteLock()
// properties with context receivers cannot have a backing field, so we need to explicitly declare this
private val nonThreadSafeMemberField: String = ""
context(ReadLock)
val nonThreadSafeMember: String
get() = nonThreadSafeMemberField
}
Then in readLock, you pass the object:
fun <T : LockableProjectItem, Out> T.readLock(block: context(ReadLock) T.() -> Out): Out {
try {
readWriteLock.readLock().lock()
return block(ReadLock, this)
} finally {
readWriteLock.readLock().unlock()
}
}
Notes:
This will give you an error if you try to access nonThreadSafeMember without the context receiver:
val resource = ExampleResource()
val readResult = resource.nonThreadSafeMember //error
You can still access nonThreadSafeMember without acquiring a read lock by doing e.g.
with(ReadLock) { // with(ReadLock) doesn't acquire the lock, just gets the context receiver
resource.nonThreadSafeMember // no error
}
But it's way harder to accidentally write something like this, which I think is what you are trying to prevent.
If you call another function inside readLock, and you want to access nonThreadSafeMember inside that function, you should mark that function with context(ReadLock) too. e.g.
fun main() {
val resource = ExampleResource()
val readResult = resource.readLock {
foo(this)
}
}
context(ReadLock)
fun foo(x: ExampleResource) {
x.nonThreadSafeMember
}
The context receiver is propagated through.
I'm not super sure what I'm doing here so go easy on me:
I'm making a wordle clone and the word that is to be guessed is stored as a string in a pre-populated room database which I am trying to retrieve to my ViewModel and currently getting:
"StandaloneCoroutine{Active}#933049a"
instead of the actual data.
I have tried using LiveData which only returned null which as far as I'm aware is because it was not observed.
Switched to coroutines which seemed to make more sense if my UI doesn't need the data anyway.
I ended up with this so far:
DAO:
#Dao
interface WordListDao {
#Query("SELECT word FROM wordlist WHERE used = 0 ORDER BY id DESC LIMIT 1")
suspend fun readWord(): String
// tried multiple versions here only string can be converted from Job
// #Query("SELECT * FROM wordlist WHERE used = 0 ORDER BY id DESC LIMIT 1")
// fun readWord(): LiveData<WordList>
// #Query("SELECT word FROM wordlist WHERE used = 0 ORDER BY id DESC LIMIT 1")
// fun readWord(): WordList
}
repository:
class WordRepository(private val wordListDao: WordListDao) {
//val readWordData: String = wordListDao.readWord()
suspend fun readWord(): String {
return wordListDao.readWord()
}
}
model:
#Entity(tableName = "wordlist")
data class WordList(
#PrimaryKey(autoGenerate = true)
val id: Int,
val word: String,
var used: Boolean
)
VM:
class HomeViewModel(application: Application) : ViewModel() {
private val repository: WordRepository
private var word: String
init {
val wordDb = WordListDatabase.getDatabase(application)
val wordDao = wordDb.wordlistDao()
repository = WordRepository(wordDao)
word = viewModelScope.launch {
repository.readWord()
}.toString()
Log.d("TAG", ": $word") // does nothing?
}
println(word) // StandaloneCoroutine{Active}#933049a
}
This is the only way that I have managed to not get the result of:
Cannot access database on the main thread
There is a better way to do this, I just can't figure it out.
You can access the return value of repository.readWord() only inside the launch block.
viewModelScope.launch {
val word = repository.readWord()
Log.d("TAG", ": $word") // Here you will get the correct word
}
If you need to update you UI when this word is fetched from database, you need to use an observable data holder like a LiveData or StateFlow.
class HomeViewModel(application: Application) : ViewModel() {
private val repository: WordRepository
private val _wordFlow = MutableStateFlow("") // A mutable version for use inside ViewModel
val wordFlow = _word.asStateFlow() // An immutable version for outsiders to read this state
init {
val wordDb = WordListDatabase.getDatabase(application)
val wordDao = wordDb.wordlistDao()
repository = WordRepository(wordDao)
viewModelScope.launch {
_wordFlow.value = repository.readWord()
}
}
}
You can collect this Flow in your UI layer,
someCoroutineScope {
viewModel.wordFlow.collect { word ->
// Update UI using this word
}
}
Edit: Since you don't need the word immediately, you can just save the word in a simple global variable for future use, easy.
class HomeViewModel(application: Application) : ViewModel() {
private lateinit var repository: WordRepository
private lateinit var word: String
init {
val wordDb = WordListDatabase.getDatabase(application)
val wordDao = wordDb.wordlistDao()
repository = WordRepository(wordDao)
viewModelScope.launch {
word = repository.readWord()
}
// word is not available here, but you also don't need it here
}
// This is the function which is called when user types a word and presses enter
fun submitGuess(userGuess: String) {
// You can access the `word` here and compare it with `userGuess`
}
}
The database operation will only take a few milliseconds to complete so you can be sure that by the time you actually need that original word, it will have been fetched and stored in the word variable.
(Now that I'm at a computer I can write a bit more.)
The problems with your current code:
You cannot safely read from the database on the main thread synchronously. That's why the suspend keyword would be used in your DAO/repository. Which means, there is no way you can have a non-nullable word property in your ViewModel class that is initialized in an init block.
Coroutines are asychronous. When you call launch, it is queuing up the coroutine to start its work, but the launch function returns a Job, not the result of the coroutine, and your code beneath the launch call continues on the same thread. The code inside the launch call is sent off to the coroutines system to be run and suspend calls will in most cases, as in this case, be switching to background threads back and forth. So when you call toString() on the Job, you are just getting a String representation of the coroutine Job itself, not the result of its work.
Since the coroutine does its work asynchronously, when you try to log the result underneath the launch block, you are logging it before the coroutine has even had a chance to fetch the value yet. So even if you had assigned the result of the coroutine to some String variable, it would still be null by the time you are logging it.
For your database word to be usable outside a coroutine, you need to put it in something like a LiveData or SharedFlow so that other places in code can subscribe to it and do something with the value when it arrives.
SharedFlow is a pretty big topic to learn, so I'll just use LiveData for the below samples.
One way to create a LiveData using your suspend function to retrieve the word is to use the liveData builder function, which returns a LiveData that uses a coroutine under the hood to get the value to publish via the LiveData:
class HomeViewModel(application: Application) : ViewModel() {
private val repository: WordRepository = WordListDatabase.getDatabase(application)
.wordDb.wordlistDao()
.let(::WordRepository)
private val word: LiveData<String> = liveData {
repository.readWord()
}
val someLiveDataForUi: LiveData<Something> = Transformations.map(word) { word ->
// Do something with word and return result. The UI code can
// observe this live data to get the result when it becomes ready.
}
}
To do this in a way that is more similar to your code (just to help with understanding, since this is less concise), you can create a MutableLiveData and publish to the LiveData from your coroutine.
class HomeViewModel(application: Application) : ViewModel() {
private val repository: WordRepository
private val word = MutableLiveData<String>()
init {
val wordDb = WordListDatabase.getDatabase(application)
val wordDao = wordDb.wordlistDao()
repository = WordRepository(wordDao)
viewModelScope.launch {
word.value = repository.readWord()
}
}
val someLiveDataForUi: LiveData<Something> = Transformations.map(word) { word ->
// Do something with word and return result. The UI code can
// observe this live data to get the result when it becomes ready.
}
}
If you're not ready to dive into coroutines yet, you can define your DAO to return a LiveData instead of suspending. It will start reading the item from the database and publish it through the live data once it's ready.
#Dao
interface WordListDao {
#Query("SELECT word FROM wordlist WHERE used = 0 ORDER BY id DESC LIMIT 1")
fun readWord(): LiveData<String>
}
class HomeViewModel(application: Application) : ViewModel() {
private val repository: WordRepository = WordListDatabase.getDatabase(application)
.wordDb.wordlistDao()
.let(::WordRepository)
private val word: LiveData<String> = repository.readWord()
//...
}
The return value is as expected, because launch does always return a Job object representing the background process.
I do not know how you want to use the String for, but all operations which should be done after receiving the String must be moved inside the Coroutine or in a function which is called from the Coroutine.
viewModelScope.launch {
val word = repository.readWord()
// do stuff with word
// switch to MainThread if needed
launch(Dispatchers.Main){}
}
I use MVVM and have a list of data elements in a database that is mapped through a DAO and repository to ViewModel functions.
Now, my problem is rather banal; I just want to use the data in fragment variables, but I get a type mismatch.
The MVVM introduces a bit of code, and for completeness of context I'll run through it, but I'll strip it to the essentials:
The data elements are of a data class, "Objects":
#Entity(tableName = "objects")
data class Objects(
#ColumnInfo(name = "object_name")
var objectName: String
) {
#PrimaryKey(autoGenerate = true)
var id: Int? = null
}
In ObjectsDao.kt:
#Dao
interface ObjectsDao {
#Query("SELECT * FROM objects")
fun getObjects(): LiveData<List<Objects>>
}
My database:
#Database(
entities = [Objects::class],
version = 1
)
abstract class ObjectsDatabase: RoomDatabase() {
abstract fun getObjectsDao(): ObjectsDao
companion object {
// create database
}
}
In ObjectsRepository.kt:
class ObjectsRepository (private val db: ObjectsDatabase) {
fun getObjects() = db.getObjectsDao().getObjects()
}
In ObjectsViewModel.kt:
class ObjectsViewModel(private val repository: ObjectsRepository): ViewModel() {
fun getObjects() = repository.getObjects()
}
In ObjectsFragment.kt:
class ObjectsFragment : Fragment(), KodeinAware {
private lateinit var viewModel: ObjectsViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(this, factory).get(ObjectsViewModel::class.java)
// I use the objects in a recyclerview; rvObjectList
rvObjectList.layoutManager = GridLayoutManager(context, gridColumns)
val adapter = ObjectsAdapter(listOf(), viewModel)
// And I use an observer to keep the recyclerview updated
viewModel.getObjects.observe(viewLifecycleOwner, {
adapter.objects = it
adapter.notifyDataSetChanged()
})
}
}
The adapter:
class ObjectsAdapter(var objects: List<Objects>,
private val viewModel: ObjectsViewModel):
RecyclerView.Adapter<ObjectsAdapter.ObjectsViewHolder>() {
// Just a recyclerview adapter
}
Now, all the above works fine - but my problem is that I don't want to use the observer to populate the recyclerview; in the database I store some objects, but there are more objects that I don't want to store.
So, I try to do this instead (in the ObjectsFragment):
var otherObjects: List<Objects>
// ...
if (condition) {
adapter.objects = viewModel.getObjects()
} else {
adapter.objects = otherObjects
}
adapter.notifyDataSetChanged()
And, finally, my problem; I get type mismatch for the true condition assignment:
Type mismatch: inferred type is LiveData<List> but List was expected
I am unable to get my head around this. Isn't this pretty much what is happening in the observer? I know about backing properties, such as explained here, but I don't know how to do that when my data is not defined in the ViewModel.
We need something to switch data source. We pass switching data source event to viewModel.
mySwitch.setOnCheckedChangeListener { _, isChecked ->
viewModel.switchDataSource(isChecked)
}
In viewModel we handle switching data source
(To use switchMap include implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0")
class ObjectsViewModel(private val repository: ObjectsRepository) : ViewModel() {
// Best practice is to keep your data in viewModel. And it is useful for us in this case too.
private val otherObjects = listOf<Objects>()
private val _loadDataFromDataBase = MutableLiveData<Boolean>()
// In case your repository returns liveData of favorite list
// from dataBase replace MutableLiveData(otherObjects) with repository.getFavorite()
fun getObjects() = _loadDataFromDataBase.switchMap {
if (it) repository.getObjects() else MutableLiveData(otherObjects)
}
fun switchDataSource(fromDataBase: Boolean) {
_loadDataFromDataBase.value = fromDataBase
}
}
In activity/fragment observe getObjects()
viewModel.getObjects.observe(viewLifecycleOwner, {
adapter.objects = it
adapter.notifyDataSetChanged()
})
You can do something like this:
var displayDataFromDatabase = true // Choose whatever default fits your use-case
var databaseList = emptyList<Objects>() // List we get from database
val otherList = // The other list that you want to show
toggleSwitch.setOnCheckedChangeListener { _, isChecked ->
displayDataFromDatabase = isChecked // Or the negation of this
// Update adapter to use databaseList or otherList depending upon "isChecked"
}
viewModel.getObjects.observe(viewLifecycleOwner) { list ->
databaseList = list
if(displayDataFromDatabase)
// Update adapter to use this databaseList
}
I'm pretty new to kotlin and I've been reading through the language docs to start picking it up.
I learn much better when I type out the examples, so I wanted to make a little example running codebase so I could follow along with the examples.
This is fine for writing each of examples in the main.kt file on it's own, running it, then blowing it away, but I'd like to create an example class for each section, create a list of the classes in main, and then foreach over them.
I created an interface which has a declaration for a common member function for running the examples:
interface ExampleCodeInterface {
/**
* Run the examples for the current Example class
*/
fun runExamples()
}
And defined an example class:
class CollectionExamples : ExampleCodeInterface{
fun listExample() {
val systemUsers: MutableList<Int> = mutableListOf(1,2,3)
val sudoers: List<Int> = systemUsers
fun addSudoer(newUser: Int) {
systemUsers.add(newUser)
}
fun getSysSudoers(): List<Int> {
return sudoers
}
addSudoer(4)
println("Total sudoers: ${getSysSudoers().size}")
getSysSudoers().forEach {
i -> println("Some useful info on user $i")
}
}
override fun runExamples() {
listExample()
}
}
The thing I'm not sure on is how to properly run this from main.kt
I know the class works because when I create a new instance and fire the method it works, but I can't quite figure out how to properly create a list of class constructors so that I can have a list of classes that all extend the ExampleCodeInterface that I can forEach through and fire the method:
fun main(args: Array<String>) {
val exampleClassList: List<ExampleCodeInterface> = listOf<ExampleCodeInterface>(CollectionExamples)
exampleClassList.forEach {
val exampleSet = it()
exampleSet.runExamples()
}
// val collectionExamples = CollectionExamples()
// collectionExamples.runExamples()
}
I've been trying to piece together the logic from the docs, but I think there are some details that I don't know yet.
Any help is appreciated!!
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 :-)