Kotlin CoroutineScope not working - JavaFx (with TornadoFx) - kotlin

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.

Related

How to implement database to BroadcastReceiver (Alarm)

I'm making a app, that will notificate when time of task expiring. For creating notifications I'm using BroadcastReceiver.
There is my code of class AlarmReceiver
class AlarmReceiver: BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
Log.e("yeah", "yeah")
val dao = TasksDatabase.getInstance(context = context!!.applicationContext).dao
//val repository = TasksRepositoryImpl(dao)
var list: List<Task> = listOf()
runBlocking {
launch {
Log.e("sad", "dsa")
list = dao.getTasks().flattenToList()
Log.e("list", list.toString())
}
}
val builder = NotificationCompat.Builder(context, "DoneApp")
.setSmallIcon(R.drawable.icon)
.setContentTitle("Done App")
.setContentText("")
.setAutoCancel(true)
.setDefaults(NotificationCompat.DEFAULT_ALL)
.setPriority(NotificationCompat.PRIORITY_HIGH)
val notificationManager = NotificationManagerCompat.from(context)
if(list.isNotEmpty()){
for(i in list){
Log.e("string", i.toString())
}
} else {
Log.e("log---", "---------------------------------")
}
notificationManager.notify(123, builder.build())
}
suspend fun <T> Flow<List<T>>.flattenToList() = flatMapConcat { it.asFlow() }.toList()
}
But it stops on list = dao.getTasks().flattenToList()
There is TasksDao
#Dao
interface TasksDAO {
#Insert
suspend fun insertTask(task: Task)
#Query("UPDATE task_table SET checked = :checked WHERE id = :id")
suspend fun checkTask(id: Int, checked: Boolean)
#Query("UPDATE task_table SET text = :text WHERE id = :id")
suspend fun textTask(id: Int, text: String)
#Query("DELETE FROM task_table WHERE id = :id")
suspend fun deleteTask(id: Int)
#Query("SELECT * FROM task_table")
fun getTasks(): Flow<List<Task>>
}

How to select and display a live table with kotlin, room, mvvm?

