marzo 28, 2024

BitCuco

¡Hola Mundo!

Retrofit Android: Ejemplo de solicitud http en Kotlin

retrofit

Un ejemplo de patrón Observable lo tenemos en la solicitud http con Retrofit Android, Reactivex y Dagger en Kotlin. Retrofit es un cliente HTTP seguro para Android y Java. ReactiveX es un patrón de Observadores (Observers) para leer los datos recibidos en forma asíncrona y enviarlos a los métodos implementados.

La librería de Jetpack está pensada para usarse en el modelo MVVM (Model, View, ViewModel) usando el Observer en el ViewModel para obtener los datos. La implementación de retrofit en Android puede parecer monstruosa al principio, sin embargo conociendo las bases del modelo MVVM, se muestra el siguiente tutorial para facilitar su implementación.

Para facilitar la implementación, utilizamos Dagger. Dagger es un framework inyector de dependencias para Java y Android, genera dependencias en forma automática después de ser configurado y puede ser usado también con Kotlin.

Uso de Retrofit para solicitudes http en Android

Este tutorial sobre cómo hacer una solicitud http con Retrofit, Reactivex y Dagger en Kotlin para Android, contiene los elementos necesarios para integrar un servicio al ViewModel para un proyecto robusto con MVVM, no obstante para proyectos sencillos o para introducirse en el tema, es suficiente prestar atención a la parte de la creación del Modelo (Model), la obtención de la data desde un endpoint (invocando a una url) y la creación de un Observable en conjunto con el ViewModel.

Adicionalmente se omiten detalles de la importación de las clases propias del usuario, definidas en este tutorial, asumiendo que el usuario es libre de elegir sus nombres de clases y por lo tanto importarlos a su propio criterio.

Definir el Modelo (Model) previo al uso de Retrofit Android

retrofit android

Primer paso es definir el modelo en donde Retrofit va a recoger la información del servidor. El model se construye pensando en la estructura del JSON del cual obtendremos los datos. Para efectos de un ejemplo se construye un ejemplo muy sencillo de Model usando un data class de la siguiente forma:

data class UserModel (
        val idUser: Int = 0,
        val nameUser: String = "",
        val emailUser: String = "",
        val isActive: Boolean = false
)

En este ejemplo se construye un Model para una lista que contiene la información de email de los usuarios y se asignan valores por defecto. Este archivo es el Model y en el ejemplo se llama User.kt.

Importar Bibliotecas de ReactiveX y Retrofit Android

En app\build.gradle agregamos la implementación de las bibliotecas (librerías) que Retrofit requiere para ser utilizado en Android.

// rx
implementation "io.reactivex.rxjava2:rxandroid:2.0.2"
implementation "io.reactivex.rxjava2:rxjava:2.1.7"

// retrofit, adapters
implementation "com.squareup.retrofit2:retrofit:2.3.0"

A partir de este punto, se diferenciarán los ambientes de desarrollo, si se va a disponer de un solo ambiente de desarrollo, podemos omitir este paso, y procedemos a crear el Servicio para los endpoints (ApiService).

Colocar una variante de configuración

Es una buena práctica crear diferentes ambientes de desarrollo con el objetivo de reducir la cantidad de errores o bugs en el lado productivo y así reducir riesgos en casos de errores en el servidor, cambios inesperados y nuevas implementaciones en el API o bien a nivel aplicación.

Para diferenciar las apis invocadas para diferentes ambientes (desarrollo, quality y release), se hace en app\build.gradle:

