Fluxo de login com Navigation no Jetpack Compose

Alex Felipe
Alex Felipe

Compartilhe

Avalie este artigo

24 minutos de leitura

Em grande parte dos aplicativos, é comum existir uma tela de autenticação, em que a pessoa usuária insere login e senha, e uma tela de cadastro, que permite registrar novos usuários para autenticação. Esse conjunto é conhecido como fluxo de login ou fluxo de 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. 

Se deseja um guia completo sobre como manter usuário logado Android, salvar dados de autenticação com DataStore Compose, criar tela de login Jetpack Compose e realizar logout (sair do app), este artigo cobre todos esses pontos passo a passo.

Exemplo prático: fluxo de login e autenticação Jetpack Compose no projeto Panucci 

Para abordar o tema de maneira prática, utilizaremos como exemplo o App Panucci, um projeto Android desenvolvido no curso de Navigation no Jetpack Compose. Veja como ele funciona no GIF abaixo: 

App Panucci em execução realizando a navegação das telas. Primeiro a tela de destaque, menu e bebidas a partir de uma barra de app inferior, depois a navegação da tela de detalhes ao clicar em um dos itens de produto a partir da tela de destaque. Por fim, é acessada a tela de pedido ao clicar no botão pedir a partir da tela de detalhes do produto.

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 

Tela de autenticação com a mensagem “welcome to Panucci”, dois campos de texto para inserir o usuário e senha e dois botões para entrar no app e registrar.

O código da tela está disponível no repositório do GitHub

Agora que conhecemos o projeto prático, vamos iniciar a implementação. 

Banner da Alura convidando profissionais a desenvolver habilidades em inteligência artificial para acompanhar as transformações do mercado de tecnologia. A campanha destaca que quem utiliza IA produz mais, cresce na carreira e se torna mais competitivo, reforçando que a inteligência artificial deixou de ser tendência e passou a ser uma habilidade essencial. O banner incentiva a começar a aprender IA com os cursos da Alura e impulsionar a transformação digital.

Como registrar telas de login e navegação entre telas no Jetpack Compose 

Como primeiro passo, é necessário registrar as telas no grafo de navegação localizado na MainActivity

KOTLIN  
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: 

KOTLIN  
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 no Jetpack Compose e manutenção do estado do usuário logado 

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ária 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 estar 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 uma pessoa está autenticada ou não. A seguir, vamos mostrar como implementar isso. 

Adicionando e usando DataStore no Jetpack Compose para salvar dados de autenticação 

Para utilizar o DataStore, você precisa adicionar a lib como dependência no build.gradle do módulo app: 

GROOVY   dependencies {  ...  implementation("androidx.datastore:datastore-preferences:1.2.1")  }

Após a sincronização, o próximo passo é realizar a configuração inicial para utilizar o DataStore. Para isso, adicione uma função de extensão em um arquivo do projeto. No exemplo, a função será criada no arquivo AppPreferences.kt, no pacote preferences

KOTLIN  
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 está acessível em todo o projeto por meio de um Context. Vamos avançar para a próxima etapa. 

Como definir tela inicial e navegar com type safety no Navigation Compose 

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: 

KOTLIN  
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. 

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: 

KOTLIN  
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 feita com o uso da API de Effect. 

Apenas essa verificação nã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 

Uma técnica para verificar se o usuário foi carregado é utilizar o "estado do dado"; para isso, criamos uma variável que servirá como indicador de status. Essa variável indicará, por exemplo, se os dados estão em carregamento ou se já foram carregados. 

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: 

KOTLIN  
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: 

KOTLIN  
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: 

Entrando no App Panucci e apresentado a tela de autenticação. Ao fazer a navegação de App, volta para a tela inicial do dispositivo.

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: 

KOTLIN  
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: 

App Panucci em execução, apresenta a tela inicial indicando carregamento em alguns segundos, e então, navega para a tela de autenticação.

