Simple Login/Auth con Jetpack Compose usando una Rest Api con Retrofit.

Luis David Orellana
6 min readDec 20, 2021

En este artículo, analizaremos como realizar un login con jetpack compose y hacer su autenticación a través de una Rest Api utilizando retrofit.

Estas list@s, comencemos con esta aventura 🚀

1️⃣ Permiso de Internet

Para poder obtener la respuesta de una Rest Api necesitaremos tener acceso a Internet, por lo que en el AndroidManifest.xml ingresaremos el siguiente permiso.

<uses-permission android:name="android.permission.INTERNET"/>

2️⃣ Importar Dependencias

  • Ingresamos la dependencia de Retrofit.
  • La dependencia logging interceptor me permitirá interceptar el código de estado de respuesta HTTP y así poder verificar en texto plano la solicitud.
  • La dependencia coroutine adapter lo que hará es gestionar los cambios de Thread del Thread principal a Threads secundarios.
  • La dependencia converter gson lo que hará es convertir objetos Json a kotlin y viceversa.
implementation "com.squareup.retrofit2:retrofit:2.9.0"
implementation "com.squareup.okhttp3:logging-interceptor:5.0.0-
alpha.3"
implementation "com.jakewharton.retrofit:retrofit2-kotlin-
coroutines-adapter:0.9.2"
implementation "com.squareup.retrofit2:converter-gson:2.9.0"
  • Adicionalmente ingresamos la dependencia de iconos de material que me permite obtener iconos adicionales.

Nota: Esta dependencia es permitida con versiones de compose superiores a 1.0.2

implementation "androidx.compose.material:material-icons-
extended:1.0.5"
  • Para poder hacer la navegación entre pantallas, necesitaremos la dependencia de Navigation Compose.
implementation "androidx.navigation:navigation-compose:2.4.0-beta02"
  • Finalmente ingresaremos la dependencia de viewmodel compose, la cual me permitirá almacenar y administrar los datos y tener optimizado el ciclo de vida de la actividad.
implementation 'androidx.lifecycle:lifecycle-viewmodel-
compose:2.4.0'

3️⃣ URL en build.gradle(module)

Una buena práctica para ingresar una URL de una Rest Api, es ingresando la URL en el build.gradle(module) en la sección de buildTypes.

  • release: Es para producción, cuando se va a subir la aplicación a la tienda.
  • debug: Se utiliza como variable de entorno para desarrollo.
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-
optimize.txt'), 'proguard-rules.pro'
buildConfigField "String", "API_BASE_URL",
"\"https://tasks-planner-api.herokuapp.com/\""
}
debug {
initWith debug
buildConfigField "String", "API_BASE_URL",
"\"https://tasks-planner-api.herokuapp.com/\""
}
}
  • Finalmente haremos un Sync Now al proyecto, para poder utilizar lo implementado anteriormente.

4️⃣ Implementación de datos

Data Transfer Object(DTO) es un patrón de diseño que nos permite devolver la estructura de datos que se van a consumir a través de un servicio rest.

  • Crearemos dos data class la cual almacenara la estructura de datos como los nombre de las variables y su tipo de dato que en este caso van hacer de tipo String que luego van a ser llamado a través de una api.
  • @SerializedName será la encargada de asignar un nombre de un campo Json válido, para ser llamado al servidor y así poder cambiar el nombre a la variable sin afectar a su llamado original.
data class LoginDto(
@SerializedName("email") val email: String,
@SerializedName("password") val password: String
)
data class TokenDto(
@SerializedName("accessToken") val accessTokenVerify: String
)

5️⃣ Implementación Retrofit

  • La Api de prueba es muy sencilla, contendrá los objetos Json que serán de tipo String, en su clave y valor. En su cuerpo contendrá el email y password para poder autenticarse y dar una respuesta exitosa a través del Token.
  • Creamos una interface de tipo @POST en la que colocaremos la ruta adicional al llamado de la URL (“auth”) y así poder consumir el endpoint.
  • @Body me permitirá controlar directamente la solicitud del cuerpo de la petición y así hacer un único llamado a sus datos, que en este caso contendrán el email y password.
interface AuthApiService {

@POST("auth")
suspend fun getLogin(@Body loginDto: LoginDto) :
Response<TokenDto>
}
  • Adicional crearemos un object, que contendrá la implementación de la llamada a retrofit y la base URL que implementamos en build.gradle(module)
  • La variable gson, lo que me permitirá es cambiar la fecha al formato estándar internacional ISO.
  • La variable loggingInterceptor, lo que hará es interceptar en consola el código de estado de respuesta HTTP solicitada.
object RetrofitHelper {    private val retrofit: Retrofit    init {
val gson = GsonBuilder().setDateFormat("yyyy-MM-
dd'T'HH:mm:ss").create()
val builder = Retrofit.Builder()
.baseUrl(BuildConfig.API_BASE_URL)
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(CoroutineCallAdapterFactory())
val loggingInterceptor = HttpLoggingInterceptor()
loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.writeTimeout(0, TimeUnit.MILLISECONDS)
.writeTimeout(2, TimeUnit.MINUTES)
.connectTimeout(1, TimeUnit.MINUTES).build()
retrofit = builder.client(okHttpClient).build()
}
fun getAuthService() : AuthApiService {
return retrofit.create(AuthApiService::class.java)
}
}

Nota: Si no se refleja el BuildConfig.API_BASE_URL haga un Build al proyecto. No se preocupe si Android Studio comienza a llorar 😂 lo que hará es obtener la ruta donde se encuentra la URL.

6️⃣ Implementación ViewModel

