• Что бы вступить в ряды "Принятый кодер" Вам нужно:
    Написать 10 полезных сообщений или тем и Получить 10 симпатий.
    Для того кто не хочет терять время,может пожертвовать средства для поддержки сервеса, и вступить в ряды VIP на месяц, дополнительная информация в лс.

  • Пользаватели которые будут спамить, уходят в бан без предупреждения. Спам сообщения определяется администрацией и модератором.

  • Гость, Что бы Вы хотели увидеть на нашем Форуме? Изложить свои идеи и пожелания по улучшению форума Вы можете поделиться с нами здесь. ----> Перейдите сюда
  • Все пользователи не прошедшие проверку электронной почты будут заблокированы. Все вопросы с разблокировкой обращайтесь по адресу электронной почте : info@guardianelinks.com . Не пришло сообщение о проверке или о сбросе также сообщите нам.

User Authentication Flow with Compose Multiplatform - Part 1

Lomanu4 Оффлайн

Lomanu4

Команда форума
Администратор
Регистрация
1 Мар 2015
Сообщения
1,481
Баллы
155
In this guide you'll learn how to make a simple User Authentication Flow using Compose Multiplatform (for an Android mobile app and a Wasm web app) with it's Ktor Server

Before we begin


All of the code is in the support repositories. I'll try to only mention the relevant parts related to the navigation, so you can catch the overall idea. Please review the other classes if you have questions about the dependency injection using koin, the screens and ViewModels

This is a 3 parts guide

  • Part 1: Adding the navigation graph in the app (this guide)
  • Part 2: Handling user sessions and tokens in the app (TBD)
  • Part 3: Creating Ktor server to handle the authentication (TBD)
Support repositories

Overview of the project


This is the app we will be doing


Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.



Navigation graph



Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.



Navigation flow



Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.



Components



Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.



Getting started


The easiest way to start is by creating a new project with the

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.



Show me the code


So let's start by making the navigation graph


NavHost(
navController = navHostController,
startDestination = Init,
) {
composable<Init> {
// This is required so the ViewModel makes the first request to server
val initViewModel = koinViewModel<InitViewModel>()
InitScreen()
}

composable<Login> {
val loginViewModel = koinViewModel<LoginViewModel>()
LoginScreen(
onLogin = { username: String, password: String ->
loginViewModel.login(username, password)
}
)
}

// Nested navigation graph
navigation<Home>(startDestination = UserSettings) {
home()
}
}

And the nested graph


fun NavGraphBuilder.home() {
composable<UserSettings> {
val userSettingsViewModel = koinViewModel<UserSettingsViewModel>()
UserSettingsScreen(
onLogout = { userSettingsViewModel.logout() },
)
}
}

Using the NavController from the navigation library you can navigate from any composable easily doing something like navHostController.popBackStack() but because the ViewModels are the ones that do the requests to server to determine if the user is logged or not, how do we navigate from inside a ViewModel?

To do this we would need to implement 3 things:

  • NavigationController: This will emit NavigationEvents which are an abstraction on top of the navigation library
  • NavigationControllerObserver: This will listen to the navigation events emitted by the NavigationController and use the NavController to navigate. Basically converting them to the actual navigation
  • RegisterNavigationControllerObserver: Is an extension function that makes the observer, and connects it to the NavController and the NavigationController
NavigationController


/**
* Proxy to send and receive navigation events from the navController inside ViewModels
*/
interface NavigationController {
val navigationEvents: SharedFlow<NavigationEvent>

suspend fun sendNavigationEvent(navigationEvent: NavigationEvent)

sealed interface NavigationEvent {
data class Navigate(
val destinationRoute: Route,
val launchSingleTop: Boolean = false,
val restoreState: Boolean = false,
val popUpTo: PopUpTo? = null,
) : NavigationEvent {
data class PopUpTo(
val startRoute: Route,
val isInclusive: Boolean,
val saveState: Boolean,
)
}

data object PopBackStack : NavigationEvent
}
}

class DefaultNavigationController : NavigationController {
private val _navigationEvents = MutableSharedFlow<NavigationEvent>()
override val navigationEvents: SharedFlow<NavigationEvent> = _navigationEvents

override suspend fun sendNavigationEvent(navigationEvent: NavigationEvent) {
_navigationEvents.emit(navigationEvent)
}
}
NavigationControllerObserver