In my First fragment, I create the team name (corresponding to my team_table).
Then, I move on to the fragment for adding player names. I would like to display directly and only the list of players corresponding to the team. It seems to work when I want to update a player's name and come back to that player list. Otherwise, it shows me the list of all the players of all the teams. How can I do, please?
Here's my code:
class UserViewModel(application: Application): AndroidViewModel(application) {
val readAllDataTeam: LiveData<List<Team>>?
val readAllDataUser: LiveData<List<User>>?
val readAllDataEval: LiveData<List<Eval>>?
val readAllDataUserWithEval: LiveData<List<UserWithEval>>?
val readAllDataTeamWithUser: LiveData<List<TeamWithUser>>?
private val repositoryTeam: UserRepository
private val repositoryUser: UserRepository
private val repositoryEval: UserRepository
private val repositoryUserWithEval: UserRepository
private val repositoryTeamWithUser: UserRepository
init {
val teamDao = UserDatabase.getDatabase(application)?.userDao()
repositoryTeam = UserRepository(teamDao!!)
readAllDataTeam = repositoryTeam.readAllDataTeam
val userDao = UserDatabase.getDatabase(application)?.userDao()
repositoryUser = UserRepository(userDao!!)
readAllDataUser = repositoryUser.readAllDataUser
val evalDao = UserDatabase.getDatabase(application)?.userDao()
repositoryEval = UserRepository(evalDao!!)
readAllDataEval = repositoryEval.readAllDataEval
val userEvalDao = UserDatabase.getDatabase(application)?.userDao()
repositoryUserWithEval = UserRepository(userEvalDao!!)
readAllDataUserWithEval= repositoryUserWithEval.readAllDataUserEval
val teamUserDao = UserDatabase.getDatabase(application)?.userDao()
repositoryTeamWithUser = UserRepository(teamUserDao!!)
readAllDataTeamWithUser = repositoryTeamWithUser.readAllDataTeamWithUser
}
//Team
fun addTeam(team: Team) {
viewModelScope.launch(Dispatchers.IO) {
repositoryTeam.addTeam(team)
}
}
fun updateTeam(team: Team) {
viewModelScope.launch(Dispatchers.IO) {
repositoryTeam.updateTeam(team)
}
}
fun deleteTeam(team: Team) {
viewModelScope.launch(Dispatchers.IO) {
repositoryTeam.deleteTeam(team)
}
}
fun deleteAllTeams() {
viewModelScope.launch(Dispatchers.IO) {
repositoryTeam.deleteAllTeams()
}
}
//Eval
fun addEval(eval: Eval) {
viewModelScope.launch(Dispatchers.IO) {
repositoryEval.addEval(eval)
}
}
fun updateEval(eval: Eval) {
viewModelScope.launch(Dispatchers.IO) {
repositoryEval.updateEval(eval)
}
}
fun deleteEval(eval: Eval) {
viewModelScope.launch(Dispatchers.IO) {
repositoryEval.deleteEval(eval)
}
}
fun deleteAllEvals() {
viewModelScope.launch(Dispatchers.IO) {
repositoryEval.deleteAllEvals()
}
}
//User
fun addUser(user: User) {
viewModelScope.launch(Dispatchers.IO) {
repositoryUser.addUser(user)
}
}
fun updateUser(user: User) {
viewModelScope.launch(Dispatchers.IO) {
repositoryUser.updateUser(user)
}
}
fun deleteUser(user: User) {
viewModelScope.launch(Dispatchers.IO) {
repositoryUser.deleteUser(user)
}
}
fun deleteAllUsers() {
viewModelScope.launch(Dispatchers.IO) {
repositoryUser.deleteAllUsers()
}
}
fun searchDatabase(searchQuery: String): LiveData<List<User>>? {
return repositoryUser.searchDatabase(searchQuery)
}
fun searchDatabaseTeam(searchQuery: String): LiveData<List<Team>>? {
return repositoryUser.searchDatabaseTeam(searchQuery)
}
fun getTeam(searchTeam: String): LiveData<List<User>>? {
return repositoryUser.getTeam(searchTeam)
}
}
class UserRepository:
class UserRepository (private val userDao: UserDao) {
val readAllDataUser: LiveData<List<User>>? = userDao.getUserData()
val readAllDataEval: LiveData<List<Eval>>? = userDao.getEvalData()
val readAllDataTeam: LiveData<List<Team>>? = userDao.getTeamData()
val readAllDataUserEval: LiveData<List<UserWithEval>>? = userDao.getAllUserWithEval()
val readAllDataTeamWithUser: LiveData<List<TeamWithUser>>? = userDao.getAllTeamWithUser()
//User
suspend fun addUser(user: User) {
userDao.addUser(user)
}
suspend fun updateUser(user: User) {
userDao.updateUser(user)
}
suspend fun deleteUser(user: User) {
userDao.deleteUser(user)
}
suspend fun deleteAllUsers() {
userDao.deleteAllUsers()
}
//Eval
suspend fun addEval(eval: Eval) {
userDao.addEval(eval)
}
suspend fun updateEval(eval: Eval) {
userDao.updateEval(eval)
}
suspend fun deleteEval(eval: Eval) {
userDao.deleteEval(eval)
}
suspend fun deleteAllEvals() {
userDao.deleteAllEvals()
}
//Eval
suspend fun addTeam(team: Team) {
userDao.addTeam(team)
}
suspend fun updateTeam(team: Team) {
userDao.updateTeam(team)
}
suspend fun deleteTeam(team: Team) {
userDao.deleteTeam(team)
}
suspend fun deleteAllTeams() {
userDao.deleteAllTeams()
}
fun searchDatabase(searchQuery: String): LiveData<List<User>>? {
return userDao.searchDatabase(searchQuery)
}
fun searchDatabaseTeam(searchQuery: String): LiveData<List<Team>>? {
return userDao.searchDatabaseTeam(searchQuery)
}
fun getTeam(searchTeam: String): LiveData<List<User>>? {
return userDao.getTeam(searchTeam)
}
interface UserDao:
#Dao
interface UserDao {
#Query("SELECT * FROM user_table ORDER BY id ASC")
fun getUserData(): LiveData<List<User>>?
#Insert(onConflict = OnConflictStrategy.IGNORE)
fun addUser(user: User)
#Update
fun insertEval(eval: Eval)
#Update
fun updateUser(user: User)
#Delete
fun deleteUser(user: User)
#Query("DELETE FROM user_table")
fun deleteAllUsers()
#Query("SELECT * FROM user_table WHERE id=:id")
fun getOneUser(id: Int): User
#Transaction
#Query("SELECT * FROM user_table JOIN eval_table on user_table.id = eval_table.idEval ")
fun getAllUserWithEval(): LiveData<List<UserWithEval>>
#Query("SELECT * FROM user_table WHERE firstName LIKE :searchQuery OR lastName LIKE :searchQuery OR nbTeam LIKE :searchQuery OR nameTeam LIKE :searchQuery")
fun searchDatabase(searchQuery: String): LiveData<List<User>>?
#Query("SELECT * FROM team_table WHERE nameTeam LIKE :searchQuery")
fun searchDatabaseTeam(searchQuery: String): LiveData<List<Team>>?
#Transaction
#Query("SELECT * FROM user_table WHERE nameTeam LIKE :searchTeam")
fun getTeam(searchTeam: String): LiveData<List<User>>?
class ListFragment:
class ListFragment : Fragment(), SearchView.OnQueryTextListener {
//Pour accéder au fab
private var _binding: FragmentListBinding? = null
private val binding get() = _binding!!
private lateinit var mUserViewModel: UserViewModel
private val args by navArgs<ListFragmentArgs>()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
_binding = FragmentListBinding.inflate(inflater, container,false)
val view = binding.root
//Recyclerview pour affichier le custom row et les données
val adapter = ListAdapter()
val recyclerView = view.findViewById<RecyclerView>(R.id.recyclerview)
recyclerView?.adapter = adapter
recyclerView?.layoutManager = LinearLayoutManager(requireContext())
val searchTeam = args.currentTeam.nameTeam
binding.tvNameTeam.text = searchTeam
// UserViewModel pour afficher les données
mUserViewModel = ViewModelProvider(this).get(UserViewModel::class.java)
mUserViewModel.getTeam(searchTeam)?.observe(viewLifecycleOwner, Observer {
userT -> adapter.setData(userT)
})
/*
mUserViewModel.getTeam(args.currentTeam.nameTeam)
?.observe(viewLifecycleOwner, Observer { userT ->
adapter.setData(userT)
})
*/
binding.floatingActionButton.setOnClickListener{
findNavController().navigate(R.id.action_listFragment_to_addFragment)
}
// Add menu
setHasOptionsMenu(true)
return view
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.main_menu, menu)
inflater.inflate(R.menu.delete_menu, menu)
val search = menu?.findItem(R.id.menu_search)
val searchView = search?.actionView as? SearchView
searchView?.isSubmitButtonEnabled = true
searchView?.setOnQueryTextListener(this)
return
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.menu_delete){
deleteAllUsers()
}
return super.onOptionsItemSelected(item)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
private fun deleteAllUsers() {
val builder = AlertDialog.Builder(requireContext())
builder.setPositiveButton("Yes"){ _, _ ->
mUserViewModel.deleteAllUsers()
Toast.makeText(
requireContext(),
"Suppression complète réussie",
Toast.LENGTH_SHORT).show()
}
builder.setNegativeButton("No"){_, _ -> }
builder.setTitle("Tout Supprimer?")
builder.setMessage("Êtes-vous sûr de vouloir supprimer l'ensemble des données?")
builder.create().show()
}
override fun onQueryTextSubmit(query: String?): Boolean {
return true
}
override fun onQueryTextChange(query: String?): Boolean {
if(query != null){
searchDatabase(query)
}
return true
}
private fun searchDatabase(query: String) {
val searchQuery = "%$query%"
val adapter = ListAdapter()
val recyclerView = view?.findViewById<RecyclerView>(R.id.recyclerview)
recyclerView?.adapter = adapter
recyclerView?.layoutManager = LinearLayoutManager(requireContext())
mUserViewModel.searchDatabase(searchQuery)?.observe(this, { userT->
userT.let {
adapter.setData(it)
}
})
}
}
class ListAdapter:
class ListAdapter : RecyclerView.Adapter<ListAdapter.MyViewHolder>() {
var userTList = emptyList<User>()
class MyViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) { }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
return MyViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.custom_row, parent, false))
}
override fun getItemCount(): Int {
return userTList.size
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val currentItemUT = userTList[position]
holder.itemView.findViewById<TextView>(R.id.id_txt).text = currentItemUT.id.toString()
holder.itemView.findViewById<TextView>(R.id.firstName_txt).text = currentItemUT.firstName
holder.itemView.findViewById<TextView>(R.id.lastName_txt).text = currentItemUT.lastName
holder.itemView.findViewById<TextView>(R.id.numeroTeam_txt).text = currentItemUT.nbTeam.toString()
holder.itemView.findViewById<View>(R.id.rowLayout).setOnClickListener {
val action = ListFragmentDirections.actionListFragmentToUpdateFragment(currentItemUT)
holder.itemView.findNavController().navigate(action)
}
holder.itemView.findViewById<Button>(R.id.btn_start_eval).setOnClickListener {
val action = ListFragmentDirections.actionListFragmentToEvalFragment(currentItemUT)
holder.itemView.findNavController().navigate(action)
}
}
fun setData(user: List<User>){
this.userTList = user
notifyDataSetChanged()
}
}

How to access class methods from anonymous suspend function inside constructor in 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
}

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()
}
}
}