This library contains several utility functions and classes to use Kotlin suspend functions in Android.
To use this library in your Android project, add the following to settings.gradle (if not already done):
dependencyResolutionManagement {
//...
repositories {
//...
maven { url 'https://jitpack.io' }
}
}And add the following to app/build.gradle:
dependencies {
//...
implementation 'com.github.gustavlindberg99:androidsuspendutils:1.8.0'
}This library requires at least API version 16.
This library provides extension functions to various Android/Kotlin classes making it easier to work with Kotlin suspend functions in Android:
-
suspend fun AlertDialog.Builder.showAsync(positiveButtonText: String, negativeButtonText: String): BooleanShows the given alert dialog with a positive and negative button, and suspends the coroutine until the alert dialog is closed. It returns
trueif the positive button was clicked, and false otherwise.Example:
import com.github.gustavlindberg99.androidsuspendutils.showAsync val result = AlertDialog.Builder(this) .setTitle("Your title") .setMessage("Your message") .showAsync("Yes", "No") // You can use strings or resource IDs here if (result) { println("Yes was clicked") } else { println("No was clicked") }
-
suspend fun AlertDialog.Builder.showAsync(positiveButtonTextId: Int, negativeButtonTextId: Int): BooleanSimilar to the above but accepts resource IDs instead of strings.
-
fun View.setOnClickListenerAsync(lifecycleOwner: LifecycleOwner? = null, listener: suspend (View) -> Unit)Similar to
View.setOnClickListener, but allows the lambda parameter to be a suspend function, and starts a new coroutine for it when the view is clicked. The optionallifecycleOwnerparameter allows specifying a lifecycle owner for the coroutine. If this parameter is null, the activity of the view will be used as lifecycle owner. -
fun View.setOnLongClickListenerAsync(lifecycleOwner: LifecycleOwner? = null, listener: suspend (View) -> Unit)Similar to
View.setOnLongClickListener, but allows the lambda parameter to be a suspend function. SeesetOnClickListenerAsyncfor more details.Unlike
setOnLongClickListenerwhich takes a listener returning a boolean,setOnLongClickListenerAsynctakes a listener returningUnit, and behaves as if the listener passed tosetOnLongClickListenerreturnedtrue. This is because since the listener issuspend, it's not always possible to immediately determine its return value. -
fun TextView.doAfterTextChangedAsync(lifecycleOwner: LifecycleOwner? = null, listener: suspend (View) -> Unit)Similar to
TextView.doAfterTextChangedAsync, but allows the lambda parameter to be a suspend function. SeesetOnClickListenerAsyncfor more details. -
fun Spinner.setOnItemSelectedAsync(lifecycleOwner: LifecycleOwner? = null, onNothingSelected: suspend (AdapterView<*>) -> Unit = {}, onItemSelected: suspend (AdapterView<*>, View, Int, Long) -> Unit)Similar to
Spinner.setOnItemSelectedListener, but with lambdas instead of interfaces, and allows the lambda parameter to be a suspend function. The only mandatory parameter isonItemSelected, it can be called with only that parameter as follows:val spinner: Spinner = findViewById(R.id.something) spinner.setOnItemSelectedAsync { parent, view, position, id -> //... }
If you need to call it with
onNothingSelectedas well, use parentheses and named parameters:val spinner: Spinner = findViewById(R.id.something) view.setOnItemSelectedAsync( onItemSelected = { parent, view, position, id -> //... }, onNothingSelected = { // The `parent` parameter can be accessed either with `it` or by naming it explicitly } )
The
lifecycleOwnerparameter can be specified first within parentheses just like for the other methods.If you specify both lambdas, it's highly recommended to use named parameters even if
lifecycleOwneris specified, sinceonNothingSelectedis first in the parameter list, which is unintuitive (but necessary to be able to specifyonItemSelectedalone without parentheses). -
suspend fun <T> Iterable<T>.concurrentForEach(context: LifecycleOwner, limit: Int = Int.MAX_VALUE, action: suspend (T) -> Unit)Runs the given action on each element of the collection concurrently. Returns when all actions have finished.
The
limitparameter allows to specify the maximum number of actions to run concurrently. Can be useful for large collections to avoid out of memory errors.If the lambda throws an exception for any of the elements,
concurrentForEachwill finish running for the remaining elements, then propagate that exception to its caller.Example:
import com.github.gustavlindberg99.androidsuspendutils.concurrentForEach val list = listOf(1, 2, 3) list.concurrentForEach(context) { withContext(Dispatchers.IO) { // You can do an HTTP request here, in that case // each request will be run simultaneously. } }
-
suspend fun <T, R : Comparable<R>> Iterable<T>.sortedByAsync(callback: suspend (T) -> R?): List<T>Similar to
sortedBy, but allows the callback to be a suspend function. -
fun CoroutineScope.launch(context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend (CoroutineScope) -> Unit): JobSimilar to
kotlinx.coroutines.launch, but preservesthisfrom the outer scope rather than rebinding to theCoroutineScopeobject. If you need theCoroutineScopein the lambda, access it withitrather thanthis.This can be very useful in activities since
thisoften needs to be passed around as context. This function allows you to write simplythisrather thanthis@SomeActivityin those cases. For example:import com.github.gustavlindberg99.androidsuspendutils.launch // Instead of kotlinx.coroutines.launch lifecycleScope.launch { // No need to write this@SomeActivity Toast.makeText(this, "Hello World", Toast.LENGTH_SHORT).show() }
-
fun <T> CoroutineScope.async(context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend (CoroutineScope) -> T): Deferred<T>Similar to
kotlinx.coroutines.async, but preservesthisfrom the outer scope rather than rebinding to theCoroutineScopeobject. If you need theCoroutineScopein the lambda, access it withitrather thanthis.For an example, see
launchabove. -
suspend fun <T> withContext(context: CoroutineContext, block: suspend (CoroutineScope) -> T): TSimilar to
kotlinx.coroutines.withContext, but preservesthisfrom the outer scope rather than rebinding to theCoroutineScopeobject. If you need theCoroutineScopein the lambda, access it withitrather thanthis.For an example, see
launchabove. -
fun <T> flow(block: suspend (FlowCollector<T>) -> Unit): Flow<T>Similar to
kotlinx.coroutines.flow, but preservesthisfrom the outer scope rather than rebinding to theFlowCollectorobject. If you need theFlowCollectorin the lambda, access it withitrather thanthis.Example:
import com.github.gustavlindberg99.androidsuspendutils.flow // Instead of kotlinx.coroutines.flow flow { // No need to write this@SomeActivity Toast.makeText(this, "Hello World", Toast.LENGTH_SHORT).show() // Use `it.emit` to emit values it.emit(1) it.emit(2) }
If you don't like this and just want to write
emit(...)instead (even if it means writingthis@SomeActivitywhen you need the activity), you can use the originalkotlinx.coroutines.flowsimply by importingkotlinx.coroutines.flowinstead ofcom.github.gustavlindberg99.androidsuspendutils.flow. Of course this applies to the otherthis-preserving overloads as well. -
fun <T> channelFlow(block: suspend (ProducerScope<T>) -> Unit): Flow<T>Similar to
kotlinx.coroutines.channelFlow, but preservesthisfrom the outer scope rather than rebinding to theProducerScopeobject. If you need theProducerScopein the lambda, access it withitrather thanthis.For an example, see
flowabove (but replaceit.emitwithit.send). -
fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend (CoroutineScope) -> T): TSimilar to
kotlinx.coroutines.runBlocking, but preservesthisfrom the outer scope rather than rebinding to theCoroutineScopeobject. If you need theCoroutineScopein the lambda, access it withitrather thanthis.For an example, see
launchabove. -
suspend fun <T : Closeable?, R> T.useWithContext(context: CoroutineContext, block: suspend (T) -> R): RExecutes the given block of code with the given context, and closes the input stream after the block is executed. Useful for input streams that read from the network.
stream.useWithContext(context){...}is syntactic sugar forstream.use{withContext(context){...}}.
SuspendableLauncher is a wrapper class for ActivityResultLauncher that allows to wait for an activity result in a suspend function.
-
SuspendableLauncher<I, O>(context: ComponentActivity, contract: ActivityResultContract<I, O>)Constructor. Must be constructed in an activity's constructor, as it calls
registerForActivityResultinternally. -
suspend fun launch(input: I): OLaunches the activity and waits for the result. If another activity has already been launched with this
SuspendableLauncherobject and hasn't finished yet, queues the new request and launches it when the old one finishes.
Example:
import com.github.gustavlindberg99.androidsuspendutils.SuspendableLauncher
class MainActivity : AppCompatActivity() {
private val _launcher = SuspendableLauncher(
this,
ActivityResultContracts.StartActivityForResult()
)
private suspend fun launchSecondaryActivity() {
// SecondaryActivity is the activity that should be launched
val intent = Intent(this, SecondaryActivity::class.java)
intent.putExtra("input", "Data to send to SecondaryActivity")
// Launches SecondaryActivity and waits for it to finish.
val result = _launcher.launch(intent)
// Assuming SecondaryActivity has called `setResult(RESULT_OK, resultIntent)`,
// where `resultIntent` is an intent containing an `"output"` extra.
val output = result.data?.getStringExtra("output")
println("Data send from SecondaryActivity: $output")
}
}