Is this a good strategy for doing some work in vie...
# android
m
Is this a good strategy for doing some work in viewmodel and mid-way requesting a user-selection (using some dialog) before continuing to complete work? Note: using an
Event
wrapper since dialog is persisted through configuration changes when using navigation component. The view layer would be responsible for `complete`ing the deferrable when the user clicks an item.
Copy code
val itemSelectorLiveData = MutableLiveData<Event<Pair<List<String>, CompletableDeferred<String>>>>()

suspend fun doSomethingFunButWorthwhile(): Boolean {
    val items: List<String> = workOutWhichItemsToChooseFrom()
    val deferredItem = CompletableDeferred<String>()
    itemSelectorLiveData.postValue(Event(Pair(items, deferredItem)))
    // wait for user to select item
    val selectedItem = deferredItem.await()
    return doSomethingWorthwhile(selectedItem)
}
There is one nasty part to this, where the selector dialog needs to peek into the event’s
CompletableDeferred
(obtained via a shared viewmodel) in order to complete it. Alternatively I could use the new (alpha) fragment result API but then still the calling fragment needs to grab a reference to that
CompletableDeferred
so doesn’t offer much in the way of advantage.
a
I'm not sure why persisting the dialog calls for an event wrapper, I think that's going to give you problems. (It always does because event wrappers in LiveData are a code smell anyway 🙂 )
That Pair also probably wants to be a class instead that exposes an API for completing the operation and letting
doSomethingFun
proceed, and since it looks to be a one-time operation you can probably use
suspendCancellableCoroutine
and store the continuation instead of a
CompletableDeferred
that would replace your call to
await
wrap that call in a try/finally that nulls out the LiveData when it resumes, and perhaps do the whole thing in a
Mutex.withLock {}
block to keep more than one call to
doSomethingFun
from stepping on each other's toes - it'll form an orderly queue for concurrent callers
the whole thing might look something like this:
Copy code
class DialogPrompt(
    val options: List<String>,
    private val continuation: Continuation<String>
) {
    fun select(item: String) {
        require(item in options) { "$item is not a valid selection from $options" }
        continuation.resume(item)
    }
}

class FooModel {
    private val promptMutex = Mutex()

    // Must hold promptMutex to modify
    private val _dialogPrompt = MutableLiveData<DialogPrompt?>()
    val dialogPrompt: LiveData<DialogPrompt?> get() = _dialogPrompt

    suspend fun doSomethingFunButWorthwhile(): Boolean {
        val items = workOutWhichItemsToChooseFrom()
        val dialogResult = prompt(items)
        return doSomethingWorthwhile(dialogResult)
    }

    private suspend fun prompt(options: List<String>): String = promptMutex.withLock {
        withContext(Dispatchers.Main) {
            try {
                suspendCancellableCoroutine { continuation ->
                    _dialogPrompt.value = DialogPrompt(options, continuation)
                }
            } finally {
                _dialogPrompt.value = null
            }
        }
    }
}
👍 1
you can also do things like add a method to
DialogPrompt
that resumes the continuation with a
CancellationException
if the user cancels the prompt
m
Ooh, that’s very nice, thanks very much Adam! handling prompt cancellaton is essential because of the mutex. Is it preferable to continue with CancellatiionException rather than using null?
Is it possible that
select
could be called multiple times (e.g. after first call and before the livedata has a chance to be set to null)?
Regarding the
Event
wrapper, I guess without it, I’d have to check whether the dialog has already been added to the fragment manager
a
Suspending code always has to deal with
CancellationException
as a teardown, it's much better to lean into it.
👍 1
But, up to you. It makes calling code cleaner, imo.
m
The livedata is observed by a fragment which will then
navigate
to the dialog fragment. At this point, is it recommended to not use any arguments (
navArgs
) but rather let the dialog fragment grab the livedata directly from, say, a
navGraphViewModels
? In the case where, the
String
arg is actually something, say, non-parcelable, this seems like the only reasonable way, but even for
String
it seems in some way cleaner because then the fragment is working entirely on
DialogPrompt
which ties the input and output together.
Regarding
CancellationException
, since the continuation is a
CancellableContinuation
better to just call
cancel()
in dialog cancel listeners I guess.
Regarding the mutex lock, the dialogs will queue up which is often not what you want. At the moment I make a quick check beforehand to see if the mutex is locked (and so break out if it is). This seems a little hacky but probably good enough.