def USER_URL = "USER_URL"
android {
...
project.android.applicationVariants.all { variant ->
    variant.productFlavors.each { flavor ->

    def flavorData = rootProject.ext[flavor.name]
    
    ...

    setVariantBuildConfigField(variant, flavorData.userHostApi, USER_URL, STRING)
    
    ...
    }
}

Y después agregamos la variante en variants.gradle

ext {
    modules = [
    ...
              userHostApi:
                       [debug  : "https://dev.bitcu.co/user/",
                       quality: "https://quality.bitcu.co/user/",
                       release: "https://api.bitcu.co/user/"],
    ...
    ]
}

Para facilitar la inyección de dependencias de Retrofit, hacemos uso del framework Dagger, del cual vamos a crear tres módulos: UserApiService, RepositoryModule y ViewModelFactoryModule a través del uso de una sola instancia (Singleton) para usarlo con Dagger.

Crear el Servicio para los endpoints (ApiService)

Los endpoints son las urls de las APIs de las cuáles vamos a recolectar los datos desde el servidor. Necesitamos enviar la petición dentro de una interface, la cual invoca una función que devuelve un Observable tal como en los siguientes ejemplos (UserApiService.kt)

Debemos importar Observable y cliente Http

import io.reactivex.Observable
import java.util.*
import retrofit2.http.*  

Ejemplos de interface

interface UserApiService {
    ...
    @GET("api/getUser")
    fun getUsers() : Observable>
    ...
    //Otros GET, POST, etc.
}

En la interfaz anterior se invocan las apis respectivas según el ambiente de desarrollo en el que nos encontremos, para colocar el resultado en una lista de UserModel al obtener los resultados de la API, si omitimos el paso de colocar una variable de configuración (crear diferentes ambientes de desarrollo), solo debemos escribir la url completa dentro de la interface.

Este mismo procedimiento funciona para cualquier método de enviar los datos al servidor, es decir GET, POST, PATCH, PUT, etc.

Para indicar ruta usamos @Path y para indicar parámetros de búsqueda usamos @Query, por ejemplo si queremos obtener información de la url https://api.bitcu.co/user/api/getUser/{idUser}?isActive=true, en donde userId es el identificador de un usuario y isActive es un parámetro enviado al API, la interface quedaría así (Obtenemos la información para un usuario activo)

import io.reactivex.Observable
import java.util.*
import retrofit2.http.*

interface UserApiService {
    ...
    @GET("api/getUser/{idUser}")
    fun getUserId(
        @Path("idUser") userId: Int,
        @Query("isActive") isActive: Boolean
    ) : Observable
    ...
    //Otros GET, POST, etc.
}

Creación del Repository

El repository o repertorio es el lugar en donde vamos a concentrar los resultados obtenidos del API. Vamos a crear UserRepository.kt, el cual nos va a funcionar para devolver el Observable después da haber obtenido la data del API en UserApiService.

import retrofit2.Call
import java.util.*
import javax.inject.Inject
import io.reactivex.Observable

class UserRepository @Inject constructor(
        private val userApiService : UserApiService
) {
        fun getUsersList(): Observable> = userApiService.getUsers()
        /**
         * Manejar error, opcional 
        .onErrorResumeNext(ErrorHandling>(UserApiError::class.java))
         */
}

Crear un Singleton para el módulo UserApiService

Para preparar la inyección de dependencias, creamos un cliente de Retrofit, el cliente debe ser invocado a partir de una única instancia o Singleton. Aquí lo llamamos Module.kt

import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
import java.io.File
import java.util.concurrent.TimeUnit
import javax.inject.Singleton

@Module
class ApiRestModule(
        private val ctx: Context,
        private val session: Session) {
    @Provides
    @Singleton
    fun provideUserApiService(): UserApiService =
            provideRetrofitClient()
                    .baseUrl(BuildConfig.USER_URL)
                    .build()
                    .create(UserApiService::class.java)
}

Crear un Inyector para RepositoryModule

Una vez cofigurado Retrofit para Android, ahora es momento de crear un singleton para inyectar dependencias usando Dagger. Dagger va a generar código en java para inyectar todas las dependencias necesarias para usarlas dentro de nuestro Factory. Este archivo lo llamaremos Injector.kt.

Si su objetivo no requiere la inyección de dependencias, o bien el uso de Dagger, le sugerimos omitir esta sección, así como la inyección de las dependencias (usando los Singletons).

import android.app.Application
import dagger.Component
import javax.inject.Singleton

object Injector {
    @Volatile
    lateinit var mApp: Application
    lateinit var session: Session

    /**
     * Singleton reference to the [DaggerAppComponent] for the app.
     */
    val component: AppComponent by lazy {
        DaggerAppComponent
                .builder()
                .apiRestModule(ApiRestModule(mApp, session))
                .repositoryModule(RepositoryModule(CoreDatabase.getInstance(mApp)))
                .build()
    }

    /**
     * Initializes this application component.
     * @param app The app component to start this Injector object.
     */
    fun initialize(app: Application,
                   ses: Session) {
        mApp = app
        session = ses
    }

    /**
     * Uses reflection to invoke an provide function with the given
     * T parameter as the return value.
     *
     * The function name is:
     * provide + Class name
     */
    @Suppress("UNCHECKED_CAST")
    inline fun  provide(): T {
        // Build the name of the provider function to find. Ej. provideString
        val functionName = "provide${T::class.java.simpleName}"
        // Attempt to execute the function using the built name.
        return try {
            component.javaClass.getMethod("provide${T::class.java.simpleName}")
                    .invoke(component) as T
        } catch (t: Throwable) {
            throw RuntimeException(
                    "No provide function with the name $functionName was found! " +
                            "is it already declared in the AppComponent?"
            )
        }
    }

    /**
     * Uses reflection to invoke an inject function for the
     * given object, if this function is not defined in the
     * [AppComponent] interface, it will throw a [RuntimeException].
     */
    fun inject(inversionObject: Any) {
        try {
            component.javaClass.getMethod("inject", inversionObject.javaClass)
                    .invoke(component, inversionObject)
        } catch (t: NoSuchMethodException) {
            throw RuntimeException(
                    "No inject function found for a " +
                            "${inversionObject.javaClass.name} instance. " +
                            "is it already added in the AppComponent?"
            )
        }
    }
}

@Singleton
@Component(modules = [ApiRestModule::class, RepositoryModule::class, ViewModelFactoryModule::class])
interface AppComponent {
    // Dependency inversion functions.
    /**
     * Provides injection for an implementation of the [UserApiService] interface.
     */
     fun provideUserApiService(): UserApiService
}

Crear un ViewModelFactory para inyectar las dependencias de Dagger en el ViewModel

Para hacer uso del UserRepository, es aconsejable crear la inyección de dependencias a través de un ViewModelFactory (ViewModelFactory.kt), para que las use Dagger, esto ayuda a mantener más limpio el código y permite usar el Factory directamente en el ViewModel.

import android.arch.lifecycle.ViewModel
import android.arch.lifecycle.ViewModelProvider
import android.content.Context

class ViewModelFactory(private val ctx: Context) : ViewModelProvider.Factory {
      @Suppress("UNCHECKED_CAST")
      override fun  create(modelClass: Class): T {
          return when {
                 modelClass.isAssignableFrom(UserViewModel::class.java) ->              
                 {
                      val userRepository = UserRepository(
                      Injector.component.provideUserApiService())
                      UserViewModel(userRepository) as T
                 }
                 else -> throw IllegalArgumentException("Unknown ViewModel class: ${modelClass::class.java}")
          }
       }
}

Creación de un ViewModelBase

Este procedimiento es útil solo si vamos a generar varios ViewModel. Si no le recomendamos omitir este paso. Consta de un ciclo de vida, y algunos métodos para el manejo de errores y suscripciones. Lo llamaremos ViewModelBase.kt.

import android.arch.lifecycle.*
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable

abstract class ViewModelBase : ViewModel(), LifecycleObserver {
    private val subscriptions = CompositeDisposable()

    val errorMessageNew: MutableLiveData = MutableLiveData()

    fun subscribe(disposable: Disposable): Disposable {
        subscriptions.add(disposable)
        return disposable
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    private fun dispose() {
        subscriptions.clear()
    }

    override fun onCleared() {
        dispose()
        super.onCleared()
    }

    fun setError(message: String?) {
        if (message == errorMessageNew.value) {
            return
        }
        errorMessageNew.value = message
    }

    fun clearError() {
        errorMessageNew.value?.let {
            errorMessageNew.value = null
        }
    }
}

Creación del ViewModel

Porfin hemos llegado a la parte emocionante, la obtención de la data en el ViewModel, el ViewModel se encarga de la parte del negocio, es decir la integración de la data obtenida por el API (o bien por parte de DAO, no visto en este tutorial) para posteriormente procesarla y enviarla al Activity o Fragment que despliega en la interfaz.

Para ello, vamos a construir nuestro ViewModel, inyectando las dependencias del Repository y que mágicamente obtenemos con el Factory. Así mismo requerimos del tipo MutableLiveData para asignar en forma asíncrona los valores a la variable una vez que se recuperan del API.

class UserViewModel
@Inject constructor(
    private var userRepository: UserRepository): ViewModelBase() {
        var usersList: MutableLiveData> = MutableLiveData()
        val usersCount: MutableLiveData = MutableLiveData()

        fun getUsers(){
           val getUsersInfo =  userRepository.getUsersList()
            .onErrorResumeNext(ErrorHandling>(DefaultError::class.java))
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe({
                usersList.postValue(it)
                usersCounter.value = it?.size
            }, { throwable ->
                usersCounter.value = 0
            })
            subscribe(getUsersInfo)
         }

         class Factory
         @Inject constructor(
              private val userRepository: UserRepository) : ViewModelProvider.Factory {
                @Suppress("UNCHECKED_CAST")
                override fun  create(modelClass: Class) = UserViewModel(userRepository) as T
         }
    }

Invocación desde el Activity o Fragment

Finalmente ponemos a trabajar el Observer una vez que el Activity se ha cargado (onCreate), o bien bajo acción de algún evento de click (setOnClickListener), o el evento deseado para llamar la obtención de la data del API. Se puede llamar con un Observador del estilo:

//Al actualizar el contador de usuarios
viewModel.usersCount.observe(this, Observer { it ->
    it?.let {
        updateCounter()
    }
})

//Al actualizar la Lista Mutable de Usuarios
viewModel.usersList.observe(this, Observer {
    loadUsers(it)
})

//etc

Conclusiones – Retrofit Android

Si te gustó este tutorial sobre Retrofit para Android, puedes compartirlo en tu blog, comunidad, red social u otro medio en forma libre y gratuita, así también puedes contribuir con tu propio tutorial y publicarlo en BitCuco.