Implementando fluxo de login com o Navigation no Jetpack Compose
Introdução
Na maioria dos Apps, é muito comum termos uma tela de autenticação que recebe as informações para entrar no App (login e senha) e uma tela de registro que permite cadastrar novos usuários para conseguir autenticar. Esse tipo de implementação também é conhecida como fluxo de login ou autenticação.
O funcionamento de todo o mecanismo exige uma combinação de implementação, desde as telas, lógica de cadastro e, principalmente, a navegação entre as telas. Dados esses pontos, neste artigo vamos aprender como implementar um fluxo de autenticação com o Navigation do Jetpack Compose. Em outras palavras, não vamos explorar o fluxo de cadastro de usuário.
É válido ressaltar que o fluxo de autenticação pode variar em sua complexidade, portanto, o exemplo que abordaremos aqui focará apenas no fluxo de navegação entre a tela de autenticação e tela inicial do App.
Projeto a ser utilizado
Para abordamos o tema de uma forma bem prática, iremos utilizar o App Panucci como exemplo, um projeto Android desenvolvido no curso de Navigation no Jetpack Compose. Conheça como ele funciona, no gif abaixo:
Para conferir mais detalhes do projeto, você pode acessar o repositório no GitHub. Também, vamos utilizar um composable para representar a tela de autenticação que vai receber o usuário e senha:
AuthenticationScreen
O código da tela está disponível no repositório do GitHub.
Agora que conhecemos o projeto prático, vamos começar a implementação de fato!
Adicionando os destinos no grafo de navageção
Como primeiro passo, você precisa registrar as telas no grafo de navegação que está na MainActivity
:
NavHost(
navController = navController,
startDestination = AppDestination.Highlight.route
) {
composable(AppDestination.Authentication.route) {
AuthenticationScreen()
}
...
}
Neste projeto, é utilizado o padrão de constantes do AppDestination
para determinar as rotas de navegação, então a implementação de AppDestination.Authentication
fica da seguinte maneira:
sealed class AppDestination(val route: String) {
object Highlight : AppDestination("highlight")
object Menu : AppDestination("menu")
object Drinks : AppDestination("drinks")
object ProductDetails : AppDestination("productDetails")
object Checkout : AppDestination("checkout")
object Authentication : AppDestination("authentication")
}
Entendendo a lógica para o fluxo de login
Com o destino declarado, é preciso determinar a lógica utilizada para definir qual tela deve ser apresentada ao rodar o App. Para fazer isso, é necessário uma reflexão sobre os seguintes pontos:
- A tela de autenticação deve ser apresentada:
- Ao entrar no App pela primeira vez;
- Ao entrar no App sem usuário autenticado.
- O destino inicial deve ser apresentado:
- Ao autenticar usuário;
- Ao entrar no App com usuário autenticado.
Observe que, para ambos os casos, precisamos de uma técnica para armazenar uma informação se o usuário está ou não autenticado. Sendo assim, podemos utilizar uma ferramenta de persistência de dados recomendada no Android, o DataStore.
Com o DataStore, temos a capacidade de armazenar informações primitivas como strings, inteiros, booleanos etc. Dessa forma, usamos essas informações para sinalizar se um usuário foi ou não autenticado; é isso que vamos ensinar você a seguir.
Adicionando o DataStore no projeto Android
Para utilizar o DataStore, você precisa adicionar a lib como dependência no build.gradle
do módulo app:
dependencies {
...
implementation("androidx.datastore:datastore-preferences:1.0.0")
}
E após fazer a sincronização, você pode fazer a primeira configuração para utilizar o DataStore. Basicamente, adicione uma função de extensão em um arquivo dentro do projeto. Você pode definir onde preferir, mas, nesse exemplo, deixarei no arquivo AppPreferences.kt
no pacote preferences
:
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore
val Context.dataStore: DataStore<Preferences>
by preferencesDataStore(name = "login")
Agora o DataStore é acessível em todo o projeto a partir de um Context
. Vamos para a próxima etapa!
Configurando o fluxo inicial de navegação
Dado que o fluxo inicial do App é apresentar a tela de destaques do dia, é preciso ter a certeza de que o usuário está autenticado. Para confirmar isso, podemos utilizar o dataStore
que foi configurado dentro do destino. Para fazer a leitura no DataStore, é necessário o seguinte código:
composable(AppDestination.Highlight.route) {
val userPreferences = stringPreferencesKey("usuario_logado")
val context = LocalContext.current
var user: String? by remember {
mutableStateOf(null)
}
LaunchedEffect(null) {
user = context.dataStore.data.first()[userPreferences]
}
HighlightsListScreen(
...
)
}
Em resumo, o código acima faz o seguinte:
- Pega uma chave de Preferences para o tipo
String
; - Utiliza o contexto disponível do composable para poder usar o DataStore;
- Declara um estado do tipo
String?
que vai permitir identificar se o usuário foi autenticado ou não; - Usa o
LaunchedEffect(null)
para rodar o código de coroutines apenas uma vez:- Tenta buscar o usuário a partir do DataStore utilizando o
first()
da API do Flow e atribui o valor encontrado
- Tenta buscar o usuário a partir do DataStore utilizando o
Em outras palavras, com essa configuração, a variável user
irá determinar o que se deve fazer no fluxo de navegação.
Você pode, por exemplo, adicionar uma condição para navegar para a tela de login caso for nulo; caso contrário, que apresenta o conteúdo da tela de destaques:
user?.let {
HighlightsListScreen(
products = sampleProducts,
onNavigateToDetails = { product ->
navController.navigate(
"${AppDestination.ProductDetails.route}/${product.id}"
)
},
onNavigateToCheckout = {
navController.navigate(AppDestination.Checkout.route)
},
)
} ?: LaunchedEffect(null) {
navController.navigate(AppDestination.Authentication.route) {
popUpTo(navController.graph.findStartDestination().id) {
inclusive = true
}
}
}
Qualquer navegação em código de composição deve ser feito com o uso da API de Effect.
Apenas essa verificação não é o suficiente! Pois o resultado imediato do user
será sempre nulo inicialmente! Em outras palavras, o App sempre vai voltar para a tela de autenticação, mesmo que exista um valor armazenado.
Sendo assim, é preciso aplicar uma técnica que permita identificar se a leitura já foi feita, para então verificar se existe o usuário ou não e realizar a ação esperada. Vamos ver como fazer isso.
Estado do dado: técnica que verifica se o usuário já foi carregado
A técnica para verificar se o usuário foi carregado - que podemos utilizar - é o estado do dado, ou seja, criamos mais uma variável que servirá como uma flag ou sinalizador do usuário. Basicamente, ela vai determinar estados esperados para tomar uma ação, por exemplo, o estados de carregamento e o de finalização.
Para determinar o estado, podemos recorrer a várias técnicas, desde tipos primitivos como inteiros ou strings até enums ou sealed objects. Para simplificar o exemplo, vou considerar o uso de string:
var dataState by remember {
mutableStateOf("loading")
}
LaunchedEffect(null) {
user = context.dataStore.data.first()[userPreferences]
dataState = "finished"
}
Então, podemos utilizar o dataState
junto com um when
para determinar o conteúdo que será exibido com base no estado:
composable(AppDestination.Highlight.route) {
val userPreferences = stringPreferencesKey("usuario_logado")
val context = LocalContext.current
var user: String? by remember {
mutableStateOf(null)
}
var dataState by remember {
mutableStateOf("loading")
}
LaunchedEffect(null) {
user = context.dataStore.data.first()[userPreferences]
dataState = "finished"
}
when (dataState) {
"loading" -> {
Box(modifier = Modifier.fillMaxSize()) {
Text(
text = "Carregando...",
Modifier
.fillMaxWidth()
.align(Alignment.Center),
textAlign = TextAlign.Center
)
}
}
"finished" -> {
user?.let {
HighlightsListScreen(
products = sampleProducts,
onNavigateToDetails = { product ->
navController.navigate(
"${AppDestination.ProductDetails.route}/${product.id}"
)
},
onNavigateToCheckout = {
navController.navigate(AppDestination.Checkout.route)
},
)
} ?: LaunchedEffect(null) {
navController.navigate(AppDestination.Authentication.route) {
popUpTo(navController.graph.findStartDestination().id) {
inclusive = true
}
}
}
}
}
}
Ao rodar o App, deve ser apresentada a tela de autenticação, e também, todo o grafo de navegação deve ser removido da back stack, fazendo com que a tela de autenticação seja o único destino:
Você pode validar esse comportamento analisando o logcat que faz a impressão da back stack atual:
I onCreate: back stack - [null, authentication]
Ou então, pode apenas fazer uma navegação de volta e verificar se o App fecha.
Um outro teste interessante é adicionar um delay()
antes do first()
para visualizar o conteúdo de carregamento antes de entrar na tela de autenticação:
LaunchedEffect(null) {
val randomMillis = Random.nextLong(500, 1000)
delay(randomMillis)
user = context.dataStore.data.first()[userPreferences]
dataState = "finished"
}
Para o teste, pode ser um intervalo de um a meio segundo:
Observe que os componentes do
Scaffold
ainda são visíveis, pois oNavHost
faz parte do conteúdo doScaffold
, portanto, você poderia considerar o estado do dado como mais um parâmetro para determinar se deve exibir ou não os componentes doScaffold
.
Agora que a tela inicial foi configurada, você pode começar a implementação para autenticar o usuário.
Indicando a autenticação do usuário
Para autenticar o usuário com o DataStore, você precisa apenas salvar alguma informação em String
, pois é o tipo de dado que vamos usar para esse exemplo, portanto, podemos pegar o usuário a partir do campo de texto da tela de autenticação e salvar no DataStore:
composable(AppDestination.Authentication.route) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
AuthenticationScreen(
onEnterClick = { user ->
val userPreferences = stringPreferencesKey("usuario_logado")
scope.launch {
context.dataStore.edit {
it[userPreferences] = user
}
}
}
)
}
Um detalhe importante é que a chave de preferência precisa ser exatamente a mesma! Neste caso, o ideal é criá-la da mesma forma que se faz com o DataStore no arquivo AppPreferences.kt
:
val Context.dataStore: DataStore<Preferences>
by preferencesDataStore(name = "login")
val userPreferences = stringPreferencesKey("usuario_logado")
Com esse ajuste, você pode apenas usar o userPreferences
, seja na escrita ou leitura. Concluído esse passo, vamos ao próximo.
Navegando para a tela de inicial
Então, após salvar o usuário no DataStore, você precisa navegar para a tela inicial considerando a mesma técnica de limpeza da back stack:
composable(AppDestination.Authentication.route) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
AuthenticationScreen(
onEnterClick = { user ->
scope.launch {
context.dataStore.edit {
it[userPreferences] = user
}
}
navController.navigate(AppDestination.Highlight.route) {
popUpTo(navController.graph.id)
}
}
}
)
}
Pronto, isso é suficiente para rodar o App e manter a tela esperada com o usuário autenticado:
A seguir, precisamos adicionar uma opção para que as pessoas consigam sair do aplicativo.
Adicionando a opção para sair do App
Dado que o usuário foi autenticado uma vez, não é mais possível simular o comportamento do fluxo de autenticação, pois o usuário foi salvo no DataStore e não temos nenhuma funcionalidade que remova-o.
Sendo assim, vamos adicionar o comportamento para deslogar. Para adicionar esse comportamento, podemos reagir a um evento de clique de um elemento visual, por exemplo, um menu na top app bar, portanto, vamos implementá-lo:
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PanucciApp(
bottomAppBarItemSelected: BottomAppBarItem = bottomAppBarItems.first(),
onBottomAppBarItemSelectedChange: (BottomAppBarItem) -> Unit = {},
onFabClick: () -> Unit = {},
onLogout: () -> Unit = {},
isShowTopBar: Boolean = false,
isShowBottomBar: Boolean = false,
isShowFab: Boolean = false,
content: @Composable () -> Unit
) {
Scaffold(
topBar = {
if (isShowTopBar) {
CenterAlignedTopAppBar(
title = {
Text(text = "Ristorante Panucci")
},
actions = {
IconButton(onClick = onLogout) {
Icon(
Icons.Filled.ExitToApp,
contentDescription = "sair do app"
)
}
}
)
}
},
...
) {
Box(
modifier = Modifier.padding(it)
) {
content()
}
}
}
Então, ajuste a MainActivity
para poder utilizar o DataStore
no PanucciApp
:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val navController = rememberNavController()
...
PanucciTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
...
PanucciApp(
...
onLogout = {
scope.launch {
context.dataStore.edit {
it.remove(userPreferences)
}
}
navController.navigate(AppDestination.Authentication.route) {
popUpTo(navController.graph.findStartDestination().id) {
inclusive = true
}
}
}
) {
NavHost(
navController = navController,
startDestination = AppDestination.Highlight.route
) {
...
}
}
}
}
}
}
}
Com esse ajuste, o App apresenta o menu que, ao ser clicado, volta para a tela de login e mantém o fluxo esperado quando o usuário não é autenticado:
Pronto! Concluímos a implementação de uma parte do fluxo de autenticação de usuário no app Panucci - tudo isso com o Jetpack Compose!
Para saber mais: Melhorias na implementação
Dado que o objetivo do artigo era focar na implementação do fluxo de login, não foram consideradas diversas técnicas e boas práticas no código de navegação.
Caso você tenha interesse em aplicar essa melhorias, sugerimos o uso do Type Safety com a DSL do Kotlin em conjunto com o gerenciamento de estados com ViewModel. Você pode encontrar esses conteúdos na documentação do Navigation, se preferir, também você pode conferir o curso de Type Safety no Navigation da Alura que foca nesses detalhes.
Desafio: Funcionalidades para o fluxo de login
Além do que abordamos neste artigo, você também pode adicionar novas funcionalidades ao aplicativo, por exemplo, criar uma tela de cadastro de usuário que permite salvar usuário com senha e salvar as informações em um banco de dados local. Então, pode integrar o banco de dados com a tela de autenticação para entrar apenas se o usuário e senha corresponderem.
Interessante, não é? Será que você topa esse desafio? Se conseguir realizar, compartilha nas redes sociais e me marca 😉:
Conclusão
Neste artigo, abordamos como implementar um fluxo de login no Navigation com o Jetpack Compose. Nesta implementação aprendemos:
- As estratégias do fluxo de autenticação;
- Como salvar o estado de usuário logado com o DataStore;
- Configurar a navegação condicional para exibir o destino inicial ou a tela de autenticação;
- Limpar adequadamente a back stack dependendo da navegação realizada;
- Implementar o comportamento para sair do App.
Se você se interessa pelo tema do Jetpack Compose e quer mergulhar e aprender ainda mais sobre essa tecnologia, recomendamos que conheça a Formação Jetpack Compose: criando telas e gerenciando estados.
Bons estudos e até a próxima!