How to access class methods from anonymous suspend function inside constructor in kotlin? - kotlin

I want to be able to call functions from the anonymous constructor's suspend function in the following example:
data class SuspendableStep(
val condition: SuspendableCondition,
val continuation: Continuation<Unit>
)
class WaitCondition(cycles: Int) : SuspendableCondition() {
private val timer = SomeTimer(cycles)
override fun resume(): Boolean = timer.elapsed() // timer is handled somewhere else
override fun toString(): String = "WaitCondition_$timer"
}
class BasicContinuation : Continuation<Unit> {
var coroutine: Continuation<Unit>
override val context: CoroutineContext = EmptyCoroutineContext
private var nextStep: SuspendableStep? = null
constructor(task: suspend () -> Unit) {
coroutine = task.createCoroutine(completion = this)
}
override fun resumeWith(result: Result<Unit>) {
nextStep = null
result.exceptionOrNull()?.let { e -> Logger.handle("Error with plugin!", e) }
}
suspend fun wait(cycles: Int): Unit = suspendCoroutine {
check(cycles > 0) { "Wait cycles must be greater than 0." }
nextStep = SuspendableStep(WaitCondition(cycles), it)
}
}
fun main() {
BasicContinuation({
println("HELLO")
wait(1)
println("WORLD")
}).coroutine.resume(Unit)
}
There only other option I found was to override a suspend function by creating an anonymous inner class and calling another function to set the coroutine:
fun main() {
val bc = BasicContinuation() {
override suspend fun test() : Unit {
println("HELLO")
wait(1)
println("WORLD")
}
}
bc.set() // assign coroutine to suspend { test }.createCoroutine(completion = this)
bc.coroutine.resume(Unit)
}

I used CoroutineScope to extend the scope of the functions I could access:
class BasicContinuation : Continuation<Unit> {
var coroutine: Continuation<Unit>
override val context: CoroutineContext = EmptyCoroutineContext
private var nextStep: SuspendableStep? = null
constructor(task: (suspend BasicContinuation.(CoroutineScope) -> Unit)) {
coroutine = suspend { task.invoke(this, CoroutineScope(context)) }.createCoroutine(completion = this)
}
override fun resumeWith(result: Result<Unit>) {
nextStep = null
result.exceptionOrNull()?.let { e -> Logger.handle("Error with plugin!", e) }
}
suspend fun wait(cycles: Int): Unit = suspendCoroutine {
check(cycles > 0) { "Wait cycles must be greater than 0." }
nextStep = SuspendableStep(WaitCondition(cycles), it)
}
}
fun main() {
val bc = BasicContinuation({
println("Hello")
wait(1)
println("World")
})
bc.coroutine.resume(Unit) // print "Hello"
// increment timer
bc.coroutine.resume(Unit) // print "World
}

Related

How to use LifecycleScope to execute coroutine

