¿Qué son las corrutinas o coroutines? ¿Para qué se pueden usar estas coroutines en Android, o en Kotlin en general? ¿Cómo se pueden usar las coroutines en Android? En este post voy intentar explicar un poco todo esto y en sucesivos post ampliaremos la información ya que esto puede dar para bastante. 

Vamos a por lo principal, ¿qué son las coroutines de Kotlin?

Las coroutines están entre nosotros desde la versión 1.3 de Kotlin y básicamente es una funcionalidad del lenguaje que nos permite gestionar la concurrencia y trabajar con los procesos asíncronos y con los hilos. Todo lo que hacemos mediante el uso de la clase Thread y relacionadas con esta,  que nos permite ir creando diferentes hilos para trabajos asíncronos y/o en background, por ejemplo, se pueden realizar ahora con las coroutines y además de forma mucho mas fácil, eficiente con mucho menos código y de forma mucho más legible y entendible.

Esto no quiere decir que no existan los hilos a partir de ahora en nuestras aplicaciones, únicamente que estos no los vamos a gestionar nosotros, serán las coroutines las que se preocupen de crearlos cuando sea necesario.

Pilares básicos de las coroutines

Las coroutines se apoyan en tres pilares básicos, o tres conceptos, que tenemos que tener muy claros:

  • Scope
  • Dispatchers
  • Funciones suspendidas

Scope en coroutines

Cuando hablamos de Scope en Coroutines nos estamos refiriendo al ámbito en el que van a vivir las coroutines de nuestro flujo. Una coroutine solo puede ser creada dentro de un Scope, y ese es el ámbito en el que puede estar viva. Si el ámbito deja de existir porque , por ejemplo, la clase donde se creó deja de estar en memoria, todas las coroutines que se hubiesen creado dentro de ese ámbito se cancelarán de forma automática.

En Android, si por ejemplo creamos un Scope Global a la aplicación, toda coroutine que se crea dentro de ese ámbito podrá existir durante todo el ciclo de vida de la aplicación; si creamos un Scope a nivel de un ViewModel, solo existirán mientras el ViewModel exista.

Dispatchers

Cuando creamos una coroutine hemos dicho que esta debe de tener un Scope donde poder vivir, pero también tenemos que decir o configurar en qué hilo queremos que se ejecute. Realmente no decidimos en qué hilo, porque eso lo gestiona la coroutine internamente, pero sí le decimos en qué «tipo» de hilo queremos que esta coroutine trabaje.

Existen tres tipos predefinidos de Dispatchers:

  • Main: Es el hilo de la interfaz. Todos sabemos que este hilo es sagrado y no se debe de obstaculizar por ningún proceso tedioso que se vaya demorar en exceso. Todo lo que sea llamadas a red, guardado en discos, gestión de ficheros, bases de datos, ordenaciones, filtros, etc… nunca jamás debe de realizarse estando en este hilo.
  • IO: Este es el hilo destinado a trabajar con operaciones de red, disco, ficheros, y similares
  • Default: Es el hilo donde cualquier proceso que requiera de un alto uso de CPU o trabajo intensivo debería de hacerse en él.

Funciones suspendidas

Las coroutines nos sirven para ejecutar acciones dentro de nuestro código, es decir son funciones que se ejecutan y realizan cierto trabajo, y este trabajo es el que queremos que se realice en cierto hilo (Dispatchers) y dentro de un cierto ámbito(Scope). Pues estas funciones para que el compilador sepa con son coroutines y que debe de tratarlas como tal deben de ser funciones suspendidas y deben de estar precedidas de la palabra suspend.

¿Qué es esto de suspendida? 

Una función suspendida es aquella que inicia su ejecución y el sistema es capaz de suspender su ejecución hasta que no recibe un dato del que depende o hasta que el dato no está listo. Una vez que recibe ese dato, la función vuelve de la suspensión y continúa por dónde iba.

Esto puede parecer un poco lioso, pero con un ejemplo en pseudo código creo que se entiende mejor.