  • Crearemos una clase LoginViewModel que extenderá de la clase ViewModel(), la cual me ayudara a almacenar y administrar los datos.
class LoginViewModel : ViewModel() {    val isSuccessLoading = mutableStateOf(value = false)
val imageErrorAuth = mutableStateOf(value = false)
val progressBar = mutableStateOf(value = false)
private val loginRequestLiveData = MutableLiveData<Boolean>()
fun login(email: String, password: String) {
viewModelScope.launch(Dispatchers.IO) {
try {
progressBar.value = true
val authService = RetrofitHelper.getAuthService()
val responseService =
authService.getLogin(LoginDto(email = email,
password = password))
if (responseService.isSuccessful) {
delay(1500L)
isSuccessLoading.value = true
responseService.body()?.let { tokenDto ->
Log.d("Logging", "Response TokenDto:
$tokenDto")
}
} else {
responseService.errorBody()?.let { error ->
imageErrorAuth.value = true
delay(1500L)
imageErrorAuth.value = false
error.close()
}
}
loginRequestLiveData.postValue(responseService.isSuccessful)
progressBar.value = false
} catch (e: Exception) {
Log.d("Logging", "Error Authentication", e)
progressBar.value = false
}
}
}
}

7️⃣ Creación UI

Veamos como nos quedaría 👇

Puedes revisar el código en mi repositorio de GITHUB.

  • Para la creación de la UI Login, lo que hice fue separar los @Composable aplicando el patrón State Hoisting.
  • Nuestra función @Composable Logincontendrá los flujos unidireccional de datos, lo que quiere decir es que los estados bajan y los eventos suben, para hacer la ejecución de los elementos llamados. Por ejemplo la función @Composable EmailOutTextField 👇
  • A continuación crearemos una función @Composable Home la cual navegaremos solo si nuestra solicitud de código de respuesta HTTP es satisfactoria, es decir 200.
@Composable
fun Home() {
val listColorBackground = listOf(
Color(238, 113, 0, 255),
Color(101, 0, 126, 255),
Color(0, 47, 187),
)
Box(
modifier = Modifier
.fillMaxSize()
.background(
Brush.verticalGradient(
colors = listColorBackground
)
),
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(id =
R.drawable.ic_home_welcome),
contentDescription = "Icon Home",
modifier = Modifier.size(400.dp)
)
}
}

Nota: Si quieres saber como convertir una imagen SGV O PSD a formato XML, puedes revisar mi articulo anterior de como implementar un Splash Screen en compose, donde explico como hacerlo.

8️⃣ Navegación entre pantallas

  • Una buena practica para obtener las rutas de destino, es creando una sealed class que contendrá las referencias de cada route y así poder instanciarlas en un futuro.
sealed class Destination(val route: String) {
object Login : Destination(route = "login")
object Home : Destination(route = "home")
companion object {
fun getStartDestination() = Login.route
}
}
  • A continuación referenciamos nuestro viewModel en el constructor principal, para poder obtener el estado de los datos de cada solicitud.
@Composable
fun NavigationScreen(viewModel: LoginViewModel) {
val navController = rememberNavController()
val loadingProgressBar = viewModel.progressBar.value
val imageError = viewModel.imageErrorAuth.value
NavHost(
navController = navController,
startDestination = Destination.getStartDestination()
) {
composable(route = Destination.Login.route) {
if (viewModel.isSuccessLoading.value) {
LaunchedEffect(key1 = Unit) {
navController.navigate(route =
Destination.Home.route) {
popUpTo(route = Destination.Login.route) {
inclusive = true
}
}
}
} else {
Login(
loadingProgressBar = loadingProgressBar,
onclickLogin = viewModel::login,
imageError = imageError
)
}
}
composable(route = Destination.Home.route) {
Home()
}
}
}

8️⃣ Ejecución App

Finalmente para poder ejecutar la aplicación implementaremos la función @Composable NavigationScreenen el setContent con el llamado a su viewModel.

class MainActivity : ComponentActivity() {private val viewModel: LoginViewModel by viewModels()override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
LoginComposeRetrofit2Theme {
Surface(color = MaterialTheme.colors.background) {
NavigationScreen(viewModel = viewModel)
}
}
}
}
}

Visualizamos 👀

  • En el Logcat visualizaremos el código de respuesta HTTP 200 que significa que la solicitud ha tenido éxito ⭐.

¡Muchas Gracias por leer! 🤗💙

Puedes consultar el código en mi repositorio de GITHUB.

Regálame un 👏 si te gusto 🤗 y siéntete libre de dejar tus comentarios para analizarlos juntos. Nos vemos en la próxima ✌.

--

--