I am discovering Kotlin and android app dev. I fail to get data from my room database (because of Cannot access database on the main thread). So I try with lifecyclescope.
The concerned code, in Fragment onViewCreated function, is :
lifecycleScope.launch {
withContext(Dispatchers.Default) {
val accountConfiguration = viewModel.get();
println("{${accountConfiguration}}")
}
}
The called function (in viewModel) is :
fun get() = viewModelScope.launch {
repository.get()
}
There is the "full" code (simplified), Entity & DAO :
#Entity
data class AccountConfiguration(
#PrimaryKey val server_address: String,
#ColumnInfo(name = "user_name") val user_name: String,
// [...]
)
#Dao
interface AccountConfigurationDao {
#Query("SELECT * FROM accountconfiguration LIMIT 1")
fun flow(): Flow<AccountConfiguration?>
#Query("SELECT * FROM accountconfiguration LIMIT 1")
suspend fun get(): AccountConfiguration?
// [...]
}
Repository :
package fr.bux.rollingdashboard
import androidx.annotation.WorkerThread
import kotlinx.coroutines.flow.Flow
class AccountConfigurationRepository(private val accountConfigurationDao: AccountConfigurationDao) {
val accountConfiguration: Flow<AccountConfiguration?> = accountConfigurationDao.flow()
// [...]
#Suppress("RedundantSuspendModifier")
#WorkerThread
suspend fun get() : AccountConfiguration? {
return accountConfigurationDao.get()
}
}
ViewModel & Factory :
class AccountConfigurationViewModel(private val repository: AccountConfigurationRepository) : ViewModel() {
val accountConfiguration: LiveData<AccountConfiguration?> = repository.accountConfiguration.asLiveData()
// [...]
fun get() = viewModelScope.launch {
repository.get()
}
// [...]
}
class AccountConfigurationViewModelFactory(private val repository: AccountConfigurationRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(AccountConfigurationViewModel::class.java)) {
#Suppress("UNCHECKED_CAST")
return AccountConfigurationViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
Fragment :
class AccountConfigurationFragment : Fragment() {
private var _binding: AccountConfigurationFragmentBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
private val viewModel: AccountConfigurationViewModel by activityViewModels {
AccountConfigurationViewModelFactory(
(activity?.application as RollingDashboardApplication).account_configuration_repository
)
}
lateinit var accountConfiguration: AccountConfiguration
// [...]
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.buttonGoBackMain.setOnClickListener {
findNavController().navigate(R.id.action_AccountConfigurationFragment_to_DashboardFragment)
}
lifecycleScope.launch {
withContext(Dispatchers.Default) {
val accountConfiguration = viewModel.get();
println("{${accountConfiguration}}")
}
}
binding.buttonSave.setOnClickListener {
save()
}
}
// [...]
}
In your current code,
lifecycleScope.launch {
withContext(Dispatchers.Default) {
val accountConfiguration = viewModel.get();
println("{${accountConfiguration}}")
}
}
viewModel.get() is not a suspend function, so it returns immediately and proceeds to the next line. It actually returns the Job created by viewModelScope.launch().
If you want your coroutine to wait for the result before continuing you should make the get() function suspend and return the AccountConfiguration?
suspend fun get(): AccountConfiguration? {
return repository.get()
}
You need not change dispatchers to Dispatchers.Default because Room itself will switch to a background thread before executing any database operation.
Right now if there is a configuration change while coroutines inside lifecyclerScope are running, everything will get cancelled and restarted.
A better way would have been to put the suspending calls inside the ViewModel and expose a LiveData/Flow to the UI.
The problem is the viewModel function :
fun get() = viewModelScope.launch {
repository.get()
}
This function must be the coroutine instead launch the coroutine itself. Correct code is :
suspend fun get(): AccountConfiguration? {
return repository.get()
}

Kotlin CoroutineScope not working - JavaFx (with TornadoFx)