fun hagoLlamadaAPI() {

	Scope.launch{
		val name = fetchName() 
	}
	
	//sigue ejecutando el código por aquí antes de recibir respuesta de la API
}

suspend fun fetchName(): String{
	val name = api.getName.execute()  
    //Esta función se suspende hasta que no reciba la respuesta de la API
    // y una vez obtenida retornará por esta linea y devolverá el nombre
	return name
}

Creo que con el ejemplo anterior queda algo más claro. La función fetchName será suspendida hasta que no reciba la respuesta de la API, pero el código dentro de la función hagoLlamadaAPI seguirá su curso fuera del ámbito de la coroutine.

Una función suspendida únicamente puede ser llamada por otra función suspendida o  por los métodos launch o async de los Scope que son generalmente desde donde se inician.

Normalmente se utilizará launch cuando no se requiere devolver un valor desde este método y async cuando es el caso contrario.

Una coroutine se inicia configurando un Dispatcher en el que se va a ejecutar, pero este Dispatcher se puede cambiar a lo largo del flujo de la coroutine. Podemos iniciarlo en el Main y llegado un punto decirle que se cambie al IO o al Default. Esto lo hacemos con withContext(Dispatcher)

Como usar las coroutines en Android

Vista la teoría básica de las coroutines vamos ver como se aplica esto en situaciones dentro de un proyecto Android. 

Para tener acceso a las coroutines tenemos que agregar la librería a nuestro proyecto:

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0")
}

En nuestro ejemplo vamos a tener un repositorio donde existirá un método que simulará una llamada a alguna API y tras un tiempo de retardo retornará una cadena de texto.

class DataRepository {

    suspend fun fetchData(): String {
        return withContext(Dispatchers.IO) {
            delay(4000L)
            "Dato obtenido"
        }
    }

}

Este repositorio será llamado desde un ViewModel que al obtener este dato lo pondrá en un LiveData que la Actvity está escuchando para cuando se modifique su valor obtenerlo y pintarlo.

class MainActivity : AppCompatActivity() {

    private val viewModel: DataViewModel by viewModels {
       DataViewModelFactory(DataRepository())
    }


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)


        findViewById<Button>(R.id.button).setOnClickListener {
            viewModel.fetchData()
        }

        val titleView = findViewById<TextView>(R.id.title)
        viewModel.data.observe(this, Observer { result ->
            titleView.text = result
        })

    }
}

Los ViewModels tienen una función de extensión , viewmodelScope, que se ejecuta con un dispatcher Main y que es el que vamos a utilizar para iniciar la coroutine. Con este Scope también estamos definiendo que si el ViewModel desaparece de la memoria, esta coroutine se cancelará de forma automática aunque o haya recibido la respuesta aún.

Esto quiere decir que el dato que obtengamos desde el repositorio estará en el hilo Main para así poder utilizarse en la vista.

class DataViewModel(private val repo: DataRepository) : ViewModel() {

    val data : MutableLiveData<String> by lazy {
        MutableLiveData<String>()
    }

    fun fetchData() {
        viewModelScope.launch {
            val repoData = repo.fetchData()
            data.postValue(repoData)
        }
    }
}


class DataViewModelFactory(private val repo: DataRepository) : ViewModelProvider.Factory {

    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(DataViewModel::class.java)) {
            return DataViewModel(repo = repo) as T
        }
        throw IllegalArgumentException()
    }

}

En el repositorio hemos cambiado el hilo, el Dispatcher al de IO, para así ver como las coroutines se encargan ellas de ir cambiando los datos de hilo siendo para nosotros totalmente indoloro y transparente.

De esto modo, a pesar de que en el repo nosotros hemos cambiado de hilo al IO, una vez llega al viewmodel, la coroutine vuelve a cambiar de hilo y lo envía en el hilo Main, ya que fue el hilo con el que iniciamos la susodicha coroutine gracias a la función de extension viewModelScope.

En siguientes post veremos con algo más de profundidad todo esto, ademas veremos el async/await, también como crear nuestro propio Scope y como cancelar por nuestra cuenta las coroutines creadas.