Simple Login/Auth con Jetpack Compose usando una Rest Api con Retrofit.
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 deThread
delThread
principal aThreads
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 tipoString
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á elemail
ypassword
para poder autenticarse y dar una respuesta exitosa a través delToken.
- 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 elemail
ypassword.
interface AuthApiService {
@POST("auth")
suspend fun getLogin(@Body loginDto: LoginDto) :
Response<TokenDto>
}
- Adicional crearemos un
object
, que contendrá la implementación de la llamada aretrofit
y la base URL que implementamos enbuild.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 unBuild
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 claseViewModel()
, 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 Login
contendrá 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 cadaroute
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 NavigationScreen
en 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 ✌.