Any(better) approach to pass compose desktop's `onClick` through `callbackFow`? - kotlin

I was wondering if it is possible to pass onClik [handling onClick] through callbackFlow as I saw from this this post.
I was having hard time implementing since the onClick Callback was inside parameter also Button is function so wasn`t able to implement extension function
anyways I tried something like
lateinit var buttonListener :Flow<Unit>
fun <T >offers(t: T) = callbackFlow {
offer(t)
awaitClose { null }
}
CoroutineScope(IO).launch {
if(::buttonListener.itInitalized){
buttonListener.collect {
println("it => Kotlin.Unit")
}
}
}
MaterialTheme {
Button(
onClick = {
println("buttonClicked")
buttonListener = offers(Unit)
} //...
) { /** designs */}
}
which is callable only 1 times on every runtime
buttonClicked <--\
Kotlin.Unit => Kotlin.Unit <--/\__first click
buttonClicked
buttonClicked
buttonClicked
yet expecting someting like
buttonClicked
Kotlin.Unit => Kotlin.Unit
buttonClicked
Kotlin.Unit => Kotlin.Unit
buttonClicked
Kotlin.Unit => Kotlin.Unit

You can use coroutine Channel instead of Flow to receive events from outside the coroutine. Then convert it to Flow using consumeAsFlow() method.
Now the flow operators like collect can be called on this Flow.
It can receive multiple onClick events from the button composable.
var buttonListener = Channel<Unit>()
CoroutineScope(Dispatchers.IO).launch {
buttonListener.consumeAsFlow().collect {
Log.d(TAG, "onCreate: $it => Kotlin.Unit")
}
}
MaterialTheme {
Button(
onClick = {
Log.d(TAG, "onCreate: buttonClicked")
buttonListener.offer(Unit)
}
){
Text(text = "Button")
}
}

Related

Why won't my UI update while a background task is running?

I have this code that should show a counter while a background task is running:
#Composable fun startIt() {
val scope = rememberCoroutineScope()
val running = remember { mutableStateOf(false) }
Button({ scope.launch { task(running) } }) {
Text("start")
}
if (running.value) Counter()
}
#Composable private fun Counter() {
val count = remember { mutableStateOf(0) }
LaunchedEffect(Unit) {
while (true) {
delay(100.milliseconds)
count.value += 1
}
}
Text(count.toString())
}
private suspend fun task(running: MutableState<Boolean>) {
running.value = true
coroutineScope {
launch {
// some code that blocks this thread
}
}
running.value = false
}
If I understand correctly, the coroutineScope block in task should unblock the main thread, so that the LaunchedEffect in Counter can run. But that block never gets past the first delay, and never returns or gets cancelled. The counter keeps showing 0 until the task finishes.
How do I allow Compose to update the UI properly?
coroutineScope doesn't change the coroutine context, so I think you're launching a child coroutine that runs in the same thread.
The correct way to synchronously do blocking work in a coroutine without blocking the thread is by using withContext(Dispatchers.IO):
private suspend fun task(running: MutableState<Boolean>) {
running.value = true
withContext(Dispatchers.IO) {
// some code that blocks this thread
}
running.value = false
}
If the blocking work is primarily CPU-bound, it is more appropriate to use Dispatchers.Default instead, I think because it helps prevent the backing thread pool from spawning more threads than necessary for CPU work.
This was a small issue of the way count was being modified, and not of coroutines. To fix your code, the remember for count in Counter() needed to be updated to :
#OptIn(ExperimentalTime::class)
#Composable private fun Counter() {
val count = remember { mutableStateOf(0) }
LaunchedEffect(Unit) {
while (true) {
delay(Duration.milliseconds(100))
count.value += 1
}
}
Text(count.value.toString())
}
Remember can be done with delegation to remove the need of using the .value such as:
#OptIn(ExperimentalTime::class)
#Composable private fun Counter() {
var count by remember { mutableStateOf(0) }
LaunchedEffect(Unit) {
while (true) {
delay(Duration.milliseconds(100))
count += 1
}
}
Text(count.toString())
}
Compose does coroutines slightly differently than Kotlin would by default, this is a small example that shows a bit more of how Compose likes Coroutines to be done:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Compose_coroutinesTheme {
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
BaseComponent()
}
}
}
}
}
// BaseComponent holds most of the state, child components respond to its values
#Composable
fun BaseComponent() {
var isRunning by remember { mutableStateOf(false) }
val composableScope = rememberCoroutineScope()
val count = remember { mutableStateOf(0) }
Column(verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally) {
Text("Count: ${count.value}")
// Using the async context of a button click, we can toggle running off and on, as well as run our background task for incrementing the counter
ToggleCounter(isRunning) {
isRunning = !isRunning
composableScope.launch {
while (isRunning) {
delay(100L)
count.value += 1
}
}
}
}
}
// Accepting an onTap function and passing it into our button, allows us to modify state as a result of the button, without the button needing to know anything more
#Composable
fun ToggleCounter(isRunning: Boolean, onTap: () -> Unit) {
val buttonText = if (isRunning) "Stop" else "Start"
Button(onClick = onTap) {
Text(buttonText)
}
}

Calling composable funciton from mouseclickable

I need to show a cursorDropDownMenu if there was a click with the secondary button on an Item.
My code which calls the function is the following:
.mouseClickable {
if(buttons.isSecondaryPressed){
showContextmenu()
}else {
//same as in clickable
}
}
my showcontextmenu function is the following:
#Composable
fun showContextmenu(){
println("rightclick detected")
CursorDropdownMenu(expanded = true, onDismissRequest = {/*todo implement? */ }){
DropdownMenuItem({/*onclick: todo get data and forward it to render the gui tree */}){
Text("render")
}
}
}
But the compile error is that #Composable invocations can only happen from the context of a #composable function.
Some help would be very nice.
Use boolean state var to show/hide menu.
var isContextMenuVisible by remember {mutableStateOf(false)}
//...
//your button code
.mouseClickable {
if(buttons.isSecondaryPressed){
isContextMenuVisible = true
}else {
//same as in clickable
}
}
//...
if (isContextMenuVisible) {
ShowContextmenu()
}
//...
And:
#Composable
fun ShowContextmenu(){
println("rightclick detected")
CursorDropdownMenu(expanded = true, onDismissRequest = {isContextMenuVisible = false}){
DropdownMenuItem({/*onclick: todo get data and forward it to render the gui tree */}){
Text("render")
}
}
}

How to subscribe to StateFlow in kotlin-react useEffect

I'm trying to create a small counter example for kotlin-react with functionalComponent with kotlin 1.4-M2.
The example should use kotlinx.coroutines.flow. I'm struggling at collecting the values from the store in reacts useEffect hook.
Store:
object CounterModel { // Modified sample from kotlin StateFlow doc
private val _counter = MutableStateFlow(0) // private mutable state flow
val counter: StateFlow<Int> get() = _counter // publicly exposed as read-only state flow
fun inc() { _counter.value++ }
}
Component:
val counter = functionalComponent<RProps> {
val (counterState, setCounter) = useState(CounterModel.counter.value)
useEffect(listOf()) {
// This does not work
GlobalScope.launch { CounterModel.counter.collect { setCounter(it) } }
}
div {
h1 {
+"Counter: $counterState"
}
button {
attrs.onClickFunction = { CounterModel.inc() }
}
}
}
When I directly call CounterModel.counter.collect { setCounter(it) } it complains about Suspend function 'collect' should be called only from a coroutine or another suspend function.
How would you implement this useEffect hook?
And once the subscription works, how would you unsubscribe from it (use useEffectWithCleanup instead of useEffect)?
Finally found a solution. We can use onEach to do an action for every new value and then 'subscribe' with launchIn. This returns a job that can be canceled for cleanup:
object CounterStore {
private val _counter = MutableStateFlow(0)
val counter: StateFlow<Int> get() = _counter
fun inc() { _counter.value++ }
}
val welcome = functionalComponent<RProps> {
val (counter, setCounter) = useState(CounterStore.counter.value)
useEffectWithCleanup(listOf()) {
val job = CounterStore.counter.onEach { setCounter(it) }.launchIn(GlobalScope)
return#useEffectWithCleanup { job.cancel() }
}
div {
+"Counter: $counter"
}
button {
attrs.onClickFunction = { CounterStore.inc() }
+"Increment"
}
}
We can extract this StateFlow logic to a custom react hook:
fun <T> useStateFlow(flow: StateFlow<T>): T {
val (state, setState) = useState(flow.value)
useEffectWithCleanup(listOf()) {
val job = flow.onEach { setState(it) }.launchIn(GlobalScope)
return#useEffectWithCleanup { job.cancel() }
}
return state
}
And use it like this in our component:
val counter = useStateFlow(CounterStore.counter)
The complete project can be found here.
The Flow-Api is very experimental so this might not be the final solution :)
if's very important to check that the value hasn't changed,
before calling setState, otherwise the rendering happens twice
external interface ViewModelProps : RProps {
var viewModel : MyViewModel
}
val App = functionalComponent<ViewModelProps> { props ->
val model = props.viewModel
val (state, setState) = useState(model.stateFlow.value)
useEffectWithCleanup {
val job = model.stateFlow.onEach {
if (it != state) {
setState(it)
}
}.launchIn(GlobalScope)
return#useEffectWithCleanup { job.cancel() }
}
}

Can't count the value with lambda

Please help to understand why the value of clicks doesn't change. I started read "Kotlin in action" and now have more questions than answers (it's joke book is great, but I'm not)
I tried to set clicks before function, it was advice from book "kotlin in action", but the value of var clicks is always 0.
I have the following interface :
interface Button {
fun onClick(function: () -> Unit)
}
And Kotlin file
fun main() {
var clicks = 0
fun tryToClickOnButton(button: Button): Int{
button.onClick{println("UPP $clicks")}
button.onClick{clicks++}
button.onClick{println("UPP $clicks")}
return clicks
}
class SpecialButton: Button{
override fun onClick(function: () -> Unit) {
println("AAAAAA $clicks")
}
}
val button = object: Button {
override fun onClick(function: () -> Unit) {
println("BBBBB $clicks")
}
}
println(tryToClickOnButton(button))
println(tryToClickOnButton(object : Button{
override fun onClick(function: () -> Unit) {
println("CCCCCCC $clicks")
}
}))
println(tryToClickOnButton(SpecialButton()))
}
The problem is that you have created a callback that takes a function, but you never call that function.
Let's remove some code, to make the example simpler:
fun main() {
var clicks = 0
fun tryToClickOnButton(button: Button): Int {
button.onClick { println("UPP $clicks") }
button.onClick { clicks++ }
button.onClick { println("UPP $clicks") }
return clicks
}
val button = object : Button {
override fun onClick(function: () -> Unit) {
println("BBBBB $clicks")
}
}
println(tryToClickOnButton(button))
}
You can see that in your Button's onClick callback, you accept an argument called function of type "function", but you never use it. Because of that, you'll never print "UPP ${clicks}" and you'll never increment clicks value.
To solve the issue, you simply need to call function, like this:
fun main() {
var clicks = 0
fun tryToClickOnButton(button: Button): Int {
button.onClick { println("UPP $clicks") }
button.onClick { clicks++ }
button.onClick { println("UPP $clicks") }
return clicks
}
val button = object : Button {
override fun onClick(function: () -> Unit) {
println("BBBBB $clicks")
function() // this was missing
}
}
println(tryToClickOnButton(button))
}
That code prints:
BBBBB 0
UPP 0
BBBBB 0
BBBBB 1
UPP 1
1

How would I "wrap" this not-quite-"by lazy" result caching function call in idiomatic Kotlin?

I can't use "by lazy" because the callbacks require suspendCoroutine, which borks in android if it blocks the main thread, so I have to use the following "cache the result" pattern over and over. Is there a way to wrap it in a funButUseCachedResultsIfTheyAlreadyExist pattern to encapsulate the xCached object?
private var cameraDeviceCached: CameraDevice? = null
private suspend fun cameraDevice(): CameraDevice {
cameraDeviceCached?.also { return it }
return suspendCoroutine { cont: Continuation<CameraDevice> ->
... deep callbacks with cont.resume(camera) ...
}.also {
cameraDeviceCached = it
}
}
When what I'd really like to write is
private suspend fun cameraDevice(): CameraDevice = theMagicFunction { cont ->
... deep callbacks with cont.resume(camera) ...
}
You can build a generalized solution by wrapping an async call as follows:
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineStart.LAZY
class LazySuspendFun<out T>(
scope: CoroutineScope,
private val block: suspend () -> T
) {
private val deferred = scope.async(Dispatchers.Unconfined, LAZY) { block() }
suspend operator fun invoke() = deferred.await()
}
fun <T> CoroutineScope.lazySuspendFun(block: suspend () -> T) =
LazySuspendFun(this, block)
This is a simple example of how you can use it. Note that we are able to compose them so that we use a lazy-inited value as a dependency to getting another one:
val fetchToken = lazySuspendFun<String> {
suspendCoroutine { continuation ->
Thread {
info { "Fetching token" }
sleep(3000)
info { "Got token" }
continuation.resume("hodda_")
}.start()
}
}
val fetchPosts = lazySuspendFun<List<String>> {
val token = fetchToken()
suspendCoroutine { continuation ->
Thread {
info { "Fetching posts" }
sleep(3000)
info { "Got posts" }
continuation.resume(listOf("${token}post1", "${token}post2"))
}
}
}
On the calling side you must be inside some coroutine context so you can call the suspending functions:
myScope.launch {
val posts = fetchPosts()
...
}
This solution is robust enough that you can concurrently request the value several times and the initializer will run only once.
I'll write this as an answer, since it's not possible to post much code in comments.
What you're looking for is something like this:
private suspend fun cameraDevice() = theMagicFunction {
CameraDevice()
}()
suspend fun theMagicFunction(block: ()->CameraDevice): () -> CameraDevice {
var cameraDeviceCached: CameraDevice? = null
return fun(): CameraDevice {
cameraDeviceCached?.also { return it }
return suspendCoroutine { cont: Continuation<CameraDevice> ->
cont.resume(block())
}.also {
cameraDeviceCached = it
}
}
}
Unfortunately, this will not compile, since closures cannot be suspendable, and neither are local functions.
Best I can suggest, unless I miss a solution there, is to encapsulate this in a class, if this variable bothers you too much.