Observe que os componentes do Scaffold ainda são visíveis, pois o NavHost faz parte do conteúdo do Scaffold. Portanto, você poderia considerar o estado do dado como mais um parâmetro para determinar se deve exibir ou não os componentes do Scaffold

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: 

KOTLIN  
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

KOTLIN  
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 na leitura. Concluído esse passo, vamos ao próximo. 

Uma dúvida muito comum é: como limpar a back stack Compose após o login? Ao navegar para a tela inicial (Home), utilize popUpTo com inclusive = true. Isso remove todas as telas anteriores, garantindo que o usuário não retorne à tela de login Jetpack Compose após estar autenticado.

KOTLIN  
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) 
         } 
         } 
     } 
) 
}

Com isso, já é possível rodar o aplicativo e garantir que a tela correta será exibida se a pessoa estiver autenticada: 

App Panucci em execução, abre a tela de autenticação primeiro, digita um usuário, no exemplo o ‘alex’, clica em ‘enter’ e abre a tela inicial do App. Ao fechar o App e abrir novamente, apresenta a tela inicial ao invés da tela de autenticação.

A seguir, precisamos adicionar uma opção para que as pessoas consigam sair do aplicativo. 

Como implementar logout no Jetpack Compose e resetar autenticação 

Após a autenticação, não é mais possível simular o fluxo de login, pois os dados da pessoa usuária foram salvos no DataStore e ainda não existe funcionalidade para removê-los. 

Vamos, então, adicionar a funcionalidade de logout. A ideia é reagir a um clique em algum elemento visual, como um item de menu na barra superior. Veja como implementar essa funcionalidade: 

KOTLIN  
@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

KOTLIN  
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 
                     ) { 
... 
}                             
                 } 
             } 
         } 
     } 
} 
}

Após esse ajuste, o aplicativo exibirá um menu que, ao ser clicado, permite sair da conta e retorna para a tela de login, mantendo o fluxo correto quando a pessoa não estiver autenticada: 

App Panucci em execução, ao entrar na tela inicial aparece o ícone na barra do app superior. O ícone indica a saída do App e, ao clicá-lo, ele volta para a tela de autenticação, ao sair do App e abrir novamente, apresenta a tela de autenticação.

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! 

Entenda navigation compose type safe e aprofunde em melhores práticas de navegação Jetpack Compose 

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 haja interesse em aprimorar ainda mais a implementação, recomenda-se o uso de Type Safety com a DSL do Kotlin junto ao gerenciamento de estados via ViewModel. Esses temas são abordados na documentação oficial do Navigation e no curso de Type Safety no Navigation da Alura. 

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 permita 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. 

Que tal encarar esse desafio? Se conseguir implementar, compartilhe sua experiência nas redes sociais e marque o autor: 

Conclusão 

Neste artigo, exploramos como implementar o fluxo de login utilizando Navigation no Jetpack Compose. Nesta implementação, vimos: 

  • 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, ótima porta de entrada para quem quer aprender a criar interfaces Android com Compose, trabalhando formulários, gerenciamento de estados e MVVM com ViewModel e StateFlow.  

Para evoluir, vale seguir com a Formação Jetpack Compose: navegação de telas, boas práticas e testes, que ensina a organizar a navegação entre telas e deixar seus apps mais profissionais, e depois aprofundar em persistência de dados com a Formação Jetpack Compose: armazenamento de dados e arquivos e com o curso Jetpack Compose: armazenamento de dados internos, usando bibliotecas queridinhas do mercado como Room, DataStore e Retrofit. 

Se preferir começar pelo básico antes de encarar o Compose, dê uma olhada na formação Desenvolva seu primeiro app Android com Kotlin, que consolida o Kotlin no contexto Android e já prepara o terreno para os próximos passos. 

Bons estudos e até a próxima! 

Avalie este artigo

Alex Felipe
Alex Felipe

Alex é instrutor e desenvolvedor e possui experiência em Java, Kotlin, Android. Atualmente cria conteúdo no canal https://www.youtube.com/@AlexFelipeDev.

Veja outros artigos sobre Mobile