/**
* Observer that converts NavigationEvents from the [NavigationController] into actual navigation using the NavHost
*/
interface NavigationControllerObserver {
val navigationController: NavigationController
val navHostController: NavHostController

suspend fun subscribe() {
coroutineScope {
launch {
navigationController.navigationEvents.collect(::onNavigationEvent)
}
}
}

fun onNavigationEvent(navigationEvent: NavigationEvent)
}

class DefaultNavigationControllerObserver(
override val navigationController: NavigationController,
override val navHostController: NavHostController,
) : NavigationControllerObserver {

override fun onNavigationEvent(navigationEvent: NavigationEvent) {
when (navigationEvent) {
is Navigate -> {
navHostController.navigate(route = navigationEvent.destinationRoute) {
navigationEvent.popUpTo?.let { popUpTo ->
popUpTo(popUpTo.startRoute) {
inclusive = popUpTo.isInclusive
saveState = popUpTo.saveState
}
}
}
}

is PopBackStack -> navHostController.popBackStack()
}
}
}

@Composable
fun rememberNavigationObserver(
navigationController: NavigationController,
navHostController: NavHostController,
): NavigationControllerObserver =
remember {
DefaultNavigationControllerObserver(navigationController, navHostController)
}
Navigation extensions


fun popUpToInclusive(
startRoute: Route,
saveState: Boolean = false,
): NavigationEvent.Navigate.PopUpTo =
NavigationEvent.Navigate.PopUpTo(
startRoute = startRoute,
isInclusive = true,
saveState = saveState,
)

/**
* Convenience composable that connects the NavHost and the [NavigationController] by a [NavigationControllerObserver]
*/
@Composable
fun RegisterNavigationControllerObserver(
navigationController: NavigationController,
navHostController: NavHostController
) {
val navigationControllerObserver: NavigationControllerObserver =
rememberNavigationObserver(navigationController, navHostController)

LaunchedEffect(navHostController, navigationController, navigationControllerObserver) {
navigationControllerObserver.subscribe()
}
}

Let's see how this works with the LoginViewModel


class LoginViewModel(
// The data source to send requests to server
private val authDataSource: AuthDataSource,
// Some utility class to get the io and ui dispatchers
dispatchersProvider: DispatchersProvider,
// The NavigationController we created to navigate
private val navigationController: NavigationController,
) : ViewModel() {
// Not related to the navigation, but needed for the request
private var loginJob: Job? = null
private val ioScope = CoroutineScope(dispatchersProvider.io())

fun login(username: String, password: String) {
loginJob = ioScope.launch {
// We do the request to the server
val loginResult = authDataSource.login(username = username, password = password)
if (loginResult) {
// If the request to login was successful we navigate to Home
navigationController.login()
} else {
// For simplicity, we are not notifying the user if there was an error with the login
}
}
}

// Convenience extension to navigate to Home
private suspend fun NavigationController.login() {
sendNavigationEvent(
NavigationEvent.Navigate(
destinationRoute = Home,
launchSingleTop = true,
popUpTo = popUpToInclusive(startRoute = Login),
)
)
}

// More code...
}

This is how you navigate from inside a ViewModel. Remember to call the RegisterNavigationControllerObserver extension function that glues everything, just over your NavHost like this


RegisterNavigationControllerObserver(
navigationController = koinInject(),
navHostController = navHostController
)

NavHost(...)
Advantages of this approach


I've found this approach to be useful for the following reasons:

  • You have a single global entry-point which is the Init destination. Here you always check if the user is logged or not, then remove the destination, so when the user tries to go back, it exits the app
  • You can very easily add and remove screens by using nested navigation graphs
  • You have a unified component (the NavigationController) to do navigation from your non-compose code which is very handy and can even be mocked for unit tests
  • All the code is shared between Android and Wasm targets so the navigation is actually multiplatform but if you wanted you could adjust the NavigationController depending on the platform you are on
  • You can add events (like analytics or logs) on each navigation event if you wanted
Coming next


To make this into a proper production-ready solution, we would add a few improvements to the authentication flow:

  • A dependencies scope that only exists if the user is logged
  • Automatic navigation to Login destination if the user is logged out for any reason
  • Persistent session handling, so exiting the app doesn't logs out the user
  • BottomNavigation or NavigationRail when the user is logged in and depending of the screen size of the device
  • Token encryption for platforms that handle token based authentication


Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

 
Вверх Снизу