Right now, I'm having a problem using Kotlin Coroutine in JavaFx (with TornadoFx). The problem is that my CoroutineScope is not working after Kotlin init block + launch extensions with some CoroutineContext is not working too.
My View abstract class.
abstract class View(
title: String? = null,
icon: Node? = null
) : tornadofx.View(title, icon), CoroutineHandler, Injectable, Cleanable {
val viewScope: CoroutineScope =
CoroutineScope(SupervisorJob() + Dispatchers.JavaFx.immediate)
open val fragmentContainer: Pane? get() = null
abstract val viewModel: ViewModel
override fun launch(start: CoroutineStart, block: suspend CoroutineScope.() -> Unit): Job =
viewScope.launch(start = start, block = block)
override fun launchDefault(start: CoroutineStart, block: suspend CoroutineScope.() -> Unit): Job =
viewScope.launch(Dispatchers.Default, start, block)
override fun launchMain(start: CoroutineStart, block: suspend CoroutineScope.() -> Unit): Job =
viewScope.launch(Dispatchers.JavaFx, start, block)
override fun launchMainImmediate(start: CoroutineStart, block: suspend CoroutineScope.() -> Unit): Job =
viewScope.launch(Dispatchers.JavaFx.immediate, start, block)
override fun launchIO(start: CoroutineStart, block: suspend CoroutineScope.() -> Unit): Job =
viewScope.launch(Dispatchers.IO, start, block)
override fun launchUnconfined(start: CoroutineStart, block: suspend CoroutineScope.() -> Unit): Job =
viewScope.launch(Dispatchers.Unconfined, start, block)
override fun <T> Flow<T>.start() = launchIn(viewScope)
open fun openFragment(fragment: Fragment) {
with(fragmentContainer!!.children) {
if (isNotEmpty()) clear()
add(fragment)
}
}
override fun clean() {
viewScope.cancel()
viewModel.clean()
}
override fun onDock() {
clean()
super.onDock()
}
My ViewModel abstract class
abstract class ViewModel : tornadofx.ViewModel(), CoroutineHandler, Cleanable {
val viewModelScope: CoroutineScope =
CoroutineScope(SupervisorJob() + Dispatchers.JavaFx.immediate)
override fun launch(start: CoroutineStart, block: suspend CoroutineScope.() -> Unit): Job =
viewModelScope.launch(start = start, block = block)
override fun launchDefault(start: CoroutineStart, block: suspend CoroutineScope.() -> Unit): Job =
viewModelScope.launch(Dispatchers.Default, start, block)
override fun launchMain(start: CoroutineStart, block: suspend CoroutineScope.() -> Unit): Job =
viewModelScope.launch(Dispatchers.JavaFx, start, block)
override fun launchMainImmediate(start: CoroutineStart, block: suspend CoroutineScope.() -> Unit): Job =
viewModelScope.launch(Dispatchers.JavaFx.immediate, start, block)
override fun launchIO(start: CoroutineStart, block: suspend CoroutineScope.() -> Unit): Job =
viewModelScope.launch(Dispatchers.IO, start, block)
override fun launchUnconfined(start: CoroutineStart, block: suspend CoroutineScope.() -> Unit): Job =
viewModelScope.launch(Dispatchers.Unconfined, start, block)
override fun <T> Flow<T>.start() = launchIn(viewModelScope)
override fun clean() {
viewModelScope.cancel()
}
}
View Implementation (LoginView). Just go to the init block and controlsInitialization method.
class LoginView : View("Login") {
override val root: VBox by fxml()
private val usernameTextField: TextField by fxid()
private val passwordField: PasswordField by fxid()
private val submitButton: Button by fxid()
private val backButton: Button by fxid()
private val resultLabel: Label by fxid()
#Inject
override lateinit var viewModel: LoginViewModel
init {
viewScope.launch { println("LAUNCH VIEW") } //Printed
launchMain { println("MAIN VIEW") } //Not Printed
launchMainImmediate { println("MAIN IMMEDIATE VIEW") } //Printed
launchDefault {
println("DEFAULT VIEW 1") //Printed
appComponent.inject(this#LoginView)
controlsInitialization()
collectorsInitialization()
println("DEFAULT VIEW") //Not Printed
}
launchIO { println("IO VIEW") } //Printed
launchUnconfined { println("UNCONFINED VIEW") } //Printed
}
private fun collectorsInitialization() {
with(viewModel) {
getUsername().onEach {
with(usernameTextField) {
if (it != null) {
invisible()
passwordField.visible()
submitButton.text = "Login"
backButton.visible()
resultLabel.invisible()
submitButton.enable()
} else {
showResultLabel("Username salah.")
}
enable()
}
}.start()
isSuccessLogin().onEach {
if (it) {
MainView().openWindow(owner = null)
close()
} else showResultLabel("Password salah.")
}.start()
getLoginFailedCount().onEach {
passwordField.clear()
if (it == 5 || it == 7 || it >= 9) {
val text = "Anda gagal login sebanyak $it, " +
"silahkan tunggu selama "
val waitTime = WaitTime(waitTimeInMinute)
var now = LocalTime.now()
var lastSecond = now.second
launchDefault {
with(waitTime) {
while (isNotOverYet) {
if (lastSecond != now.second) {
if (second == 0) {
minute--
second = 59
} else second--
viewScope.launch {
resultLabel.text = "$text $waitTime : $second"
}
lastSecond = now.second
}
now = LocalTime.now()
}
}
waitTimeInMinute += 5
submitButton.enable()
resultLabel.invisible()
}
} else {
submitButton.enable()
}
}.start()
}
}
private fun controlsInitialization() {
with(backButton) {
setOnAction {
invisible()
passwordField.apply {
invisible()
clear()
}
usernameTextField.visible()
if (!resultLabel.text.contains("Anda")) resultLabel.invisible()
}
}
submitButton.setOnAction {
submitButton.disable()
if (usernameTextField.isVisible) {
with(usernameTextField) {
disable()
println("$text 1") //Printed
launchIO { "$text 2" } //Not Printed
viewModel.checkIsUserExist(text)
}
} else {
with(passwordField) {
disable()
launchIO {
viewModel.login(text)
}
}
}
}
}
private fun showResultLabel(text: String) {
with(resultLabel) {
this.text = text
visible()
}
}
}
ViewModel implementation (LoginViewModel). Just go to the init block and checkIsUserExist method.
class LoginViewModel #Inject constructor(
private val loginUseCase: LoginUseCase
) : ViewModel() {
private val username = MutableStateFlow<String?>(null)
private val isSuccessLogin = MutableStateFlow(false)
private val loginFailedCount = MutableStateFlow(0)
var waitTimeInMinute: Int = 5
init {
viewModelScope.launch { println("LAUNCH VIEW MODEL") } //Not Printed
launchMain { println("MAIN VIEW MODEL") } //Not Printed
launchMainImmediate { println("MAIN IMMEDIATE VIEW MODEL") } //Not Printed
launchDefault { println("DEFAULT VIEW MODEL") } //Printed
launchIO { println("IO VIEW MODEL") } //Printed
launchUnconfined { println("UNCONFINED VIEW MODEL") } //Printed
}
fun checkIsUserExist(username: String) {
println("$username AAAA") //Printed
launchIO {
println(username) //Not Printed
val isExist = loginUseCase.checkIsUserExist(username)
println(username) //Not Printed
println("isExist: $isExist") //Not Printed
with(this#LoginViewModel.username) {
if (isExist) {
emit(username)
} else {
emit(null)
with(loginFailedCount) { emit(value + 1) }
}
}
}
println("$username ZZZZ") //Printed
}
fun login(password: String) = launchIO {
if (username.value == null) {
isSuccessLogin.emit(false)
with(loginFailedCount) { emit(value + 1) }
} else {
val user = loginUseCase.login(username.value!!, password)
isSuccessLogin.value = if (user != null) {
CashierApplication.build(user)
true
} else {
with(loginFailedCount) { emit(value + 1) }
false
}
}
}
fun getUsername(): StateFlow<String?> = username
fun isSuccessLogin(): StateFlow<Boolean> = isSuccessLogin
fun getLoginFailedCount(): StateFlow<Int> = loginFailedCount
My CoroutineHandler Interface
interface CoroutineHandler {
fun <T> Flow<T>.start(): Job
fun launch(
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
fun launchDefault(
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
fun launchMain(
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
fun launchMainImmediate(
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
fun launchIO(
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
fun launchUnconfined(
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
suspend fun <T> withDefault(block: suspend CoroutineScope.() -> T): T =
withContext(Dispatchers.Default, block)
suspend fun <T> withMain(block: suspend CoroutineScope.() -> T): T =
withContext(Dispatchers.Main, block)
suspend fun <T> withMainImmediate(block: suspend CoroutineScope.() -> T): T =
withContext(Dispatchers.Main.immediate, block)
suspend fun <T> withIO(block: suspend CoroutineScope.() -> T): T =
withContext(Dispatchers.IO, block)
suspend fun <T> withUnconfined(block: suspend CoroutineScope.() -> T): T =
withContext(Dispatchers.Unconfined, block)
}
Actually, I was override the wrong method. The method I need to override is onUndock not onDock.

Is it possible to pass an argument into a sequence function?

I'm looking for a way to pass an argument into a Kotlin sequence function similar to how it works in JS:
function *gen () {
console.log(yield) // prints 1
console.log(yield) // prints 2
}
const it = gen()
it.next() // first iteration will execute the first yield and pause
it.next(1) // we pass 1 to the first yield which will be printed
it.next(2) // we pass 2 to the second yield which will be printed
Something like this in Kotlin:
fun main() {
val it = gen().iterator()
// Iterator#next() doesn't expect an argument
it.next(1)
it.next(2)
}
fun gen() = sequence {
println(yield(null)) // Would print 1
println(yield(null)) // Would print 2
}
Kotlin Sequences do not support passing arguments to each yield, but you have at least 2 ways to implement needed behaviour:
Using actors:
class NextQuery<A, T>(val arg: A, val next: CompletableDeferred<T> = CompletableDeferred())
fun test() = runBlocking {
val actor = GlobalScope.actor<NextQuery<String, Int>> {
for (nextQuery in channel) {
nextQuery.next.complete(nextQuery.arg.length)
}
}
val query1 = NextQuery<String, Int>("12345")
actor.send(query1)
println(query1.next.await())
val query2 = NextQuery<String, Int>("1234")
actor.send(query2)
println(query2.next.await())
}
Using channels:
class ArgSequenceScope<out A, in T>(
private val argChannel: ReceiveChannel<A>,
private val nextChannel: SendChannel<T>
) {
suspend fun yield(next: T) {
nextChannel.send(next)
}
suspend fun arg(): A = argChannel.receive()
}
class ArgSequence<in A, out T>(
private val argChannel: SendChannel<A>,
private val nextChannel: ReceiveChannel<T>
) {
suspend fun next(arg: A): T {
argChannel.send(arg)
return nextChannel.receive()
}
}
fun <A, T> sequenceWithArg(block: suspend ArgSequenceScope<A, T>.() -> Unit): ArgSequence<A, T> {
val argChannel = Channel<A>()
val nextChannel = Channel<T>()
val argSequenceScope = ArgSequenceScope(argChannel, nextChannel)
GlobalScope.launch {
argSequenceScope.block()
argChannel.close()
nextChannel.close()
}
return ArgSequence(argChannel, nextChannel)
}
fun test() {
val sequence = sequenceWithArg<String, Int> {
yield(arg().length)
yield(arg().length)
}
runBlocking {
println(sequence.next("12345"))
println(sequence.next("1234"))
}
}

Kotlin Coroutine Unit Test Flow collection with viewModelScope

I want to test a method of my ViewModel that collects a Flow. Inside the collector a LiveData object is mutated, which I want to check in the end. This is roughly how the setup looks:
//Outside viewmodel
val f = flow { emit("Test") }.flowOn(Dispatchers.IO)
//Inside viewmodel
val liveData = MutableLiveData<String>()
fun action() {
viewModelScope.launch { privateAction() }
}
suspend fun privateAction() {
f.collect {
liveData.value = it
}
}
When I now call the action() method in my unit test, the test finishes before the flow is collected. This is how the test might look:
#Test
fun example() = runBlockingTest {
viewModel.action()
assertEquals(viewModel.liveData.value, "Test")
}
I am using the TestCoroutineDispatcher via this Junit5 extension and also the instant executor extension for LiveData:
class TestCoroutineDispatcherExtension : BeforeEachCallback, AfterEachCallback, ParameterResolver {
#SuppressLint("NewApi") // Only used in unit tests
override fun supportsParameter(parameterContext: ParameterContext?, extensionContext: ExtensionContext?): Boolean {
return parameterContext?.parameter?.type === testDispatcher.javaClass
}
override fun resolveParameter(parameterContext: ParameterContext?, extensionContext: ExtensionContext?): Any {
return testDispatcher
}
private val testDispatcher = TestCoroutineDispatcher()
override fun beforeEach(context: ExtensionContext?) {
Dispatchers.setMain(testDispatcher)
}
override fun afterEach(context: ExtensionContext?) {
Dispatchers.resetMain()
testDispatcher.cleanupTestCoroutines()
}
}
class InstantExecutorExtension : BeforeEachCallback, AfterEachCallback {
override fun beforeEach(context: ExtensionContext?) {
ArchTaskExecutor.getInstance()
.setDelegate(object : TaskExecutor() {
override fun executeOnDiskIO(runnable: Runnable) = runnable.run()
override fun postToMainThread(runnable: Runnable) = runnable.run()
override fun isMainThread(): Boolean = true
})
}
override fun afterEach(context: ExtensionContext?) {
ArchTaskExecutor.getInstance().setDelegate(null)
}
}
You can try either,
fun action() = viewModelScope.launch { privateAction() }
suspend fun privateAction() {
f.collect {
liveData.value = it
}
}
#Test
fun example() = runBlockingTest {
viewModel.action().join()
assertEquals(viewModel.liveData.value, "Test")
}
or
fun action() {
viewModelScope.launch { privateAction()
}
suspend fun privateAction() {
f.collect {
liveData.value = it
}
}
#Test
fun example() = runBlockingTest {
viewModel.action()
viewModel.viewModelScope.coroutineContext[Job]!!.join()
assertEquals(viewModel.liveData.value, "Test")
}
You could also try this,
suspend fun <T> LiveData<T>.awaitValue(): T? {
return suspendCoroutine { cont ->
val observer = object : Observer<T> {
override fun onChanged(t: T?) {
removeObserver(this)
cont.resume(t)
}
}
observeForever(observer)
}
}
#Test
fun example() = runBlockingTest {
viewModel.action()
assertEquals(viewModel.liveData.awaitValue(), "Test")
}
So what I ended up doing is just passing the Dispatcher to the viewmodel constructor:
class MyViewModel(..., private val dispatcher = Dispatchers.Main)
and then using it like this:
viewModelScope.launch(dispatcher) {}
So now I can override this when I instantiate the ViewModel in my test with a TestCoroutineDispatcher and then advance the time, use testCoroutineDispatcher.runBlockingTest {}, etc.

Single method to launch a coroutine

I have a StorageRepository that talks with RoomDB and also shared prefs. I want this communication to happen through a single method on a IO thread. I have done this until now -
class StorageRepository(private val coroutineDispatcher: CoroutineContext = Dispatchers.Main
) : CoroutineScope {
private val job = Job()
override val coroutineContext: CoroutineContext
get() = job + coroutineDispatcher
override fun storeUserDetails(userDetails: UserDetails) {
roomDB.store(userDetails)
}
override fun storeTimeStamp(timeStamp: String) {
sharedPrefs.store(timeStamp)
}
private fun executeAllOpsOnIOThread() = launch {
withContext(Dispatchers.IO) {
//Any DB write, read operations to be done here
}
}
}
My question is how can I pass roomDB.store(userDetails) and sharedPrefs.store(timeStamp) to executeAllOpsOnIOThread() so that all DB communication happens on IO thread?
Hmm.. Maybe I misunderstand you but it seems you can just pass a block of code as lambda function like this:
class StorageRepository(
private val coroutineDispatcher: CoroutineContext = Dispatchers.Main
) : CoroutineScope {
private val job = Job()
override val coroutineContext: CoroutineContext
get() = job + coroutineDispatcher
override fun storeUserDetails(userDetails: UserDetails) = executeAllOpsOnIOThread {
roomDB.store(userDetails)
}
override fun storeTimeStamp(timeStamp: String) = executeAllOpsOnIOThread {
sharedPrefs.store(timeStamp)
}
private fun executeAllOpsOnIOThread(block: () -> Unit) = launch {
withContext(Dispatchers.IO) {
block()
}
}
//async get
fun getTimestamp(): Deferred<String> = getOnIOThread { sharedPrefs.getTime() }
private fun <T> getOnIOThread(block: () -> T):Deferred<T> = async {
withContext(Dispatchers.IO) {
block()
}
}
}