https://kotlinlang.org logo
#splitties
Title
# splitties
i

ispbox

08/27/2019, 7:24 AM
@louiscad I found one split with coroutines (https://github.com/LouisCAD/Splitties/tree/master/modules/views-coroutines), but looks like it is not relevant for my use case. I just want to handle one and only one click on button and start long-lasting process in background. There are several solutions on how to lock consequent clicks on that button until process finishes. One is suggested by Roman Elizarov (https://github.com/Kotlin/kotlinx.coroutines/blob/master/ui/coroutines-guide-ui.md):
Copy code
fun View.onClick(action: suspend (View) -> Unit) {
    // launch one actor
    val eventActor = GlobalScope.actor<View>(Dispatchers.Main) {
        for (event in channel) action(event)
    }
    // install a listener to activate this actor
    setOnClickListener { 
        eventActor.offer(it)
    }
}
Another cool guy here suggests even better dsl solution: https://www.hellsoft.se/even-smarter-async-with-coroutine-actors/ But my dream was to have some handy solution like
View.awaitOneClick
(maybe,
View.processOneClick
?) that incorporates waiting for coroutine to finish and then unblocks button. My current solution is pretty simple - just disable button before long operation, and enable it after its completion. How do you handle the similar tasks, and what is the real necessity for
View.awaitOneClick
?
l

louiscad

08/27/2019, 7:26 AM
I avoid
GlobalScope
in UI. I just make the button disabled at first, and
awaitOneClick
handles the rest (enabling it while waiting for the click). Then, I wait for the processing, then call
awaitOneClick
again. You can see it in the sample in the
develop
branch.
@ispbox You can use it as a combination with a loop
i

ispbox

08/27/2019, 8:40 PM
@louiscad Finally I took a look at Splitties sample app. The good point is that you have sense of humor 🙂 But I'm really not sure that your approach with loops looks good. Maybe I don't understand some idea, and that is for sure only demo app, but what I see is an attempt to combine all responsibilities of the app in one place. That's not good for big apps.
And from perspective of my initial question what I see is that you are awaiting for clicks. I'm not so big expert, but it looks reasonable for me that this is already implemented in android platform - you don't have to organize your own event loops, but should react on events. And handling of event (like button click) should be localized (in your case) in relevant Activity (because Activity acts like view and presenter at the same time).
In my previous project I used
actor
approach from coroutine's author, and dismissed consequent clicks by offering messages via actor`s channel. But it looks not so good for me now, because you have to create these scaffolds everywhere or have one centralized service for that purpose and inject it here and there. I was thinking about silver bullet, but looks like there is no one 🙂 So will try with some helper for button disabling after click and enabling after long-running task.
Here is a sample code for that old approach:
Copy code
override suspend fun doAction(v: View) {
        when (v.id) {
            R.id.userRegisterButton -> listener?.onUserRegister()
            R.id.accountSignInButton -> listener?.onAccountSignIn()
            R.id.skipRegistrationButton -> {
                val progressContext = ProgressContext(
                        action = { listener?.onSkipRegistrationAction() == true },
                        postAction = { actionResult ->
                            if (actionResult) {
                                listener?.onSkipRegistrationPostAction()
                            }
                        })
                progressService.execute(progressContext)
            }
        }
    }
where ProgressService was implemented based on the following interface:
Copy code
/**
 * Interface for progress service without android platform specifics.
 */
interface ProgressService {
    /**
     * Executes action in background thread and shows progress.
     */
    suspend fun execute(progressContext: ProgressContext)

    /**
     * Pauses execution of post action.
     * Post action usually touches UI that can be not available at the moment.
     */
    fun pausePostAction()

    /**
     * Resumes execution of post action.
     */
    fun resumePostAction()
}
l

louiscad

08/27/2019, 9:49 PM
@ispbox The approach used in Splitties sample on develop branch scales well to "large apps". It's what I've been using for months. The
View
implementation is the
Ui
implementation, and its contract is the implemented
Ui
sub-interface. The
Activity
is the controller/presenter in that case, although you can easily extract it away as a (suspending) function.
i

ispbox

08/28/2019, 4:30 AM
@louiscad, it is just my personal feeling, and as I've said earlier, I'm not so big expert. But can you explain what is the reason/preference of doing `loop`<-
awaitForClick
instead of
click
->
handleClick
? The latter works well in most cases, and if you want any long-lasting operation - you just launch new coroutine on desired dispatcher (and dismiss it on finish)? OK, for annoying popups it is OK to launch one loop and show popup every second, but what's the reason of waiting for simple navigation to other activity?
l

louiscad

08/28/2019, 6:10 AM
@ispbox For simple cases, the loop with
awaitOneClick()
is not really better than using
onClick { ... }
, but if you want to keep the button disabled while something is happening, then it becomes useful.
i

ispbox

08/28/2019, 4:40 PM
@louiscad for me this approach looks more reasonable: https://discuss.kotlinlang.org/t/coroutines-ui-event-race-condition/3326
8 Views